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.
- package/README.md +74 -51
- package/dist/cjs/BaseEvent.d.ts +46 -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/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/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 +46 -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/index.d.ts +1 -0
- package/package.json +4 -3
- package/src/BaseEvent.ts +456 -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/dist/cjs/base_event.d.ts +0 -211
- package/dist/cjs/bridge_jsonl.d.ts +0 -26
- package/dist/cjs/bridge_nats.d.ts +0 -20
- package/dist/cjs/bridge_postgres.d.ts +0 -31
- package/dist/cjs/bridge_redis.d.ts +0 -34
- package/dist/cjs/bridge_sqlite.d.ts +0 -30
- package/dist/cjs/event_bus.d.ts +0 -125
- package/dist/cjs/event_handler.d.ts +0 -139
- package/dist/cjs/event_history.d.ts +0 -45
- package/dist/cjs/event_result.d.ts +0 -86
- package/dist/cjs/lock_manager.d.ts +0 -70
- package/dist/types/base_event.d.ts +0 -211
- package/dist/types/bridge_jsonl.d.ts +0 -26
- package/dist/types/bridge_nats.d.ts +0 -20
- package/dist/types/bridge_postgres.d.ts +0 -31
- package/dist/types/bridge_redis.d.ts +0 -34
- package/dist/types/bridge_sqlite.d.ts +0 -30
- package/dist/types/event_bus.d.ts +0 -125
- package/dist/types/event_handler.d.ts +0 -139
- package/dist/types/event_history.d.ts +0 -45
- package/dist/types/event_result.d.ts +0 -86
- 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 {
|
|
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,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
|
|
137
|
-
|
|
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
|
|
141
|
-
|
|
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
|
|
147
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
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.
|
|
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) =>
|
|
512
|
+
EventFactory.fromJSON = (data: unknown) => ExtendedEvent.fromJSON(data) as FactoryResult
|
|
341
513
|
EventFactory.prototype = ExtendedEvent.prototype
|
|
342
|
-
|
|
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
|
|
350
|
-
const parsed =
|
|
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
|
-
|
|
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(
|
|
420
|
-
|
|
421
|
-
|
|
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
|
|
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(
|
|
547
|
-
|
|
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 ??
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
return
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
})
|
|
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
|
-
|
|
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.
|
|
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
|
-
})
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
982
|
-
errors
|
|
983
|
-
|
|
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: ${
|
|
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
|
-
|
|
998
|
-
|
|
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
|
-
|
|
1002
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)) {
|