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.
- package/README.md +74 -51
- package/dist/cjs/BaseEvent.d.ts +45 -55
- package/dist/cjs/BaseEvent.js +350 -169
- package/dist/cjs/BaseEvent.js.map +3 -3
- package/dist/cjs/EventBus.d.ts +8 -1
- package/dist/cjs/EventBus.js +153 -85
- package/dist/cjs/EventBus.js.map +2 -2
- package/dist/cjs/EventHandler.d.ts +3 -3
- package/dist/cjs/EventHandler.js.map +1 -1
- package/dist/cjs/EventResult.js +16 -22
- package/dist/cjs/EventResult.js.map +2 -2
- package/dist/cjs/LockManager.d.ts +1 -0
- package/dist/cjs/LockManager.js +4 -1
- package/dist/cjs/LockManager.js.map +2 -2
- package/dist/cjs/base_event.d.ts +2 -2
- package/dist/cjs/bridge_ipc.d.ts +45 -0
- package/dist/cjs/event_handler.d.ts +1 -0
- package/dist/cjs/events_suck.js +1 -1
- package/dist/cjs/events_suck.js.map +2 -2
- package/dist/cjs/index.d.ts +1 -0
- package/dist/cjs/index.js.map +2 -2
- package/dist/cjs/middleware_otel_tracing.d.ts +49 -0
- package/dist/cjs/timing.js +1 -1
- package/dist/cjs/timing.js.map +2 -2
- package/dist/esm/BaseEvent.js +351 -170
- package/dist/esm/BaseEvent.js.map +3 -3
- package/dist/esm/EventBus.js +153 -85
- package/dist/esm/EventBus.js.map +2 -2
- package/dist/esm/EventHandler.js.map +1 -1
- package/dist/esm/EventResult.js +16 -22
- package/dist/esm/EventResult.js.map +2 -2
- package/dist/esm/LockManager.js +4 -1
- package/dist/esm/LockManager.js.map +2 -2
- package/dist/esm/events_suck.js +1 -1
- package/dist/esm/events_suck.js.map +2 -2
- package/dist/esm/index.js.map +2 -2
- package/dist/esm/timing.js +1 -1
- package/dist/esm/timing.js.map +2 -2
- package/dist/types/BaseEvent.d.ts +45 -55
- package/dist/types/EventBus.d.ts +8 -1
- package/dist/types/EventHandler.d.ts +3 -3
- package/dist/types/LockManager.d.ts +1 -0
- package/dist/types/base_event.d.ts +2 -2
- package/dist/types/bridge_ipc.d.ts +45 -0
- package/dist/types/event_handler.d.ts +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/middleware_otel_tracing.d.ts +49 -0
- package/package.json +4 -3
- package/src/BaseEvent.ts +452 -219
- package/src/EventBus.ts +186 -99
- package/src/EventHandler.ts +3 -3
- package/src/EventResult.ts +18 -22
- package/src/LockManager.ts +5 -1
- package/src/events_suck.ts +1 -1
- package/src/index.ts +1 -0
- 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 {
|
|
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([
|
|
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().
|
|
62
|
-
event_slow_timeout: z.number().
|
|
63
|
-
event_handler_timeout: z.number().
|
|
64
|
-
event_handler_slow_timeout: z.number().
|
|
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
|
|
137
|
-
result:
|
|
164
|
+
export type EventResultInclude<TEvent extends BaseEvent> = (
|
|
165
|
+
result: EventResult<TEvent>['result'],
|
|
138
166
|
event_result: EventResult<TEvent>
|
|
139
167
|
) => boolean
|
|
140
|
-
type
|
|
141
|
-
|
|
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
|
|
147
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
298
|
-
): EventFactory<ZodShapeFrom<TShape>, ResultSchemaFromShape<TShape>> {
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
|
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.
|
|
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) =>
|
|
508
|
+
EventFactory.fromJSON = (data: unknown) => ExtendedEvent.fromJSON(data) as FactoryResult
|
|
341
509
|
EventFactory.prototype = ExtendedEvent.prototype
|
|
342
|
-
|
|
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
|
|
350
|
-
const parsed =
|
|
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
|
-
|
|
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(
|
|
420
|
-
|
|
421
|
-
|
|
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
|
|
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(
|
|
547
|
-
|
|
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 ??
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
return
|
|
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
|
-
|
|
858
|
-
|
|
859
|
-
|
|
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
|
-
|
|
868
|
-
|
|
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
|
-
|
|
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.
|
|
892
|
-
|
|
893
|
-
|
|
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
|
-
|
|
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
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
982
|
-
errors
|
|
983
|
-
|
|
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: ${
|
|
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
|
-
|
|
998
|
-
|
|
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
|
-
|
|
1002
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)) {
|