abxbus 2.4.32 → 2.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +74 -51
  2. package/dist/cjs/BaseEvent.d.ts +46 -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/events_suck.js +1 -1
  16. package/dist/cjs/events_suck.js.map +2 -2
  17. package/dist/cjs/index.d.ts +1 -0
  18. package/dist/cjs/index.js.map +2 -2
  19. package/dist/cjs/timing.js +1 -1
  20. package/dist/cjs/timing.js.map +2 -2
  21. package/dist/esm/BaseEvent.js +351 -170
  22. package/dist/esm/BaseEvent.js.map +3 -3
  23. package/dist/esm/EventBus.js +153 -85
  24. package/dist/esm/EventBus.js.map +2 -2
  25. package/dist/esm/EventHandler.js.map +1 -1
  26. package/dist/esm/EventResult.js +16 -22
  27. package/dist/esm/EventResult.js.map +2 -2
  28. package/dist/esm/LockManager.js +4 -1
  29. package/dist/esm/LockManager.js.map +2 -2
  30. package/dist/esm/events_suck.js +1 -1
  31. package/dist/esm/events_suck.js.map +2 -2
  32. package/dist/esm/index.js.map +2 -2
  33. package/dist/esm/timing.js +1 -1
  34. package/dist/esm/timing.js.map +2 -2
  35. package/dist/types/BaseEvent.d.ts +46 -55
  36. package/dist/types/EventBus.d.ts +8 -1
  37. package/dist/types/EventHandler.d.ts +3 -3
  38. package/dist/types/LockManager.d.ts +1 -0
  39. package/dist/types/index.d.ts +1 -0
  40. package/package.json +4 -3
  41. package/src/BaseEvent.ts +456 -219
  42. package/src/EventBus.ts +186 -99
  43. package/src/EventHandler.ts +3 -3
  44. package/src/EventResult.ts +18 -22
  45. package/src/LockManager.ts +5 -1
  46. package/src/events_suck.ts +1 -1
  47. package/src/index.ts +1 -0
  48. package/src/timing.ts +1 -1
  49. package/dist/cjs/base_event.d.ts +0 -211
  50. package/dist/cjs/bridge_jsonl.d.ts +0 -26
  51. package/dist/cjs/bridge_nats.d.ts +0 -20
  52. package/dist/cjs/bridge_postgres.d.ts +0 -31
  53. package/dist/cjs/bridge_redis.d.ts +0 -34
  54. package/dist/cjs/bridge_sqlite.d.ts +0 -30
  55. package/dist/cjs/event_bus.d.ts +0 -125
  56. package/dist/cjs/event_handler.d.ts +0 -139
  57. package/dist/cjs/event_history.d.ts +0 -45
  58. package/dist/cjs/event_result.d.ts +0 -86
  59. package/dist/cjs/lock_manager.d.ts +0 -70
  60. package/dist/types/base_event.d.ts +0 -211
  61. package/dist/types/bridge_jsonl.d.ts +0 -26
  62. package/dist/types/bridge_nats.d.ts +0 -20
  63. package/dist/types/bridge_postgres.d.ts +0 -31
  64. package/dist/types/bridge_redis.d.ts +0 -34
  65. package/dist/types/bridge_sqlite.d.ts +0 -30
  66. package/dist/types/event_bus.d.ts +0 -125
  67. package/dist/types/event_handler.d.ts +0 -139
  68. package/dist/types/event_history.d.ts +0 -45
  69. package/dist/types/event_result.d.ts +0 -86
  70. package/dist/types/lock_manager.d.ts +0 -70
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,23 @@ 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
+ type ResultSchemaFromEventSchema<TSchema> = TSchema extends z.ZodObject<infer TShape> ? ResultSchemaFromShape<TShape> : unknown
165
+ export type EventResultInclude<TEvent extends BaseEvent> = (
166
+ result: EventResult<TEvent>['result'],
138
167
  event_result: EventResult<TEvent>
139
168
  ) => boolean
140
- type EventResultsListOptions<TEvent extends BaseEvent> = {
141
- timeout?: number | null
142
- include?: EventResultsListInclude<TEvent>
169
+ export type EventResultOptions<TEvent extends BaseEvent> = {
170
+ include?: EventResultInclude<TEvent>
143
171
  raise_if_any?: boolean
144
172
  raise_if_none?: boolean
145
173
  }
146
- type EventDoneOptions = {
147
- raise_if_any?: boolean
174
+ export type EventWaitOptions = {
175
+ timeout?: number | null
176
+ first_result?: boolean
177
+ }
178
+ export type EventWaitPromise<TEvent extends BaseEvent> = Promise<TEvent> & {
179
+ eventResult(options?: EventResultOptions<TEvent>): Promise<EventResultType<TEvent> | undefined>
180
+ eventResultsList(options?: EventResultOptions<TEvent>): Promise<Array<EventResultType<TEvent> | undefined>>
148
181
  }
149
182
  type EventResultUpdateOptions<TEvent extends BaseEvent> = {
150
183
  eventbus?: EventBus
@@ -153,13 +186,12 @@ type EventResultUpdateOptions<TEvent extends BaseEvent> = {
153
186
  error?: unknown
154
187
  }
155
188
 
156
- const EVENT_CLASS_DEFAULTS = new WeakMap<Function, Record<string, unknown>>()
157
189
  const ROOT_EVENTBUS_ID = '00000000-0000-0000-0000-000000000000'
158
190
 
159
191
  export type EventFactory<TShape extends z.ZodRawShape, TResult = unknown> = {
160
192
  (data: EventInit<TShape>): EventWithResultSchema<TResult> & EventPayload<TShape>
161
193
  new (data: EventInit<TShape>): EventWithResultSchema<TResult> & EventPayload<TShape>
162
- schema: EventSchema<TShape>
194
+ event_schema: EventSchema<TShape>
163
195
  class?: new (data: EventInit<TShape>) => EventWithResultSchema<TResult> & EventPayload<TShape>
164
196
  event_type?: string
165
197
  event_version?: string
@@ -167,6 +199,17 @@ export type EventFactory<TShape extends z.ZodRawShape, TResult = unknown> = {
167
199
  fromJSON?: (data: unknown) => EventWithResultSchema<TResult> & EventPayload<TShape>
168
200
  }
169
201
 
202
+ export type SchemaEventFactory<TSchema extends AnyEventSchema, TResult = unknown> = {
203
+ (data: EventInitFromSchema<TSchema>): EventWithResultSchema<TResult> & EventPayloadFromSchema<TSchema>
204
+ new (data: EventInitFromSchema<TSchema>): EventWithResultSchema<TResult> & EventPayloadFromSchema<TSchema>
205
+ event_schema: TSchema
206
+ class?: new (data: EventInitFromSchema<TSchema>) => EventWithResultSchema<TResult> & EventPayloadFromSchema<TSchema>
207
+ event_type?: string
208
+ event_version?: string
209
+ event_result_type?: z.ZodTypeAny
210
+ fromJSON?: (data: unknown) => EventWithResultSchema<TResult> & EventPayloadFromSchema<TSchema>
211
+ }
212
+
170
213
  type ZodShapeFrom<TShape extends Record<string, unknown>> = {
171
214
  [K in keyof TShape as K extends 'event_result_type' ? never : TShape[K] extends z.ZodTypeAny ? K : never]: Extract<
172
215
  TShape[K],
@@ -174,6 +217,130 @@ type ZodShapeFrom<TShape extends Record<string, unknown>> = {
174
217
  >
175
218
  }
176
219
 
220
+ function baseEventDefaultShape(event_type: string): z.ZodRawShape {
221
+ return {
222
+ event_id: z.string().uuid(),
223
+ event_created_at: z.string().datetime(),
224
+ event_type: z.string().default(event_type),
225
+ event_version: z.string().default('0.0.1'),
226
+ event_timeout: z.number().nonnegative().nullable().default(null),
227
+ event_slow_timeout: z.number().nonnegative().nullable().optional(),
228
+ event_handler_timeout: z.number().nonnegative().nullable().optional(),
229
+ event_handler_slow_timeout: z.number().nonnegative().nullable().optional(),
230
+ event_blocks_parent_completion: z.boolean().default(false),
231
+ event_parent_id: z.string().uuid().nullable().optional(),
232
+ event_path: z.array(z.string()).optional(),
233
+ event_result_type: z.unknown().optional(),
234
+ event_emitted_by_handler_id: z.string().uuid().nullable().optional(),
235
+ event_pending_bus_count: z.number().nonnegative().optional(),
236
+ event_status: z.enum(['pending', 'started', 'completed']).optional(),
237
+ event_started_at: z.string().datetime().nullable().optional(),
238
+ event_completed_at: z.string().datetime().nullable().optional(),
239
+ event_results: z.record(z.string(), z.unknown()).optional(),
240
+ event_concurrency: z.enum(EVENT_CONCURRENCY_MODES).nullable().optional(),
241
+ event_handler_concurrency: z.enum(EVENT_HANDLER_CONCURRENCY_MODES).nullable().optional(),
242
+ event_handler_completion: z.enum(EVENT_HANDLER_COMPLETION_MODES).nullable().optional(),
243
+ }
244
+ }
245
+
246
+ function missingBaseFields(event_type: string, user_shape: z.ZodRawShape): z.ZodRawShape {
247
+ return Object.fromEntries(Object.entries(baseEventDefaultShape(event_type)).filter(([key]) => !(key in user_shape))) as z.ZodRawShape
248
+ }
249
+
250
+ type ZodSchemaWithPrefault = z.ZodTypeAny & {
251
+ prefault: (value: unknown) => z.ZodTypeAny
252
+ }
253
+
254
+ function shortcutDefaultSchema(base_field_schema: z.ZodTypeAny | undefined, value: unknown): z.ZodTypeAny {
255
+ if (!base_field_schema) {
256
+ return z.unknown().optional().default(value)
257
+ }
258
+ return (base_field_schema as ZodSchemaWithPrefault).prefault(base_field_schema.parse(value))
259
+ }
260
+
261
+ function schemaDefaultsForShortcut(event_type: string, raw_shape: Record<string, unknown>): z.ZodRawShape {
262
+ const defaults: Record<string, z.ZodTypeAny> = {}
263
+ const base_shape = baseEventDefaultShape(event_type)
264
+ for (const [key, value] of Object.entries(raw_shape)) {
265
+ if (key === 'event_result_type') continue
266
+ if (!isZodSchema(value)) {
267
+ defaults[key] = shortcutDefaultSchema(base_shape[key] as z.ZodTypeAny | undefined, value)
268
+ }
269
+ }
270
+ return defaults
271
+ }
272
+
273
+ function zodFieldsForShortcut(raw_shape: Record<string, unknown>): z.ZodRawShape {
274
+ const fields: Record<string, z.ZodTypeAny> = {}
275
+ for (const [key, value] of Object.entries(raw_shape)) {
276
+ if (key === 'event_result_type') continue
277
+ if (isZodSchema(value)) {
278
+ fields[key] = value
279
+ }
280
+ }
281
+ return fields
282
+ }
283
+
284
+ function eventResultTypeFromObjectSchema(schema: z.ZodObject<z.ZodRawShape>): z.ZodTypeAny | undefined {
285
+ const raw_event_result_type = schema.shape.event_result_type
286
+ return raw_event_result_type === undefined ? undefined : normalizeEventResultType(raw_event_result_type)
287
+ }
288
+
289
+ function buildFullEventSchema(
290
+ event_type: string,
291
+ spec: unknown
292
+ ): {
293
+ event_schema: AnyEventSchema
294
+ event_result_type?: z.ZodTypeAny
295
+ event_version?: string
296
+ } {
297
+ if (isZodObjectSchema(spec)) {
298
+ const user_shape = spec.shape
299
+ assertNoReservedUserEventFields(user_shape, `BaseEvent.extend(${event_type})`)
300
+ assertNoUnknownEventPrefixedFields(user_shape, `BaseEvent.extend(${event_type})`)
301
+ assertNoModelPrefixedFields(user_shape, `BaseEvent.extend(${event_type})`)
302
+ const full_schema = spec.safeExtend({
303
+ event_result_type: z.unknown().optional(),
304
+ ...missingBaseFields(event_type, user_shape),
305
+ })
306
+ return {
307
+ event_schema: full_schema,
308
+ event_result_type: eventResultTypeFromObjectSchema(spec),
309
+ }
310
+ }
311
+
312
+ const raw_shape = (isRecord(spec) ? spec : {}) as Record<string, unknown>
313
+ assertNoReservedUserEventFields(raw_shape, `BaseEvent.extend(${event_type})`)
314
+ assertNoUnknownEventPrefixedFields(raw_shape, `BaseEvent.extend(${event_type})`)
315
+ assertNoModelPrefixedFields(raw_shape, `BaseEvent.extend(${event_type})`)
316
+ const shortcut_shape = {
317
+ ...schemaDefaultsForShortcut(event_type, raw_shape),
318
+ ...zodFieldsForShortcut(raw_shape),
319
+ }
320
+ const full_schema = z.object(shortcut_shape).safeExtend(missingBaseFields(event_type, shortcut_shape)).loose()
321
+ return {
322
+ event_schema: full_schema,
323
+ event_result_type: normalizeEventResultType(raw_shape.event_result_type),
324
+ event_version: typeof raw_shape.event_version === 'string' ? raw_shape.event_version : undefined,
325
+ }
326
+ }
327
+
328
+ function decodeEventSchema(schema: AnyEventSchema, input: unknown): Record<string, unknown> {
329
+ const decoded = (z as unknown as { decode: (schema: AnyEventSchema, input: unknown) => unknown }).decode(schema, input)
330
+ if (!isRecord(decoded)) {
331
+ throw new Error('BaseEvent schema must decode to an object')
332
+ }
333
+ return decoded
334
+ }
335
+
336
+ function encodeEventSchema(schema: AnyEventSchema, input: Record<string, unknown>): Record<string, unknown> {
337
+ const encoded = (z as unknown as { encode: (schema: AnyEventSchema, input: unknown) => unknown }).encode(schema, input)
338
+ if (!isRecord(encoded)) {
339
+ throw new Error('BaseEvent schema must encode to an object')
340
+ }
341
+ return encoded
342
+ }
343
+
177
344
  export class BaseEvent {
178
345
  // event metadata fields
179
346
  event_id!: string // unique uuidv7 identifier for the event
@@ -184,7 +351,7 @@ export class BaseEvent {
184
351
  event_slow_timeout?: number | null // optional per-event slow warning threshold in seconds
185
352
  event_handler_timeout?: number | null // optional per-event handler timeout override in seconds
186
353
  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()
354
+ event_blocks_parent_completion!: boolean // true only for children explicitly awaited via now()
188
355
  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
356
  event_path!: string[] // list of bus labels (name#id) that the event has been dispatched to, including the current bus
190
357
  event_result_type?: z.ZodTypeAny // optional zod schema to enforce the shape of return values from handlers
@@ -197,15 +364,18 @@ export class BaseEvent {
197
364
  event_concurrency?: EventConcurrencyMode | null // concurrency mode for the event as a whole in relation to other events
198
365
  event_handler_concurrency?: EventHandlerConcurrencyMode | null // concurrency mode for the handlers within the event
199
366
  event_handler_completion?: EventHandlerCompletionMode | null // completion strategy: 'all' (default) waits for every handler, 'first' returns earliest non-undefined result and cancels the rest
367
+ event_schema?: z.ZodTypeAny
200
368
 
201
369
  static event_type?: string // class name of the event, e.g. BaseEvent.extend("MyEvent").event_type === "MyEvent"
202
370
  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
371
+ static event_result_type?: z.ZodTypeAny
372
+ static event_schema: AnyEventSchema = BaseEventSchema // generated Zod schema for local TS event data validation; never sent over the wire
204
373
 
205
374
  // internal runtime state
206
375
  event_bus?: EventBus // bus that dispatched this event, also used by event.emit(child)
207
376
  _event_original?: BaseEvent // underlying event object that was dispatched, if this is a bus-scoped proxy wrapping it
208
377
  _event_dispatch_context?: unknown | null // captured AsyncLocalStorage context at dispatch site, used to restore that context when running handlers
378
+ _event_fields_set?: Set<string>
209
379
 
210
380
  _event_completed_signal: Deferred<this> | null
211
381
  _lock_for_event_handler: AsyncLock | null
@@ -216,39 +386,48 @@ export class BaseEvent {
216
386
  const ctor = this.constructor as typeof BaseEvent & {
217
387
  event_version?: string
218
388
  event_result_type?: z.ZodTypeAny
389
+ event_schema?: AnyEventSchema
219
390
  }
220
- const ctor_defaults = EVENT_CLASS_DEFAULTS.get(ctor) ?? {}
221
- const merged_data = {
222
- ...ctor_defaults,
223
- ...data,
224
- } as BaseEventInit<Record<string, unknown>>
391
+ const explicit_event_fields = new Set(Object.keys(data ?? {}))
392
+ const merged_data = { ...data } as BaseEventInit<Record<string, unknown>>
225
393
  const event_type = merged_data.event_type ?? ctor.event_type ?? ctor.name
226
394
  const event_version = merged_data.event_version ?? ctor.event_version ?? '0.0.1'
227
395
  const raw_event_result_type = merged_data.event_result_type ?? ctor.event_result_type
228
396
  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
397
 
234
- const base_data = {
398
+ const event_schema = ctor.event_schema ?? BaseEventSchema
399
+ const base_data: Record<string, unknown> = {
235
400
  ...merged_data,
236
- event_id,
237
- event_created_at,
401
+ event_id: merged_data.event_id ?? uuidv7(),
402
+ event_created_at: merged_data.event_created_at ?? monotonicDatetime(),
238
403
  event_type,
239
404
  event_version,
240
- event_timeout,
241
- event_blocks_parent_completion,
242
405
  event_result_type,
243
406
  }
407
+ if (event_schema === BaseEventSchema) {
408
+ base_data.event_timeout ??= null
409
+ base_data.event_blocks_parent_completion ??= false
410
+ }
244
411
 
245
- const schema = ctor.schema ?? BaseEventSchema
246
- const parsed = schema.parse(base_data) as BaseEventData & Record<string, unknown>
412
+ const parsed = decodeEventSchema(event_schema, base_data) as BaseEventData & Record<string, unknown>
247
413
 
248
414
  Object.assign(this, parsed)
415
+ Object.defineProperty(this, 'event_schema', {
416
+ value: event_schema,
417
+ writable: true,
418
+ enumerable: false,
419
+ configurable: true,
420
+ })
421
+ Object.defineProperty(this, '_event_fields_set', {
422
+ value: explicit_event_fields,
423
+ writable: true,
424
+ enumerable: false,
425
+ configurable: true,
426
+ })
249
427
 
250
428
  const parsed_path = (parsed as { event_path?: string[] }).event_path
251
429
  this.event_path = Array.isArray(parsed_path) ? [...parsed_path] : []
430
+ this.event_created_at = monotonicDatetime(parsed.event_created_at)
252
431
 
253
432
  // load event results from potentially raw objects from JSON to proper EventResult objects
254
433
  this.event_results = hydrateEventResults(this, (parsed as { event_results?: unknown }).event_results)
@@ -273,7 +452,7 @@ export class BaseEvent {
273
452
  ? (parsed as { event_emitted_by_handler_id: string }).event_emitted_by_handler_id
274
453
  : null
275
454
 
276
- this.event_result_type = event_result_type
455
+ this.event_result_type = normalizeEventResultType(parsed.event_result_type ?? event_result_type)
277
456
 
278
457
  this._event_completed_signal = null
279
458
  this._lock_for_event_handler = null
@@ -287,6 +466,10 @@ export class BaseEvent {
287
466
 
288
467
  // main entry point for users to define their own event types
289
468
  // BaseEvent.extend("MyEvent", { some_custom_field: z.string(), event_result_type: z.string(), event_timeout: 25, ... }) -> MyEvent
469
+ static extend<TSchema extends z.ZodObject<z.ZodRawShape>>(
470
+ event_type: string,
471
+ event_schema: TSchema
472
+ ): SchemaEventFactory<TSchema, ResultSchemaFromEventSchema<TSchema>>
290
473
  static extend<TShape extends z.ZodRawShape>(event_type: string, shape?: TShape): EventFactory<TShape, ResultSchemaFromShape<TShape>>
291
474
  static extend<TShape extends Record<string, unknown>>(
292
475
  event_type: string,
@@ -294,32 +477,21 @@ export class BaseEvent {
294
477
  ): EventFactory<ZodShapeFrom<TShape>, ResultSchemaFromShape<TShape>>
295
478
  static extend<TShape extends Record<string, unknown>>(
296
479
  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)
480
+ shape?: TShape
481
+ ): EventFactory<ZodShapeFrom<TShape>, ResultSchemaFromShape<TShape>> | SchemaEventFactory<AnyEventSchema, unknown> {
482
+ const built = buildFullEventSchema(event_type, shape ?? {})
483
+ const full_schema = built.event_schema
484
+ const event_result_type = built.event_result_type
485
+ const event_version = built.event_version
314
486
 
315
487
  // create a new event class that extends BaseEvent and adds the custom fields
316
488
  class ExtendedEvent extends BaseEvent {
317
- static schema = full_schema as unknown as typeof BaseEvent.schema
489
+ static event_schema = full_schema
318
490
  static event_type = event_type
319
491
  static event_version = event_version ?? BaseEvent.event_version
320
492
  static event_result_type = event_result_type
321
493
 
322
- constructor(data: EventInit<ZodShapeFrom<TShape>>) {
494
+ constructor(data: EventInit<ZodShapeFrom<TShape>> | EventInitFromSchema<AnyEventSchema>) {
323
495
  super(data as BaseEventInit<Record<string, unknown>>)
324
496
  }
325
497
  }
@@ -330,27 +502,40 @@ export class BaseEvent {
330
502
  return new ExtendedEvent(data) as FactoryResult
331
503
  }
332
504
 
333
- EventFactory.schema = full_schema as EventSchema<ZodShapeFrom<TShape>>
505
+ EventFactory.event_schema = full_schema as EventSchema<ZodShapeFrom<TShape>>
334
506
  EventFactory.event_type = event_type
335
507
  EventFactory.event_version = event_version ?? BaseEvent.event_version
336
508
  EventFactory.event_result_type = event_result_type
337
509
  EventFactory.class = ExtendedEvent as unknown as new (
338
510
  data: EventInit<ZodShapeFrom<TShape>>
339
511
  ) => EventWithResultSchema<ResultSchemaFromShape<TShape>> & EventPayload<ZodShapeFrom<TShape>>
340
- EventFactory.fromJSON = (data: unknown) => (ExtendedEvent.fromJSON as (data: unknown) => FactoryResult)(data)
512
+ EventFactory.fromJSON = (data: unknown) => ExtendedEvent.fromJSON(data) as FactoryResult
341
513
  EventFactory.prototype = ExtendedEvent.prototype
342
- EVENT_CLASS_DEFAULTS.set(ExtendedEvent, event_defaults)
514
+ EVENT_TYPE_REGISTRY.set(event_type, ExtendedEvent)
343
515
 
344
516
  return EventFactory as unknown as EventFactory<ZodShapeFrom<TShape>, ResultSchemaFromShape<TShape>>
345
517
  }
346
518
 
347
519
  static fromJSON<T extends typeof BaseEvent>(this: T, data: unknown): InstanceType<T> {
348
520
  if (!data || typeof data !== 'object') {
349
- const schema = this.schema ?? BaseEventSchema
350
- const parsed = schema.parse(data)
521
+ const event_schema = this.event_schema ?? BaseEventSchema
522
+ const parsed = decodeEventSchema(event_schema, data)
351
523
  return new this(parsed) as InstanceType<T>
352
524
  }
353
525
  const record = { ...(data as Record<string, unknown>) }
526
+ if (this === BaseEvent) {
527
+ const event_type = record.event_type
528
+ if (typeof event_type === 'string') {
529
+ const KnownEvent = EVENT_TYPE_REGISTRY.get(event_type)
530
+ if (KnownEvent) {
531
+ return KnownEvent.fromJSON(record) as InstanceType<T>
532
+ }
533
+ }
534
+ }
535
+ const ctor = this as typeof BaseEvent
536
+ if (this !== BaseEvent && ctor.event_result_type && record.event_result_type !== undefined) {
537
+ delete record.event_result_type
538
+ }
354
539
  if (record.event_result_type !== undefined && record.event_result_type !== null) {
355
540
  record.event_result_type = normalizeEventResultType(record.event_result_type)
356
541
  }
@@ -374,7 +559,7 @@ export class BaseEvent {
374
559
  toJSON(): BaseEventJSON {
375
560
  const record: Record<string, unknown> = {}
376
561
  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
562
+ if (key.startsWith('_') || key === 'bus' || key === 'event_bus' || key === 'event_schema' || key === 'event_results') continue
378
563
  if (value === undefined || typeof value === 'function') continue
379
564
  record[key] = value
380
565
  }
@@ -382,11 +567,45 @@ export class BaseEvent {
382
567
  Array.from(this.event_results.entries()).map(([handler_id, result]) => [handler_id, result.toJSON()])
383
568
  )
384
569
 
385
- return {
570
+ const event_schema = ((this.constructor as typeof BaseEvent).event_schema ?? this.event_schema ?? BaseEventSchema) as AnyEventSchema
571
+ const encoded = encodeEventSchema(event_schema, {
386
572
  ...record,
387
573
  event_id: this.event_id,
388
574
  event_type: this.event_type,
389
575
  event_version: this.event_version,
576
+ event_result_type: this.event_result_type,
577
+
578
+ // static configuration options
579
+ event_timeout: this.event_timeout,
580
+ event_slow_timeout: this.event_slow_timeout,
581
+ event_concurrency: this.event_concurrency,
582
+ event_handler_concurrency: this.event_handler_concurrency,
583
+ event_handler_completion: this.event_handler_completion,
584
+ event_handler_slow_timeout: this.event_handler_slow_timeout,
585
+ event_handler_timeout: this.event_handler_timeout,
586
+ event_blocks_parent_completion: this.event_blocks_parent_completion,
587
+
588
+ // mutable parent/child/bus tracking runtime state
589
+ event_parent_id: this.event_parent_id,
590
+ event_path: this.event_path,
591
+ event_emitted_by_handler_id: this.event_emitted_by_handler_id,
592
+ event_pending_bus_count: this.event_pending_bus_count,
593
+
594
+ // mutable runtime status and timestamps
595
+ event_status: this.event_status,
596
+ event_created_at: this.event_created_at,
597
+ event_started_at: this.event_started_at ?? null,
598
+ event_completed_at: this.event_completed_at ?? null,
599
+
600
+ ...(Object.keys(event_results).length > 0 ? { event_results } : {}),
601
+ })
602
+ delete encoded.event_schema
603
+
604
+ return {
605
+ ...encoded,
606
+ event_id: this.event_id,
607
+ event_type: this.event_type,
608
+ event_version: this.event_version,
390
609
  event_result_type: this.event_result_type ? toJsonSchema(this.event_result_type) : this.event_result_type,
391
610
 
392
611
  // static configuration options
@@ -416,13 +635,15 @@ export class BaseEvent {
416
635
  }
417
636
  }
418
637
 
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
638
+ _createSlowEventWarningTimer(
639
+ event_slow_timeout: number | null = this.event_slow_timeout ?? null,
640
+ bus_name?: string
641
+ ): ReturnType<typeof setTimeout> | null {
642
+ const event_warn_ms = event_slow_timeout === null || event_slow_timeout <= 0 ? null : event_slow_timeout * 1000
422
643
  if (event_warn_ms === null) {
423
644
  return null
424
645
  }
425
- const name = this.event_bus?.name ?? 'EventBus'
646
+ const name = bus_name ?? this.event_bus?.name ?? 'EventBus'
426
647
  return setTimeout(() => {
427
648
  if (this.event_status === 'completed') {
428
649
  return
@@ -528,7 +749,28 @@ export class BaseEvent {
528
749
  }
529
750
 
530
751
  private _isFirstModeWinningResult(entry: EventResult): boolean {
531
- return entry.status === 'completed' && entry.result !== undefined && entry.result !== null && !(entry.result instanceof BaseEvent)
752
+ return BaseEvent._defaultResultInclude(entry.result, entry)
753
+ }
754
+
755
+ private static _defaultResultInclude<TEvent extends BaseEvent>(
756
+ result: EventResult<TEvent>['result'],
757
+ event_result: EventResult<TEvent>
758
+ ): boolean {
759
+ return (
760
+ event_result.status === 'completed' &&
761
+ result !== undefined &&
762
+ result !== null &&
763
+ !(result instanceof Error) &&
764
+ !(result instanceof BaseEvent) &&
765
+ event_result.error === undefined
766
+ )
767
+ }
768
+
769
+ private static _includeEventResult<TEvent extends BaseEvent>(
770
+ include: EventResultInclude<TEvent>,
771
+ event_result: EventResult<TEvent>
772
+ ): boolean {
773
+ return include(event_result.result, event_result)
532
774
  }
533
775
 
534
776
  private _markFirstModeWinnerIfNeeded(original: BaseEvent, entry: EventResult, first_state: { found: boolean }): void {
@@ -543,9 +785,13 @@ export class BaseEvent {
543
785
  if (!this.event_bus) {
544
786
  throw new Error('event has no bus attached')
545
787
  }
546
- await this.event_bus.locks._runWithHandlerLock(original, this.event_bus.event_handler_concurrency, async (handler_lock) => {
547
- await entry.runHandler(handler_lock)
548
- })
788
+ await this.event_bus.locks._runWithHandlerLock(
789
+ original,
790
+ original.event_handler_concurrency ?? this.event_bus.event_handler_concurrency,
791
+ async (handler_lock) => {
792
+ await entry.runHandler(handler_lock)
793
+ }
794
+ )
549
795
  }
550
796
 
551
797
  // Run all pending handler results for the current bus context.
@@ -562,7 +808,7 @@ export class BaseEvent {
562
808
  }
563
809
  const resolved_completion = original.event_handler_completion ?? this.event_bus?.event_handler_completion ?? 'all'
564
810
  if (resolved_completion === 'first') {
565
- if (original._getHandlerLock(this.event_bus?.event_handler_concurrency) !== null) {
811
+ if (original._getHandlerLock(original.event_handler_concurrency ?? this.event_bus?.event_handler_concurrency ?? 'serial') !== null) {
566
812
  for (const entry of pending_results) {
567
813
  await this._runHandlerWithLock(original, entry)
568
814
  if (!this._isFirstModeWinningResult(entry)) {
@@ -590,7 +836,7 @@ export class BaseEvent {
590
836
 
591
837
  _getHandlerLock(default_concurrency?: EventHandlerConcurrencyMode): AsyncLock | null {
592
838
  const original = this._event_original ?? this
593
- const resolved = original.event_handler_concurrency ?? default_concurrency ?? original.event_bus?.event_handler_concurrency ?? 'serial'
839
+ const resolved = original.event_handler_concurrency ?? default_concurrency ?? 'serial'
594
840
  if (resolved === 'parallel') {
595
841
  return null
596
842
  }
@@ -715,7 +961,7 @@ export class BaseEvent {
715
961
  original_child._markCancelled(cancellation_cause)
716
962
 
717
963
  // Force-complete the child event. In JS we can't stop running async
718
- // handlers, but _markCompleted() resolves the done() promise so callers
964
+ // handlers, but _markCompleted() resolves active waiters so callers
719
965
  // aren't blocked waiting for background work to finish. The background
720
966
  // handler's eventual _markCompleted/_markError is a no-op (terminal guard).
721
967
  if (original_child.event_status !== 'completed') {
@@ -732,11 +978,11 @@ export class BaseEvent {
732
978
  }
733
979
  }
734
980
 
735
- // Cancel all handler results for an event except the winner, used by first() mode.
981
+ // Cancel all handler results for an event except the winner, used by event_handler_completion='first'.
736
982
  // Cancels pending handlers immediately, aborts started handlers via _signalAbort(),
737
983
  // and cancels any child events emitted by the losing handlers.
738
984
  _markRemainingFirstModeResultCancelled(winner: EventResult): void {
739
- const cause = new Error('first() resolved: another handler returned a result first')
985
+ const cause = new Error("event_handler_completion='first' resolved: another handler returned a result first")
740
986
  const bus_id = winner.eventbus_id
741
987
 
742
988
  for (const result of this.event_results.values()) {
@@ -745,7 +991,7 @@ export class BaseEvent {
745
991
 
746
992
  if (result.status === 'pending') {
747
993
  result._markError(
748
- new EventHandlerCancelledError(`Cancelled: first() resolved`, {
994
+ new EventHandlerCancelledError(`Cancelled: event_handler_completion='first' resolved`, {
749
995
  event_result: result,
750
996
  cause,
751
997
  })
@@ -763,7 +1009,7 @@ export class BaseEvent {
763
1009
 
764
1010
  // Abort the handler itself
765
1011
  result._lock?.exitHandlerRun()
766
- const aborted_error = new EventHandlerAbortedError(`Aborted: first() resolved`, {
1012
+ const aborted_error = new EventHandlerAbortedError(`Aborted: event_handler_completion='first' resolved`, {
767
1013
  event_result: result,
768
1014
  cause,
769
1015
  })
@@ -848,127 +1094,52 @@ export class BaseEvent {
848
1094
  }
849
1095
  }
850
1096
 
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'))
1097
+ private _withEventResultMethods(promise: Promise<this>): EventWaitPromise<this> {
1098
+ const chainable = promise as EventWaitPromise<this>
1099
+ chainable.eventResult = async (options?: EventResultOptions<this>) => {
1100
+ const event = await promise
1101
+ return event.eventResult(options)
856
1102
  }
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
1103
+ chainable.eventResultsList = async (options?: EventResultOptions<this>) => {
1104
+ const event = await promise
1105
+ return event.eventResultsList(options)
865
1106
  }
1107
+ return chainable
1108
+ }
866
1109
 
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
- })
1110
+ private _timeoutPromise<T>(timeout: number | null, message: () => string, fn: () => Promise<T>): Promise<T> {
1111
+ return timeout === null || timeout <= 0 ? fn() : _runWithTimeout(timeout, () => new Error(message()), fn)
880
1112
  }
881
1113
 
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
- }
1114
+ private _orderedEventResults(): EventResult<this>[] {
890
1115
  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
- })
1116
+ return (Array.from(original.event_results.values()) as EventResult<this>[]).sort((a, b) =>
1117
+ compareIsoDatetime(a.completed_at, b.completed_at)
1118
+ )
910
1119
  }
911
1120
 
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
-
1121
+ private _orderedEventResultsByRegistration(): EventResult<this>[] {
943
1122
  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
- }
1123
+ return (Array.from(original.event_results.values()) as EventResult<this>[]).sort(
1124
+ (a, b) =>
1125
+ compareIsoDatetime(a.handler.handler_registered_at, b.handler.handler_registered_at) ||
1126
+ compareIsoDatetime(a.started_at, b.started_at) ||
1127
+ a.handler_id.localeCompare(b.handler_id)
1128
+ )
1129
+ }
956
1130
 
957
- const all_results: EventResult<this>[] = Array.from(completed_event.event_results.values())
1131
+ private _collectResultValues(
1132
+ options: EventResultOptions<this> = {},
1133
+ order: 'completion' | 'registration' = 'completion'
1134
+ ): Array<EventResultType<this> | undefined> {
1135
+ const include: EventResultInclude<this> = options.include ?? BaseEvent._defaultResultInclude
1136
+ const raise_if_any = options.raise_if_any ?? true
1137
+ const raise_if_none = options.raise_if_none ?? false
1138
+ const all_results = order === 'registration' ? this._orderedEventResultsByRegistration() : this._orderedEventResults()
958
1139
  const error_results = all_results.filter((event_result) => event_result.error !== undefined || event_result.result instanceof Error)
1140
+ const included_results = all_results.filter((event_result) => BaseEvent._includeEventResult(include, event_result))
959
1141
 
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
-
1142
+ if (error_results.length > 0 && raise_if_any) {
972
1143
  const errors = error_results.map((event_result) => {
973
1144
  if (event_result.error instanceof Error) {
974
1145
  return event_result.error
@@ -978,31 +1149,119 @@ export class BaseEvent {
978
1149
  }
979
1150
  return new Error(String(event_result.error ?? event_result.result))
980
1151
  })
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
- )
1152
+ if (errors.length === 1) {
1153
+ throw errors[0]
1154
+ }
1155
+ throw new AggregateError(errors, `Event ${this.event_type}#${this.event_id.slice(-4)} had ${errors.length} handler error(s)`)
985
1156
  }
986
1157
 
987
- const included_results = all_results.filter((event_result) => include(event_result.result, event_result))
988
1158
  if (raise_if_none && included_results.length === 0) {
989
1159
  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)}`
1160
+ `Expected at least one handler to return a non-null result, but none did: ${this.event_type}#${this.event_id.slice(-4)}`
991
1161
  )
992
1162
  }
993
1163
 
994
1164
  return included_results.map((event_result) => event_result.result)
995
1165
  }
996
1166
 
997
- // awaitable that waits for the event to be processed in normal queue order by the _runloop
998
- eventCompleted(): Promise<this> {
1167
+ private _hasIncludedResult(options: EventResultOptions<this> = {}): boolean {
1168
+ const include: EventResultInclude<this> = options.include ?? BaseEvent._defaultResultInclude
1169
+ return this._orderedEventResults().some((event_result) => BaseEvent._includeEventResult(include, event_result))
1170
+ }
1171
+
1172
+ private async _waitForFirstResultOrCompletion(options: EventWaitOptions & EventResultOptions<this> = {}): Promise<this> {
999
1173
  const original = this._event_original ?? this
1174
+ if (options.timeout !== undefined && options.timeout !== null && options.timeout < 0) {
1175
+ throw new Error('timeout must be >= 0 or null')
1176
+ }
1177
+ if (!this.event_bus && original.event_status !== 'completed') {
1178
+ throw new Error('event has no bus attached')
1179
+ }
1180
+ if (original.event_status === 'completed' || this._hasIncludedResult(options)) {
1181
+ return this
1182
+ }
1183
+
1184
+ const waitForResult = async (): Promise<this> => {
1185
+ for (;;) {
1186
+ if (original.event_status === 'completed' || this._hasIncludedResult(options)) {
1187
+ return this
1188
+ }
1189
+ await new Promise((resolve) => setTimeout(resolve, 1))
1190
+ }
1191
+ }
1192
+
1193
+ const timeout = options.timeout ?? null
1194
+ return this._timeoutPromise(timeout, () => `Timed out waiting for ${original.event_type} result after ${timeout}s`, waitForResult)
1195
+ }
1196
+
1197
+ // Active awaitable that triggers immediate (queue-jump) processing of the event on all buses where it is queued.
1198
+ now(options: EventWaitOptions = {}): EventWaitPromise<this> {
1199
+ const original = this._event_original ?? this
1200
+ if (options.timeout !== undefined && options.timeout !== null && options.timeout < 0) {
1201
+ return this._withEventResultMethods(Promise.reject(new Error('timeout must be >= 0 or null')))
1202
+ }
1203
+ if (!this.event_bus && original.event_status !== 'completed') {
1204
+ return this._withEventResultMethods(Promise.reject(new Error('event has no bus attached')))
1205
+ }
1000
1206
  original._markBlocksParentCompletionIfAwaitedFromEmittingHandler()
1001
- if (this.event_status === 'completed') {
1002
- return Promise.resolve(this)
1207
+ const resolved_timeout_seconds = options.timeout ?? null
1208
+ const processing =
1209
+ original.event_status === 'completed'
1210
+ ? Promise.resolve(this)
1211
+ : this._timeoutPromise(
1212
+ resolved_timeout_seconds,
1213
+ () => `Timed out waiting for ${original.event_type} completion after ${resolved_timeout_seconds}s`,
1214
+ () => this.event_bus!._processEventImmediately(this)
1215
+ )
1216
+
1217
+ if (options.first_result) {
1218
+ void processing.catch(() => undefined)
1219
+ return this._withEventResultMethods(this._waitForFirstResultOrCompletion(options))
1220
+ }
1221
+
1222
+ return this._withEventResultMethods(processing)
1223
+ }
1224
+
1225
+ // Passive awaitable that waits for normal queue-order processing without forcing execution.
1226
+ wait(options: EventWaitOptions = {}): EventWaitPromise<this> {
1227
+ const original = this._event_original ?? this
1228
+ if (options.timeout !== undefined && options.timeout !== null && options.timeout < 0) {
1229
+ return this._withEventResultMethods(Promise.reject(new Error('timeout must be >= 0 or null')))
1230
+ }
1231
+ if (!this.event_bus && original.event_status !== 'completed') {
1232
+ return this._withEventResultMethods(Promise.reject(new Error('event has no bus attached')))
1233
+ }
1234
+ if (options.first_result) {
1235
+ return this._withEventResultMethods(this._waitForFirstResultOrCompletion(options))
1236
+ }
1237
+ if (original.event_status === 'completed') {
1238
+ return this._withEventResultMethods(Promise.resolve(this))
1003
1239
  }
1004
1240
  this._notifyDoneListeners()
1005
- return this._event_completed_signal!.promise
1241
+ const timeout = options.timeout ?? null
1242
+ return this._withEventResultMethods(
1243
+ this._timeoutPromise(
1244
+ timeout,
1245
+ () => `Timed out waiting for ${original.event_type} completion after ${timeout}s`,
1246
+ () => this._event_completed_signal!.promise.then(() => this)
1247
+ )
1248
+ )
1249
+ }
1250
+
1251
+ async eventResult(options: EventResultOptions<this> = {}): Promise<EventResultType<this> | undefined> {
1252
+ const original = this._event_original ?? this
1253
+ if (original.event_status === 'pending' && original.event_results.size === 0) {
1254
+ await this.now({ first_result: true })
1255
+ }
1256
+ return this._collectResultValues(options, 'registration').at(0)
1257
+ }
1258
+
1259
+ async eventResultsList(options: EventResultOptions<this> = {}): Promise<Array<EventResultType<this> | undefined>> {
1260
+ const original = this._event_original ?? this
1261
+ if (original.event_status === 'pending' && original.event_results.size === 0) {
1262
+ await this.now({ first_result: false })
1263
+ }
1264
+ return this._collectResultValues(options, 'registration')
1006
1265
  }
1007
1266
 
1008
1267
  _markBlocksParentCompletionIfAwaitedFromEmittingHandler(): void {
@@ -1115,36 +1374,14 @@ export class BaseEvent {
1115
1374
  )
1116
1375
  }
1117
1376
 
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
1377
+ _firstProcessingError(): unknown | undefined {
1130
1378
  return Array.from(this.event_results.values())
1131
1379
  .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
1380
  .sort((event_result_a, event_result_b) => compareIsoDatetime(event_result_a.completed_at, event_result_b.completed_at))
1134
1381
  .map((event_result) => event_result.error)
1135
1382
  .at(0)
1136
1383
  }
1137
1384
 
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
1385
  _areAllChildrenComplete(visited: Set<string> = new Set()): boolean {
1149
1386
  const original = this._event_original ?? this
1150
1387
  if (visited.has(original.event_id)) {