abxbus 2.4.31 → 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/BaseEvent.ts CHANGED
@@ -13,11 +13,23 @@ import {
13
13
  withResolvers,
14
14
  } from './LockManager.js'
15
15
  import { _runWithTimeout } from './timing.js'
16
- import { extractZodShape, normalizeEventResultType, toJsonSchema } from './types.js'
16
+ import { isZodSchema, normalizeEventResultType, toJsonSchema } from './types.js'
17
17
  import type { EventHandlerCallable, EventResultType } from './types.js'
18
18
  import { monotonicDatetime } from './helpers.js'
19
19
 
20
- const RESERVED_USER_EVENT_FIELDS = new Set(['bus', 'emit', 'first', 'toString', 'toJSON', 'fromJSON'])
20
+ const RESERVED_USER_EVENT_FIELDS = new Set([
21
+ 'bus',
22
+ 'emit',
23
+ 'wait',
24
+ 'now',
25
+ 'eventResult',
26
+ 'eventResultsList',
27
+ 'toString',
28
+ 'toJSON',
29
+ 'fromJSON',
30
+ ])
31
+
32
+ const EVENT_TYPE_REGISTRY = new Map<string, typeof BaseEvent>()
21
33
 
22
34
  function assertNoReservedUserEventFields(data: Record<string, unknown>, context: string): void {
23
35
  for (const field_name of RESERVED_USER_EVENT_FIELDS) {
@@ -43,6 +55,18 @@ function assertNoModelPrefixedFields(data: Record<string, unknown>, context: str
43
55
  }
44
56
  }
45
57
 
58
+ function isRecord(value: unknown): value is Record<string, unknown> {
59
+ return !!value && typeof value === 'object' && !Array.isArray(value)
60
+ }
61
+
62
+ function isZodObjectSchema(value: unknown): value is z.ZodObject<z.ZodRawShape> {
63
+ return (
64
+ isZodSchema(value) &&
65
+ typeof (value as { safeExtend?: unknown }).safeExtend === 'function' &&
66
+ isRecord((value as { shape?: unknown }).shape)
67
+ )
68
+ }
69
+
46
70
  function compareIsoDatetime(left: string | null | undefined, right: string | null | undefined): number {
47
71
  const left_value = left ?? ''
48
72
  const right_value = right ?? ''
@@ -58,10 +82,10 @@ export const BaseEventSchema = z
58
82
  event_created_at: z.string().datetime(),
59
83
  event_type: z.string(),
60
84
  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(),
85
+ event_timeout: z.number().nonnegative().nullable(),
86
+ event_slow_timeout: z.number().nonnegative().nullable().optional(),
87
+ event_handler_timeout: z.number().nonnegative().nullable().optional(),
88
+ event_handler_slow_timeout: z.number().nonnegative().nullable().optional(),
65
89
  event_blocks_parent_completion: z.boolean().optional(),
66
90
  event_parent_id: z.string().uuid().nullable().optional(),
67
91
  event_path: z.array(z.string()).optional(),
@@ -79,6 +103,7 @@ export const BaseEventSchema = z
79
103
  .loose()
80
104
 
81
105
  const KNOWN_BASE_EVENT_FIELDS = new Set(Object.keys(BaseEventSchema.shape))
106
+ type AnyEventSchema = z.ZodTypeAny
82
107
 
83
108
  export type BaseEventData = z.infer<typeof BaseEventSchema>
84
109
  export type BaseEventJSON = BaseEventData & Record<string, unknown>
@@ -109,12 +134,15 @@ type BaseEventFields = { [K in BaseEventFieldName]: BaseEventData[K] }
109
134
  export type BaseEventInit<TFields extends Record<string, unknown>> = TFields & Partial<BaseEventFields>
110
135
 
111
136
  type BaseEventSchemaShape = typeof BaseEventSchema.shape
112
-
113
137
  export type EventSchema<TShape extends z.ZodRawShape> = z.ZodObject<BaseEventSchemaShape & TShape>
114
138
  type EventPayload<TShape extends z.ZodRawShape> = TShape extends Record<string, never> ? {} : z.infer<z.ZodObject<TShape>>
115
139
 
116
140
  type EventInput<TShape extends z.ZodRawShape> = z.input<EventSchema<TShape>>
117
141
  export type EventInit<TShape extends z.ZodRawShape> = Omit<EventInput<TShape>, keyof BaseEventFields> & Partial<BaseEventFields>
142
+ type EventPayloadFromSchema<TSchema extends AnyEventSchema> = z.output<TSchema> extends Record<string, unknown> ? z.output<TSchema> : {}
143
+ type EventInputFromSchema<TSchema extends AnyEventSchema> = z.input<TSchema> extends Record<string, unknown> ? z.input<TSchema> : never
144
+ export type EventInitFromSchema<TSchema extends AnyEventSchema> = Omit<EventInputFromSchema<TSchema>, keyof BaseEventFields> &
145
+ Partial<BaseEventFields>
118
146
 
119
147
  type EventWithResultSchema<TResult> = BaseEvent & { __event_result_type__?: TResult }
120
148
 
@@ -133,18 +161,22 @@ type ResultTypeFromEventResultTypeInput<TInput> = TInput extends z.ZodTypeAny
133
161
  : unknown
134
162
 
135
163
  type ResultSchemaFromShape<TShape> = TShape extends { event_result_type: infer S } ? ResultTypeFromEventResultTypeInput<S> : unknown
136
- type EventResultsListInclude<TEvent extends BaseEvent> = (
137
- result: EventResultType<TEvent> | undefined,
164
+ export type EventResultInclude<TEvent extends BaseEvent> = (
165
+ result: EventResult<TEvent>['result'],
138
166
  event_result: EventResult<TEvent>
139
167
  ) => boolean
140
- type EventResultsListOptions<TEvent extends BaseEvent> = {
141
- timeout?: number | null
142
- include?: EventResultsListInclude<TEvent>
168
+ export type EventResultOptions<TEvent extends BaseEvent> = {
169
+ include?: EventResultInclude<TEvent>
143
170
  raise_if_any?: boolean
144
171
  raise_if_none?: boolean
145
172
  }
146
- type EventDoneOptions = {
147
- raise_if_any?: boolean
173
+ export type EventWaitOptions = {
174
+ timeout?: number | null
175
+ first_result?: boolean
176
+ }
177
+ export type EventWaitPromise<TEvent extends BaseEvent> = Promise<TEvent> & {
178
+ eventResult(options?: EventResultOptions<TEvent>): Promise<EventResultType<TEvent> | undefined>
179
+ eventResultsList(options?: EventResultOptions<TEvent>): Promise<Array<EventResultType<TEvent> | undefined>>
148
180
  }
149
181
  type EventResultUpdateOptions<TEvent extends BaseEvent> = {
150
182
  eventbus?: EventBus
@@ -153,13 +185,12 @@ type EventResultUpdateOptions<TEvent extends BaseEvent> = {
153
185
  error?: unknown
154
186
  }
155
187
 
156
- const EVENT_CLASS_DEFAULTS = new WeakMap<Function, Record<string, unknown>>()
157
188
  const ROOT_EVENTBUS_ID = '00000000-0000-0000-0000-000000000000'
158
189
 
159
190
  export type EventFactory<TShape extends z.ZodRawShape, TResult = unknown> = {
160
191
  (data: EventInit<TShape>): EventWithResultSchema<TResult> & EventPayload<TShape>
161
192
  new (data: EventInit<TShape>): EventWithResultSchema<TResult> & EventPayload<TShape>
162
- schema: EventSchema<TShape>
193
+ event_schema: EventSchema<TShape>
163
194
  class?: new (data: EventInit<TShape>) => EventWithResultSchema<TResult> & EventPayload<TShape>
164
195
  event_type?: string
165
196
  event_version?: string
@@ -167,6 +198,17 @@ export type EventFactory<TShape extends z.ZodRawShape, TResult = unknown> = {
167
198
  fromJSON?: (data: unknown) => EventWithResultSchema<TResult> & EventPayload<TShape>
168
199
  }
169
200
 
201
+ export type SchemaEventFactory<TSchema extends AnyEventSchema, TResult = unknown> = {
202
+ (data: EventInitFromSchema<TSchema>): EventWithResultSchema<TResult> & EventPayloadFromSchema<TSchema>
203
+ new (data: EventInitFromSchema<TSchema>): EventWithResultSchema<TResult> & EventPayloadFromSchema<TSchema>
204
+ event_schema: TSchema
205
+ class?: new (data: EventInitFromSchema<TSchema>) => EventWithResultSchema<TResult> & EventPayloadFromSchema<TSchema>
206
+ event_type?: string
207
+ event_version?: string
208
+ event_result_type?: z.ZodTypeAny
209
+ fromJSON?: (data: unknown) => EventWithResultSchema<TResult> & EventPayloadFromSchema<TSchema>
210
+ }
211
+
170
212
  type ZodShapeFrom<TShape extends Record<string, unknown>> = {
171
213
  [K in keyof TShape as K extends 'event_result_type' ? never : TShape[K] extends z.ZodTypeAny ? K : never]: Extract<
172
214
  TShape[K],
@@ -174,6 +216,130 @@ type ZodShapeFrom<TShape extends Record<string, unknown>> = {
174
216
  >
175
217
  }
176
218
 
219
+ function baseEventDefaultShape(event_type: string): z.ZodRawShape {
220
+ return {
221
+ event_id: z.string().uuid(),
222
+ event_created_at: z.string().datetime(),
223
+ event_type: z.string().default(event_type),
224
+ event_version: z.string().default('0.0.1'),
225
+ event_timeout: z.number().nonnegative().nullable().default(null),
226
+ event_slow_timeout: z.number().nonnegative().nullable().optional(),
227
+ event_handler_timeout: z.number().nonnegative().nullable().optional(),
228
+ event_handler_slow_timeout: z.number().nonnegative().nullable().optional(),
229
+ event_blocks_parent_completion: z.boolean().default(false),
230
+ event_parent_id: z.string().uuid().nullable().optional(),
231
+ event_path: z.array(z.string()).optional(),
232
+ event_result_type: z.unknown().optional(),
233
+ event_emitted_by_handler_id: z.string().uuid().nullable().optional(),
234
+ event_pending_bus_count: z.number().nonnegative().optional(),
235
+ event_status: z.enum(['pending', 'started', 'completed']).optional(),
236
+ event_started_at: z.string().datetime().nullable().optional(),
237
+ event_completed_at: z.string().datetime().nullable().optional(),
238
+ event_results: z.record(z.string(), z.unknown()).optional(),
239
+ event_concurrency: z.enum(EVENT_CONCURRENCY_MODES).nullable().optional(),
240
+ event_handler_concurrency: z.enum(EVENT_HANDLER_CONCURRENCY_MODES).nullable().optional(),
241
+ event_handler_completion: z.enum(EVENT_HANDLER_COMPLETION_MODES).nullable().optional(),
242
+ }
243
+ }
244
+
245
+ function missingBaseFields(event_type: string, user_shape: z.ZodRawShape): z.ZodRawShape {
246
+ return Object.fromEntries(Object.entries(baseEventDefaultShape(event_type)).filter(([key]) => !(key in user_shape))) as z.ZodRawShape
247
+ }
248
+
249
+ type ZodSchemaWithPrefault = z.ZodTypeAny & {
250
+ prefault: (value: unknown) => z.ZodTypeAny
251
+ }
252
+
253
+ function shortcutDefaultSchema(base_field_schema: z.ZodTypeAny | undefined, value: unknown): z.ZodTypeAny {
254
+ if (!base_field_schema) {
255
+ return z.unknown().optional().default(value)
256
+ }
257
+ return (base_field_schema as ZodSchemaWithPrefault).prefault(base_field_schema.parse(value))
258
+ }
259
+
260
+ function schemaDefaultsForShortcut(event_type: string, raw_shape: Record<string, unknown>): z.ZodRawShape {
261
+ const defaults: Record<string, z.ZodTypeAny> = {}
262
+ const base_shape = baseEventDefaultShape(event_type)
263
+ for (const [key, value] of Object.entries(raw_shape)) {
264
+ if (key === 'event_result_type') continue
265
+ if (!isZodSchema(value)) {
266
+ defaults[key] = shortcutDefaultSchema(base_shape[key] as z.ZodTypeAny | undefined, value)
267
+ }
268
+ }
269
+ return defaults
270
+ }
271
+
272
+ function zodFieldsForShortcut(raw_shape: Record<string, unknown>): z.ZodRawShape {
273
+ const fields: Record<string, z.ZodTypeAny> = {}
274
+ for (const [key, value] of Object.entries(raw_shape)) {
275
+ if (key === 'event_result_type') continue
276
+ if (isZodSchema(value)) {
277
+ fields[key] = value
278
+ }
279
+ }
280
+ return fields
281
+ }
282
+
283
+ function eventResultTypeFromObjectSchema(schema: z.ZodObject<z.ZodRawShape>): z.ZodTypeAny | undefined {
284
+ const raw_event_result_type = schema.shape.event_result_type
285
+ return raw_event_result_type === undefined ? undefined : normalizeEventResultType(raw_event_result_type)
286
+ }
287
+
288
+ function buildFullEventSchema(
289
+ event_type: string,
290
+ spec: unknown
291
+ ): {
292
+ event_schema: AnyEventSchema
293
+ event_result_type?: z.ZodTypeAny
294
+ event_version?: string
295
+ } {
296
+ if (isZodObjectSchema(spec)) {
297
+ const user_shape = spec.shape
298
+ assertNoReservedUserEventFields(user_shape, `BaseEvent.extend(${event_type})`)
299
+ assertNoUnknownEventPrefixedFields(user_shape, `BaseEvent.extend(${event_type})`)
300
+ assertNoModelPrefixedFields(user_shape, `BaseEvent.extend(${event_type})`)
301
+ const full_schema = spec.safeExtend({
302
+ event_result_type: z.unknown().optional(),
303
+ ...missingBaseFields(event_type, user_shape),
304
+ })
305
+ return {
306
+ event_schema: full_schema,
307
+ event_result_type: eventResultTypeFromObjectSchema(spec),
308
+ }
309
+ }
310
+
311
+ const raw_shape = (isRecord(spec) ? spec : {}) as Record<string, unknown>
312
+ assertNoReservedUserEventFields(raw_shape, `BaseEvent.extend(${event_type})`)
313
+ assertNoUnknownEventPrefixedFields(raw_shape, `BaseEvent.extend(${event_type})`)
314
+ assertNoModelPrefixedFields(raw_shape, `BaseEvent.extend(${event_type})`)
315
+ const shortcut_shape = {
316
+ ...schemaDefaultsForShortcut(event_type, raw_shape),
317
+ ...zodFieldsForShortcut(raw_shape),
318
+ }
319
+ const full_schema = z.object(shortcut_shape).safeExtend(missingBaseFields(event_type, shortcut_shape)).loose()
320
+ return {
321
+ event_schema: full_schema,
322
+ event_result_type: normalizeEventResultType(raw_shape.event_result_type),
323
+ event_version: typeof raw_shape.event_version === 'string' ? raw_shape.event_version : undefined,
324
+ }
325
+ }
326
+
327
+ function decodeEventSchema(schema: AnyEventSchema, input: unknown): Record<string, unknown> {
328
+ const decoded = (z as unknown as { decode: (schema: AnyEventSchema, input: unknown) => unknown }).decode(schema, input)
329
+ if (!isRecord(decoded)) {
330
+ throw new Error('BaseEvent schema must decode to an object')
331
+ }
332
+ return decoded
333
+ }
334
+
335
+ function encodeEventSchema(schema: AnyEventSchema, input: Record<string, unknown>): Record<string, unknown> {
336
+ const encoded = (z as unknown as { encode: (schema: AnyEventSchema, input: unknown) => unknown }).encode(schema, input)
337
+ if (!isRecord(encoded)) {
338
+ throw new Error('BaseEvent schema must encode to an object')
339
+ }
340
+ return encoded
341
+ }
342
+
177
343
  export class BaseEvent {
178
344
  // event metadata fields
179
345
  event_id!: string // unique uuidv7 identifier for the event
@@ -184,7 +350,7 @@ export class BaseEvent {
184
350
  event_slow_timeout?: number | null // optional per-event slow warning threshold in seconds
185
351
  event_handler_timeout?: number | null // optional per-event handler timeout override in seconds
186
352
  event_handler_slow_timeout?: number | null // optional per-event slow handler warning threshold in seconds
187
- event_blocks_parent_completion!: boolean // true only for children explicitly awaited via done()/eventCompleted()
353
+ event_blocks_parent_completion!: boolean // true only for children explicitly awaited via now()
188
354
  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
189
355
  event_path!: string[] // list of bus labels (name#id) that the event has been dispatched to, including the current bus
190
356
  event_result_type?: z.ZodTypeAny // optional zod schema to enforce the shape of return values from handlers
@@ -197,15 +363,18 @@ export class BaseEvent {
197
363
  event_concurrency?: EventConcurrencyMode | null // concurrency mode for the event as a whole in relation to other events
198
364
  event_handler_concurrency?: EventHandlerConcurrencyMode | null // concurrency mode for the handlers within the event
199
365
  event_handler_completion?: EventHandlerCompletionMode | null // completion strategy: 'all' (default) waits for every handler, 'first' returns earliest non-undefined result and cancels the rest
366
+ event_schema?: z.ZodTypeAny
200
367
 
201
368
  static event_type?: string // class name of the event, e.g. BaseEvent.extend("MyEvent").event_type === "MyEvent"
202
369
  static event_version = '0.0.1'
203
- static schema = BaseEventSchema // zod schema for the event data fields, used to parse and validate event data when creating a new event
370
+ static event_result_type?: z.ZodTypeAny
371
+ static event_schema: AnyEventSchema = BaseEventSchema // generated Zod schema for local TS event data validation; never sent over the wire
204
372
 
205
373
  // internal runtime state
206
374
  event_bus?: EventBus // bus that dispatched this event, also used by event.emit(child)
207
375
  _event_original?: BaseEvent // underlying event object that was dispatched, if this is a bus-scoped proxy wrapping it
208
376
  _event_dispatch_context?: unknown | null // captured AsyncLocalStorage context at dispatch site, used to restore that context when running handlers
377
+ _event_fields_set?: Set<string>
209
378
 
210
379
  _event_completed_signal: Deferred<this> | null
211
380
  _lock_for_event_handler: AsyncLock | null
@@ -216,39 +385,48 @@ export class BaseEvent {
216
385
  const ctor = this.constructor as typeof BaseEvent & {
217
386
  event_version?: string
218
387
  event_result_type?: z.ZodTypeAny
388
+ event_schema?: AnyEventSchema
219
389
  }
220
- const ctor_defaults = EVENT_CLASS_DEFAULTS.get(ctor) ?? {}
221
- const merged_data = {
222
- ...ctor_defaults,
223
- ...data,
224
- } as BaseEventInit<Record<string, unknown>>
390
+ const explicit_event_fields = new Set(Object.keys(data ?? {}))
391
+ const merged_data = { ...data } as BaseEventInit<Record<string, unknown>>
225
392
  const event_type = merged_data.event_type ?? ctor.event_type ?? ctor.name
226
393
  const event_version = merged_data.event_version ?? ctor.event_version ?? '0.0.1'
227
394
  const raw_event_result_type = merged_data.event_result_type ?? ctor.event_result_type
228
395
  const event_result_type = normalizeEventResultType(raw_event_result_type)
229
- const event_id = merged_data.event_id ?? uuidv7()
230
- const event_created_at = monotonicDatetime(merged_data.event_created_at)
231
- const event_timeout = merged_data.event_timeout ?? null
232
- const event_blocks_parent_completion = merged_data.event_blocks_parent_completion ?? false
233
396
 
234
- const base_data = {
397
+ const event_schema = ctor.event_schema ?? BaseEventSchema
398
+ const base_data: Record<string, unknown> = {
235
399
  ...merged_data,
236
- event_id,
237
- event_created_at,
400
+ event_id: merged_data.event_id ?? uuidv7(),
401
+ event_created_at: merged_data.event_created_at ?? monotonicDatetime(),
238
402
  event_type,
239
403
  event_version,
240
- event_timeout,
241
- event_blocks_parent_completion,
242
404
  event_result_type,
243
405
  }
406
+ if (event_schema === BaseEventSchema) {
407
+ base_data.event_timeout ??= null
408
+ base_data.event_blocks_parent_completion ??= false
409
+ }
244
410
 
245
- const schema = ctor.schema ?? BaseEventSchema
246
- const parsed = schema.parse(base_data) as BaseEventData & Record<string, unknown>
411
+ const parsed = decodeEventSchema(event_schema, base_data) as BaseEventData & Record<string, unknown>
247
412
 
248
413
  Object.assign(this, parsed)
414
+ Object.defineProperty(this, 'event_schema', {
415
+ value: event_schema,
416
+ writable: true,
417
+ enumerable: false,
418
+ configurable: true,
419
+ })
420
+ Object.defineProperty(this, '_event_fields_set', {
421
+ value: explicit_event_fields,
422
+ writable: true,
423
+ enumerable: false,
424
+ configurable: true,
425
+ })
249
426
 
250
427
  const parsed_path = (parsed as { event_path?: string[] }).event_path
251
428
  this.event_path = Array.isArray(parsed_path) ? [...parsed_path] : []
429
+ this.event_created_at = monotonicDatetime(parsed.event_created_at)
252
430
 
253
431
  // load event results from potentially raw objects from JSON to proper EventResult objects
254
432
  this.event_results = hydrateEventResults(this, (parsed as { event_results?: unknown }).event_results)
@@ -273,7 +451,7 @@ export class BaseEvent {
273
451
  ? (parsed as { event_emitted_by_handler_id: string }).event_emitted_by_handler_id
274
452
  : null
275
453
 
276
- this.event_result_type = event_result_type
454
+ this.event_result_type = normalizeEventResultType(parsed.event_result_type ?? event_result_type)
277
455
 
278
456
  this._event_completed_signal = null
279
457
  this._lock_for_event_handler = null
@@ -287,6 +465,7 @@ export class BaseEvent {
287
465
 
288
466
  // main entry point for users to define their own event types
289
467
  // BaseEvent.extend("MyEvent", { some_custom_field: z.string(), event_result_type: z.string(), event_timeout: 25, ... }) -> MyEvent
468
+ static extend<TSchema extends z.ZodObject<z.ZodRawShape>>(event_type: string, event_schema: TSchema): SchemaEventFactory<TSchema, unknown>
290
469
  static extend<TShape extends z.ZodRawShape>(event_type: string, shape?: TShape): EventFactory<TShape, ResultSchemaFromShape<TShape>>
291
470
  static extend<TShape extends Record<string, unknown>>(
292
471
  event_type: string,
@@ -294,32 +473,21 @@ export class BaseEvent {
294
473
  ): EventFactory<ZodShapeFrom<TShape>, ResultSchemaFromShape<TShape>>
295
474
  static extend<TShape extends Record<string, unknown>>(
296
475
  event_type: string,
297
- shape: TShape = {} as TShape
298
- ): EventFactory<ZodShapeFrom<TShape>, ResultSchemaFromShape<TShape>> {
299
- const raw_shape = shape as Record<string, unknown>
300
- assertNoReservedUserEventFields(raw_shape, `BaseEvent.extend(${event_type})`)
301
- assertNoUnknownEventPrefixedFields(raw_shape, `BaseEvent.extend(${event_type})`)
302
- assertNoModelPrefixedFields(raw_shape, `BaseEvent.extend(${event_type})`)
303
- const raw_event_result_type = raw_shape.event_result_type
304
- const event_result_type = normalizeEventResultType(raw_event_result_type)
305
- const event_version = typeof raw_shape.event_version === 'string' ? raw_shape.event_version : undefined
306
- const event_defaults = Object.fromEntries(
307
- Object.entries(raw_shape).filter(
308
- ([key, value]) => key !== 'event_result_type' && key !== 'event_version' && !(value instanceof z.ZodType)
309
- )
310
- )
311
-
312
- const zod_shape = extractZodShape(raw_shape)
313
- const full_schema = BaseEventSchema.extend(zod_shape)
476
+ shape?: TShape
477
+ ): EventFactory<ZodShapeFrom<TShape>, ResultSchemaFromShape<TShape>> | SchemaEventFactory<AnyEventSchema, unknown> {
478
+ const built = buildFullEventSchema(event_type, shape ?? {})
479
+ const full_schema = built.event_schema
480
+ const event_result_type = built.event_result_type
481
+ const event_version = built.event_version
314
482
 
315
483
  // create a new event class that extends BaseEvent and adds the custom fields
316
484
  class ExtendedEvent extends BaseEvent {
317
- static schema = full_schema as unknown as typeof BaseEvent.schema
485
+ static event_schema = full_schema
318
486
  static event_type = event_type
319
487
  static event_version = event_version ?? BaseEvent.event_version
320
488
  static event_result_type = event_result_type
321
489
 
322
- constructor(data: EventInit<ZodShapeFrom<TShape>>) {
490
+ constructor(data: EventInit<ZodShapeFrom<TShape>> | EventInitFromSchema<AnyEventSchema>) {
323
491
  super(data as BaseEventInit<Record<string, unknown>>)
324
492
  }
325
493
  }
@@ -330,27 +498,40 @@ export class BaseEvent {
330
498
  return new ExtendedEvent(data) as FactoryResult
331
499
  }
332
500
 
333
- EventFactory.schema = full_schema as EventSchema<ZodShapeFrom<TShape>>
501
+ EventFactory.event_schema = full_schema as EventSchema<ZodShapeFrom<TShape>>
334
502
  EventFactory.event_type = event_type
335
503
  EventFactory.event_version = event_version ?? BaseEvent.event_version
336
504
  EventFactory.event_result_type = event_result_type
337
505
  EventFactory.class = ExtendedEvent as unknown as new (
338
506
  data: EventInit<ZodShapeFrom<TShape>>
339
507
  ) => EventWithResultSchema<ResultSchemaFromShape<TShape>> & EventPayload<ZodShapeFrom<TShape>>
340
- EventFactory.fromJSON = (data: unknown) => (ExtendedEvent.fromJSON as (data: unknown) => FactoryResult)(data)
508
+ EventFactory.fromJSON = (data: unknown) => ExtendedEvent.fromJSON(data) as FactoryResult
341
509
  EventFactory.prototype = ExtendedEvent.prototype
342
- EVENT_CLASS_DEFAULTS.set(ExtendedEvent, event_defaults)
510
+ EVENT_TYPE_REGISTRY.set(event_type, ExtendedEvent)
343
511
 
344
512
  return EventFactory as unknown as EventFactory<ZodShapeFrom<TShape>, ResultSchemaFromShape<TShape>>
345
513
  }
346
514
 
347
515
  static fromJSON<T extends typeof BaseEvent>(this: T, data: unknown): InstanceType<T> {
348
516
  if (!data || typeof data !== 'object') {
349
- const schema = this.schema ?? BaseEventSchema
350
- const parsed = schema.parse(data)
517
+ const event_schema = this.event_schema ?? BaseEventSchema
518
+ const parsed = decodeEventSchema(event_schema, data)
351
519
  return new this(parsed) as InstanceType<T>
352
520
  }
353
521
  const record = { ...(data as Record<string, unknown>) }
522
+ if (this === BaseEvent) {
523
+ const event_type = record.event_type
524
+ if (typeof event_type === 'string') {
525
+ const KnownEvent = EVENT_TYPE_REGISTRY.get(event_type)
526
+ if (KnownEvent) {
527
+ return KnownEvent.fromJSON(record) as InstanceType<T>
528
+ }
529
+ }
530
+ }
531
+ const ctor = this as typeof BaseEvent
532
+ if (this !== BaseEvent && ctor.event_result_type && record.event_result_type !== undefined) {
533
+ delete record.event_result_type
534
+ }
354
535
  if (record.event_result_type !== undefined && record.event_result_type !== null) {
355
536
  record.event_result_type = normalizeEventResultType(record.event_result_type)
356
537
  }
@@ -374,7 +555,7 @@ export class BaseEvent {
374
555
  toJSON(): BaseEventJSON {
375
556
  const record: Record<string, unknown> = {}
376
557
  for (const [key, value] of Object.entries(this as unknown as Record<string, unknown>)) {
377
- if (key.startsWith('_') || key === 'bus' || key === 'event_bus' || key === 'event_results') continue
558
+ if (key.startsWith('_') || key === 'bus' || key === 'event_bus' || key === 'event_schema' || key === 'event_results') continue
378
559
  if (value === undefined || typeof value === 'function') continue
379
560
  record[key] = value
380
561
  }
@@ -382,11 +563,45 @@ export class BaseEvent {
382
563
  Array.from(this.event_results.entries()).map(([handler_id, result]) => [handler_id, result.toJSON()])
383
564
  )
384
565
 
385
- return {
566
+ const event_schema = ((this.constructor as typeof BaseEvent).event_schema ?? this.event_schema ?? BaseEventSchema) as AnyEventSchema
567
+ const encoded = encodeEventSchema(event_schema, {
386
568
  ...record,
387
569
  event_id: this.event_id,
388
570
  event_type: this.event_type,
389
571
  event_version: this.event_version,
572
+ event_result_type: this.event_result_type,
573
+
574
+ // static configuration options
575
+ event_timeout: this.event_timeout,
576
+ event_slow_timeout: this.event_slow_timeout,
577
+ event_concurrency: this.event_concurrency,
578
+ event_handler_concurrency: this.event_handler_concurrency,
579
+ event_handler_completion: this.event_handler_completion,
580
+ event_handler_slow_timeout: this.event_handler_slow_timeout,
581
+ event_handler_timeout: this.event_handler_timeout,
582
+ event_blocks_parent_completion: this.event_blocks_parent_completion,
583
+
584
+ // mutable parent/child/bus tracking runtime state
585
+ event_parent_id: this.event_parent_id,
586
+ event_path: this.event_path,
587
+ event_emitted_by_handler_id: this.event_emitted_by_handler_id,
588
+ event_pending_bus_count: this.event_pending_bus_count,
589
+
590
+ // mutable runtime status and timestamps
591
+ event_status: this.event_status,
592
+ event_created_at: this.event_created_at,
593
+ event_started_at: this.event_started_at ?? null,
594
+ event_completed_at: this.event_completed_at ?? null,
595
+
596
+ ...(Object.keys(event_results).length > 0 ? { event_results } : {}),
597
+ })
598
+ delete encoded.event_schema
599
+
600
+ return {
601
+ ...encoded,
602
+ event_id: this.event_id,
603
+ event_type: this.event_type,
604
+ event_version: this.event_version,
390
605
  event_result_type: this.event_result_type ? toJsonSchema(this.event_result_type) : this.event_result_type,
391
606
 
392
607
  // static configuration options
@@ -416,13 +631,15 @@ export class BaseEvent {
416
631
  }
417
632
  }
418
633
 
419
- _createSlowEventWarningTimer(): ReturnType<typeof setTimeout> | null {
420
- const event_slow_timeout = this.event_slow_timeout ?? this.event_bus?.event_slow_timeout ?? null
421
- const event_warn_ms = event_slow_timeout === null ? null : event_slow_timeout * 1000
634
+ _createSlowEventWarningTimer(
635
+ event_slow_timeout: number | null = this.event_slow_timeout ?? null,
636
+ bus_name?: string
637
+ ): ReturnType<typeof setTimeout> | null {
638
+ const event_warn_ms = event_slow_timeout === null || event_slow_timeout <= 0 ? null : event_slow_timeout * 1000
422
639
  if (event_warn_ms === null) {
423
640
  return null
424
641
  }
425
- const name = this.event_bus?.name ?? 'EventBus'
642
+ const name = bus_name ?? this.event_bus?.name ?? 'EventBus'
426
643
  return setTimeout(() => {
427
644
  if (this.event_status === 'completed') {
428
645
  return
@@ -528,7 +745,28 @@ export class BaseEvent {
528
745
  }
529
746
 
530
747
  private _isFirstModeWinningResult(entry: EventResult): boolean {
531
- return entry.status === 'completed' && entry.result !== undefined && entry.result !== null && !(entry.result instanceof BaseEvent)
748
+ return BaseEvent._defaultResultInclude(entry.result, entry)
749
+ }
750
+
751
+ private static _defaultResultInclude<TEvent extends BaseEvent>(
752
+ result: EventResult<TEvent>['result'],
753
+ event_result: EventResult<TEvent>
754
+ ): boolean {
755
+ return (
756
+ event_result.status === 'completed' &&
757
+ result !== undefined &&
758
+ result !== null &&
759
+ !(result instanceof Error) &&
760
+ !(result instanceof BaseEvent) &&
761
+ event_result.error === undefined
762
+ )
763
+ }
764
+
765
+ private static _includeEventResult<TEvent extends BaseEvent>(
766
+ include: EventResultInclude<TEvent>,
767
+ event_result: EventResult<TEvent>
768
+ ): boolean {
769
+ return include(event_result.result, event_result)
532
770
  }
533
771
 
534
772
  private _markFirstModeWinnerIfNeeded(original: BaseEvent, entry: EventResult, first_state: { found: boolean }): void {
@@ -543,9 +781,13 @@ export class BaseEvent {
543
781
  if (!this.event_bus) {
544
782
  throw new Error('event has no bus attached')
545
783
  }
546
- await this.event_bus.locks._runWithHandlerLock(original, this.event_bus.event_handler_concurrency, async (handler_lock) => {
547
- await entry.runHandler(handler_lock)
548
- })
784
+ await this.event_bus.locks._runWithHandlerLock(
785
+ original,
786
+ original.event_handler_concurrency ?? this.event_bus.event_handler_concurrency,
787
+ async (handler_lock) => {
788
+ await entry.runHandler(handler_lock)
789
+ }
790
+ )
549
791
  }
550
792
 
551
793
  // Run all pending handler results for the current bus context.
@@ -562,7 +804,7 @@ export class BaseEvent {
562
804
  }
563
805
  const resolved_completion = original.event_handler_completion ?? this.event_bus?.event_handler_completion ?? 'all'
564
806
  if (resolved_completion === 'first') {
565
- if (original._getHandlerLock(this.event_bus?.event_handler_concurrency) !== null) {
807
+ if (original._getHandlerLock(original.event_handler_concurrency ?? this.event_bus?.event_handler_concurrency ?? 'serial') !== null) {
566
808
  for (const entry of pending_results) {
567
809
  await this._runHandlerWithLock(original, entry)
568
810
  if (!this._isFirstModeWinningResult(entry)) {
@@ -590,7 +832,7 @@ export class BaseEvent {
590
832
 
591
833
  _getHandlerLock(default_concurrency?: EventHandlerConcurrencyMode): AsyncLock | null {
592
834
  const original = this._event_original ?? this
593
- const resolved = original.event_handler_concurrency ?? default_concurrency ?? original.event_bus?.event_handler_concurrency ?? 'serial'
835
+ const resolved = original.event_handler_concurrency ?? default_concurrency ?? 'serial'
594
836
  if (resolved === 'parallel') {
595
837
  return null
596
838
  }
@@ -715,7 +957,7 @@ export class BaseEvent {
715
957
  original_child._markCancelled(cancellation_cause)
716
958
 
717
959
  // Force-complete the child event. In JS we can't stop running async
718
- // handlers, but _markCompleted() resolves the done() promise so callers
960
+ // handlers, but _markCompleted() resolves active waiters so callers
719
961
  // aren't blocked waiting for background work to finish. The background
720
962
  // handler's eventual _markCompleted/_markError is a no-op (terminal guard).
721
963
  if (original_child.event_status !== 'completed') {
@@ -732,11 +974,11 @@ export class BaseEvent {
732
974
  }
733
975
  }
734
976
 
735
- // Cancel all handler results for an event except the winner, used by first() mode.
977
+ // Cancel all handler results for an event except the winner, used by event_handler_completion='first'.
736
978
  // Cancels pending handlers immediately, aborts started handlers via _signalAbort(),
737
979
  // and cancels any child events emitted by the losing handlers.
738
980
  _markRemainingFirstModeResultCancelled(winner: EventResult): void {
739
- const cause = new Error('first() resolved: another handler returned a result first')
981
+ const cause = new Error("event_handler_completion='first' resolved: another handler returned a result first")
740
982
  const bus_id = winner.eventbus_id
741
983
 
742
984
  for (const result of this.event_results.values()) {
@@ -745,7 +987,7 @@ export class BaseEvent {
745
987
 
746
988
  if (result.status === 'pending') {
747
989
  result._markError(
748
- new EventHandlerCancelledError(`Cancelled: first() resolved`, {
990
+ new EventHandlerCancelledError(`Cancelled: event_handler_completion='first' resolved`, {
749
991
  event_result: result,
750
992
  cause,
751
993
  })
@@ -763,7 +1005,7 @@ export class BaseEvent {
763
1005
 
764
1006
  // Abort the handler itself
765
1007
  result._lock?.exitHandlerRun()
766
- const aborted_error = new EventHandlerAbortedError(`Aborted: first() resolved`, {
1008
+ const aborted_error = new EventHandlerAbortedError(`Aborted: event_handler_completion='first' resolved`, {
767
1009
  event_result: result,
768
1010
  cause,
769
1011
  })
@@ -848,127 +1090,52 @@ export class BaseEvent {
848
1090
  }
849
1091
  }
850
1092
 
851
- // awaitable that triggers immediate (queue-jump) processing of the event on all buses where it is queued
852
- // use eventCompleted() to wait for normal queue-order completion without queue-jumping.
853
- done(options: EventDoneOptions = {}): Promise<this> {
854
- if (!this.event_bus) {
855
- return Promise.reject(new Error('event has no bus attached'))
1093
+ private _withEventResultMethods(promise: Promise<this>): EventWaitPromise<this> {
1094
+ const chainable = promise as EventWaitPromise<this>
1095
+ chainable.eventResult = async (options?: EventResultOptions<this>) => {
1096
+ const event = await promise
1097
+ return event.eventResult(options)
856
1098
  }
857
- const original = this._event_original ?? this
858
- original._markBlocksParentCompletionIfAwaitedFromEmittingHandler()
859
- const raise_if_any = options.raise_if_any ?? true
860
- const completion_promise =
861
- this.event_status === 'completed' ? Promise.resolve(original as this) : this.event_bus._processEventImmediately(this)
862
-
863
- if (!raise_if_any) {
864
- return completion_promise
1099
+ chainable.eventResultsList = async (options?: EventResultOptions<this>) => {
1100
+ const event = await promise
1101
+ return event.eventResultsList(options)
865
1102
  }
1103
+ return chainable
1104
+ }
866
1105
 
867
- // Always delegate to _processEventImmediately it walks up the parent event tree
868
- // to determine whether we're inside a handler (works cross-bus). If no
869
- // ancestor handler is in-flight, it falls back to eventCompleted().
870
- return completion_promise.then((completed_event) => {
871
- const first_error = completed_event._firstProcessingError()
872
- if (first_error !== undefined) {
873
- if (first_error instanceof Error) {
874
- throw first_error
875
- }
876
- throw new Error(String(first_error))
877
- }
878
- return completed_event
879
- })
1106
+ private _timeoutPromise<T>(timeout: number | null, message: () => string, fn: () => Promise<T>): Promise<T> {
1107
+ return timeout === null || timeout <= 0 ? fn() : _runWithTimeout(timeout, () => new Error(message()), fn)
880
1108
  }
881
1109
 
882
- // returns the first non-undefined handler result value, cancelling remaining handlers
883
- // when any handler completes. Works with all event_handler_concurrency modes:
884
- // parallel: races all handlers, returns first non-undefined, aborts the rest
885
- // serial: runs handlers sequentially, returns first non-undefined, skips remaining
886
- first(): Promise<EventResultType<this> | undefined> {
887
- if (!this.event_bus) {
888
- return Promise.reject(new Error('event has no bus attached'))
889
- }
1110
+ private _orderedEventResults(): EventResult<this>[] {
890
1111
  const original = this._event_original ?? this
891
- original.event_handler_completion = 'first'
892
- return this.done({ raise_if_any: false }).then((completed_event) => {
893
- const first_error = completed_event._firstProcessingError({ ignore_first_mode_control_errors: true })
894
- if (first_error !== undefined) {
895
- if (first_error instanceof Error) {
896
- throw first_error
897
- }
898
- throw new Error(String(first_error))
899
- }
900
- const orig = completed_event._event_original ?? completed_event
901
- return Array.from(orig.event_results.values())
902
- .filter(
903
- (result) =>
904
- result.status === 'completed' && result.result !== undefined && result.result !== null && !(result.result instanceof BaseEvent)
905
- )
906
- .sort((a, b) => compareIsoDatetime(a.completed_at, b.completed_at))
907
- .map((result) => result.result as EventResultType<this>)
908
- .at(0)
909
- })
1112
+ return (Array.from(original.event_results.values()) as EventResult<this>[]).sort((a, b) =>
1113
+ compareIsoDatetime(a.completed_at, b.completed_at)
1114
+ )
910
1115
  }
911
1116
 
912
- // returns handler result values in event_results insertion order.
913
- // equivalent to await event.done(); Array.from(event.event_results.values()).map((entry) => entry.result)
914
- eventResultsList(
915
- include: EventResultsListInclude<this>,
916
- options?: EventResultsListOptions<this>
917
- ): Promise<Array<EventResultType<this> | undefined>>
918
- eventResultsList(options?: EventResultsListOptions<this>): Promise<Array<EventResultType<this> | undefined>>
919
- async eventResultsList(
920
- include_or_options?: EventResultsListInclude<this> | EventResultsListOptions<this>,
921
- maybe_options?: EventResultsListOptions<this>
922
- ): Promise<Array<EventResultType<this> | undefined>> {
923
- const default_include: EventResultsListInclude<this> = (_result, event_result) =>
924
- event_result.status === 'completed' &&
925
- event_result.result !== undefined &&
926
- event_result.result !== null &&
927
- !(event_result.result instanceof Error) &&
928
- !(event_result.result instanceof BaseEvent) &&
929
- event_result.error === undefined
930
-
931
- let options: EventResultsListOptions<this>
932
- let include: EventResultsListInclude<this>
933
- if (typeof include_or_options === 'function') {
934
- options = maybe_options ?? {}
935
- include = include_or_options
936
- } else {
937
- options = include_or_options ?? {}
938
- include = options.include ?? default_include
939
- }
940
- const raise_if_any = options.raise_if_any ?? true
941
- const raise_if_none = options.raise_if_none ?? true
942
-
1117
+ private _orderedEventResultsByRegistration(): EventResult<this>[] {
943
1118
  const original = this._event_original ?? this
944
- const resolved_timeout_seconds = options.timeout ?? original.event_timeout ?? this.event_bus?.event_timeout ?? null
945
- let completed_event: this
946
-
947
- if (resolved_timeout_seconds === null) {
948
- completed_event = await this.done({ raise_if_any: false })
949
- } else {
950
- completed_event = await _runWithTimeout(
951
- resolved_timeout_seconds,
952
- () => new Error(`Timed out waiting for ${original.event_type} results after ${resolved_timeout_seconds}s`),
953
- () => this.done({ raise_if_any: false })
954
- )
955
- }
1119
+ return (Array.from(original.event_results.values()) as EventResult<this>[]).sort(
1120
+ (a, b) =>
1121
+ compareIsoDatetime(a.handler.handler_registered_at, b.handler.handler_registered_at) ||
1122
+ compareIsoDatetime(a.started_at, b.started_at) ||
1123
+ a.handler_id.localeCompare(b.handler_id)
1124
+ )
1125
+ }
956
1126
 
957
- const all_results: EventResult<this>[] = Array.from(completed_event.event_results.values())
1127
+ private _collectResultValues(
1128
+ options: EventResultOptions<this> = {},
1129
+ order: 'completion' | 'registration' = 'completion'
1130
+ ): Array<EventResultType<this> | undefined> {
1131
+ const include: EventResultInclude<this> = options.include ?? BaseEvent._defaultResultInclude
1132
+ const raise_if_any = options.raise_if_any ?? true
1133
+ const raise_if_none = options.raise_if_none ?? false
1134
+ const all_results = order === 'registration' ? this._orderedEventResultsByRegistration() : this._orderedEventResults()
958
1135
  const error_results = all_results.filter((event_result) => event_result.error !== undefined || event_result.result instanceof Error)
1136
+ const included_results = all_results.filter((event_result) => BaseEvent._includeEventResult(include, event_result))
959
1137
 
960
- if (raise_if_any && error_results.length > 0) {
961
- if (error_results.length === 1) {
962
- const first_error = error_results[0]
963
- if (first_error.error instanceof Error) {
964
- throw first_error.error
965
- }
966
- if (first_error.result instanceof Error) {
967
- throw first_error.result
968
- }
969
- throw new Error(String(first_error.error ?? first_error.result))
970
- }
971
-
1138
+ if (error_results.length > 0 && raise_if_any) {
972
1139
  const errors = error_results.map((event_result) => {
973
1140
  if (event_result.error instanceof Error) {
974
1141
  return event_result.error
@@ -978,31 +1145,119 @@ export class BaseEvent {
978
1145
  }
979
1146
  return new Error(String(event_result.error ?? event_result.result))
980
1147
  })
981
- throw new AggregateError(
982
- errors,
983
- `Event ${completed_event.event_type}#${completed_event.event_id.slice(-4)} had ${errors.length} handler error(s)`
984
- )
1148
+ if (errors.length === 1) {
1149
+ throw errors[0]
1150
+ }
1151
+ throw new AggregateError(errors, `Event ${this.event_type}#${this.event_id.slice(-4)} had ${errors.length} handler error(s)`)
985
1152
  }
986
1153
 
987
- const included_results = all_results.filter((event_result) => include(event_result.result, event_result))
988
1154
  if (raise_if_none && included_results.length === 0) {
989
1155
  throw new Error(
990
- `Expected at least one handler to return a non-null result, but none did: ${completed_event.event_type}#${completed_event.event_id.slice(-4)}`
1156
+ `Expected at least one handler to return a non-null result, but none did: ${this.event_type}#${this.event_id.slice(-4)}`
991
1157
  )
992
1158
  }
993
1159
 
994
1160
  return included_results.map((event_result) => event_result.result)
995
1161
  }
996
1162
 
997
- // awaitable that waits for the event to be processed in normal queue order by the _runloop
998
- eventCompleted(): Promise<this> {
1163
+ private _hasIncludedResult(options: EventResultOptions<this> = {}): boolean {
1164
+ const include: EventResultInclude<this> = options.include ?? BaseEvent._defaultResultInclude
1165
+ return this._orderedEventResults().some((event_result) => BaseEvent._includeEventResult(include, event_result))
1166
+ }
1167
+
1168
+ private async _waitForFirstResultOrCompletion(options: EventWaitOptions & EventResultOptions<this> = {}): Promise<this> {
999
1169
  const original = this._event_original ?? this
1170
+ if (options.timeout !== undefined && options.timeout !== null && options.timeout < 0) {
1171
+ throw new Error('timeout must be >= 0 or null')
1172
+ }
1173
+ if (!this.event_bus && original.event_status !== 'completed') {
1174
+ throw new Error('event has no bus attached')
1175
+ }
1176
+ if (original.event_status === 'completed' || this._hasIncludedResult(options)) {
1177
+ return this
1178
+ }
1179
+
1180
+ const waitForResult = async (): Promise<this> => {
1181
+ for (;;) {
1182
+ if (original.event_status === 'completed' || this._hasIncludedResult(options)) {
1183
+ return this
1184
+ }
1185
+ await new Promise((resolve) => setTimeout(resolve, 1))
1186
+ }
1187
+ }
1188
+
1189
+ const timeout = options.timeout ?? null
1190
+ return this._timeoutPromise(timeout, () => `Timed out waiting for ${original.event_type} result after ${timeout}s`, waitForResult)
1191
+ }
1192
+
1193
+ // Active awaitable that triggers immediate (queue-jump) processing of the event on all buses where it is queued.
1194
+ now(options: EventWaitOptions = {}): EventWaitPromise<this> {
1195
+ const original = this._event_original ?? this
1196
+ if (options.timeout !== undefined && options.timeout !== null && options.timeout < 0) {
1197
+ return this._withEventResultMethods(Promise.reject(new Error('timeout must be >= 0 or null')))
1198
+ }
1199
+ if (!this.event_bus && original.event_status !== 'completed') {
1200
+ return this._withEventResultMethods(Promise.reject(new Error('event has no bus attached')))
1201
+ }
1000
1202
  original._markBlocksParentCompletionIfAwaitedFromEmittingHandler()
1001
- if (this.event_status === 'completed') {
1002
- return Promise.resolve(this)
1203
+ const resolved_timeout_seconds = options.timeout ?? null
1204
+ const processing =
1205
+ original.event_status === 'completed'
1206
+ ? Promise.resolve(this)
1207
+ : this._timeoutPromise(
1208
+ resolved_timeout_seconds,
1209
+ () => `Timed out waiting for ${original.event_type} completion after ${resolved_timeout_seconds}s`,
1210
+ () => this.event_bus!._processEventImmediately(this)
1211
+ )
1212
+
1213
+ if (options.first_result) {
1214
+ void processing.catch(() => undefined)
1215
+ return this._withEventResultMethods(this._waitForFirstResultOrCompletion(options))
1216
+ }
1217
+
1218
+ return this._withEventResultMethods(processing)
1219
+ }
1220
+
1221
+ // Passive awaitable that waits for normal queue-order processing without forcing execution.
1222
+ wait(options: EventWaitOptions = {}): EventWaitPromise<this> {
1223
+ const original = this._event_original ?? this
1224
+ if (options.timeout !== undefined && options.timeout !== null && options.timeout < 0) {
1225
+ return this._withEventResultMethods(Promise.reject(new Error('timeout must be >= 0 or null')))
1226
+ }
1227
+ if (!this.event_bus && original.event_status !== 'completed') {
1228
+ return this._withEventResultMethods(Promise.reject(new Error('event has no bus attached')))
1229
+ }
1230
+ if (options.first_result) {
1231
+ return this._withEventResultMethods(this._waitForFirstResultOrCompletion(options))
1232
+ }
1233
+ if (original.event_status === 'completed') {
1234
+ return this._withEventResultMethods(Promise.resolve(this))
1003
1235
  }
1004
1236
  this._notifyDoneListeners()
1005
- return this._event_completed_signal!.promise
1237
+ const timeout = options.timeout ?? null
1238
+ return this._withEventResultMethods(
1239
+ this._timeoutPromise(
1240
+ timeout,
1241
+ () => `Timed out waiting for ${original.event_type} completion after ${timeout}s`,
1242
+ () => this._event_completed_signal!.promise.then(() => this)
1243
+ )
1244
+ )
1245
+ }
1246
+
1247
+ async eventResult(options: EventResultOptions<this> = {}): Promise<EventResultType<this> | undefined> {
1248
+ const original = this._event_original ?? this
1249
+ if (original.event_status === 'pending' && original.event_results.size === 0) {
1250
+ await this.now({ first_result: true })
1251
+ }
1252
+ return this._collectResultValues(options, 'registration').at(0)
1253
+ }
1254
+
1255
+ async eventResultsList(options: EventResultOptions<this> = {}): Promise<Array<EventResultType<this> | undefined>> {
1256
+ const original = this._event_original ?? this
1257
+ if (original.event_status === 'pending' && original.event_results.size === 0) {
1258
+ await this.now({ first_result: false })
1259
+ }
1260
+ return this._collectResultValues(options, 'registration')
1006
1261
  }
1007
1262
 
1008
1263
  _markBlocksParentCompletionIfAwaitedFromEmittingHandler(): void {
@@ -1115,36 +1370,14 @@ export class BaseEvent {
1115
1370
  )
1116
1371
  }
1117
1372
 
1118
- private _isFirstModeControlError(error: unknown): boolean {
1119
- if (!(error instanceof EventHandlerCancelledError || error instanceof EventHandlerAbortedError)) {
1120
- return false
1121
- }
1122
- if (error.message.includes('first() resolved')) {
1123
- return true
1124
- }
1125
- return error.cause instanceof Error && error.cause.message.includes('first() resolved')
1126
- }
1127
-
1128
- _firstProcessingError(options: { ignore_first_mode_control_errors?: boolean } = {}): unknown | undefined {
1129
- const ignore_first_mode_control_errors = options.ignore_first_mode_control_errors ?? false
1373
+ _firstProcessingError(): unknown | undefined {
1130
1374
  return Array.from(this.event_results.values())
1131
1375
  .filter((event_result) => event_result.error !== undefined && event_result.completed_at !== null)
1132
- .filter((event_result) => (ignore_first_mode_control_errors ? !this._isFirstModeControlError(event_result.error) : true))
1133
1376
  .sort((event_result_a, event_result_b) => compareIsoDatetime(event_result_a.completed_at, event_result_b.completed_at))
1134
1377
  .map((event_result) => event_result.error)
1135
1378
  .at(0)
1136
1379
  }
1137
1380
 
1138
- // Returns the first non-undefined completed handler result, sorted by completion time.
1139
- // Useful after first() or done() to get the winning result value.
1140
- get event_result(): EventResultType<this> | undefined {
1141
- return Array.from(this.event_results.values())
1142
- .filter((event_result) => event_result.completed_at !== null && event_result.result !== undefined)
1143
- .sort((event_result_a, event_result_b) => compareIsoDatetime(event_result_a.completed_at, event_result_b.completed_at))
1144
- .map((event_result) => event_result.result as EventResultType<this>)
1145
- .at(0)
1146
- }
1147
-
1148
1381
  _areAllChildrenComplete(visited: Set<string> = new Set()): boolean {
1149
1382
  const original = this._event_original ?? this
1150
1383
  if (visited.has(original.event_id)) {