abxbus 2.4.16 → 2.4.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/cjs/event_bus.js +1 -1
  2. package/dist/cjs/event_bus.js.map +2 -2
  3. package/dist/cjs/event_handler.d.ts +1 -0
  4. package/dist/cjs/event_handler.js +14 -1
  5. package/dist/cjs/event_handler.js.map +2 -2
  6. package/dist/cjs/middleware_otel_tracing.js +3 -3
  7. package/dist/cjs/middleware_otel_tracing.js.map +2 -2
  8. package/dist/cjs/retry.js +39 -0
  9. package/dist/cjs/retry.js.map +2 -2
  10. package/dist/esm/event_bus.js +1 -1
  11. package/dist/esm/event_bus.js.map +2 -2
  12. package/dist/esm/event_handler.js +14 -1
  13. package/dist/esm/event_handler.js.map +2 -2
  14. package/dist/esm/middleware_otel_tracing.js +4 -4
  15. package/dist/esm/middleware_otel_tracing.js.map +2 -2
  16. package/dist/esm/retry.js +39 -0
  17. package/dist/esm/retry.js.map +2 -2
  18. package/dist/types/event_handler.d.ts +1 -0
  19. package/package.json +5 -1
  20. package/src/async_context.ts +70 -0
  21. package/src/base_event.ts +1201 -0
  22. package/src/bridge_jsonl.ts +174 -0
  23. package/src/bridge_nats.ts +104 -0
  24. package/src/bridge_postgres.ts +277 -0
  25. package/src/bridge_redis.ts +194 -0
  26. package/src/bridge_sqlite.ts +289 -0
  27. package/src/bridges.ts +376 -0
  28. package/src/event_bus.ts +1263 -0
  29. package/src/event_handler.ts +379 -0
  30. package/src/event_history.ts +247 -0
  31. package/src/event_result.ts +483 -0
  32. package/src/events_suck.ts +96 -0
  33. package/src/helpers.ts +65 -0
  34. package/src/index.ts +37 -0
  35. package/src/lock_manager.ts +401 -0
  36. package/src/logging.ts +261 -0
  37. package/src/middleware_otel_tracing.ts +201 -0
  38. package/src/middlewares.ts +16 -0
  39. package/src/optional_deps.ts +52 -0
  40. package/src/retry.ts +578 -0
  41. package/src/timing.ts +52 -0
  42. package/src/types.ts +132 -0
@@ -0,0 +1,483 @@
1
+ import { v7 as uuidv7 } from 'uuid'
2
+
3
+ import { z } from 'zod'
4
+
5
+ import { BaseEvent } from './base_event.js'
6
+ import type { EventBus } from './event_bus.js'
7
+ import { EventHandler, EventHandlerCancelledError, EventHandlerResultSchemaError, EventHandlerTimeoutError } from './event_handler.js'
8
+ import { withResolvers, type HandlerLock } from './lock_manager.js'
9
+ import type { Deferred } from './lock_manager.js'
10
+ import type { EventHandlerCallable, EventResultType } from './types.js'
11
+ import { isZodSchema } from './types.js'
12
+ import { _runWithAsyncContext } from './async_context.js'
13
+ import { RetryTimeoutError } from './retry.js'
14
+ import { _runWithAbortMonitor, _runWithSlowMonitor, _runWithTimeout } from './timing.js'
15
+ import { monotonicDatetime } from './helpers.js'
16
+
17
+ // More precise than event.event_status, includes separate 'error' state for handlers that throw errors during execution
18
+ export type EventResultStatus = 'pending' | 'started' | 'completed' | 'error'
19
+
20
+ export const EventResultJSONSchema = z
21
+ .object({
22
+ id: z.string(),
23
+ status: z.enum(['pending', 'started', 'completed', 'error']),
24
+ event_id: z.string(),
25
+ handler_id: z.string(),
26
+ handler_name: z.string(),
27
+ handler_file_path: z.string().nullable().optional(),
28
+ handler_timeout: z.number().nullable().optional(),
29
+ handler_slow_timeout: z.number().nullable().optional(),
30
+ handler_registered_at: z.string().datetime().optional(),
31
+ handler_event_pattern: z.union([z.string(), z.literal('*')]).optional(),
32
+ eventbus_name: z.string(),
33
+ eventbus_id: z.string().uuid(),
34
+ started_at: z.string().datetime().nullable().optional(),
35
+ completed_at: z.string().datetime().nullable().optional(),
36
+ result: z.unknown().optional(),
37
+ error: z.unknown().optional(),
38
+ event_children: z.array(z.string()),
39
+ })
40
+ .strict()
41
+
42
+ export type EventResultJSON = z.infer<typeof EventResultJSONSchema>
43
+
44
+ // Object that tracks the pending or completed execution of a single event handler
45
+ export class EventResult<TEvent extends BaseEvent = BaseEvent> {
46
+ id: string // unique uuidv7 identifier for the event result
47
+ status: EventResultStatus // 'pending', 'started', 'completed', or 'error'
48
+ event: TEvent // the Event that the handler is processing
49
+ handler: EventHandler // the EventHandler object that going to process the event
50
+ started_at: string | null
51
+ completed_at: string | null
52
+ result?: EventResultType<TEvent> // parsed return value from the event handler
53
+ error?: unknown // error object thrown by the event handler, or null if the handler completed successfully
54
+ event_children: BaseEvent[] // list of emitted child events
55
+
56
+ // Abort signal: created when handler starts, rejected by _signalAbort() to
57
+ // interrupt runHandler's await via Promise.race.
58
+ _abort: Deferred<never> | null
59
+ // Handler lock: tracks ownership of the handler concurrency lock
60
+ // during handler execution. Set by runHandler(), used by
61
+ // _processEventImmediately for yield-and-reacquire during queue-jumps.
62
+ _lock: HandlerLock | null
63
+ // Runloop pause releases keyed by bus for queue-jump; released when handler exits.
64
+ _queue_jump_pause_releases: Map<EventBus, () => void> | null
65
+
66
+ constructor(params: { event: TEvent; handler: EventHandler }) {
67
+ this.id = uuidv7()
68
+ this.status = 'pending'
69
+ this.event = params.event
70
+ this.handler = params.handler
71
+ this.started_at = null
72
+ this.completed_at = null
73
+ this.result = undefined
74
+ this.error = undefined
75
+ this.event_children = []
76
+ this._abort = null
77
+ this._lock = null
78
+ this._queue_jump_pause_releases = null
79
+ }
80
+
81
+ toString(): string {
82
+ return `${this.result ?? 'null'} (${this.status})`
83
+ }
84
+
85
+ get event_id(): string {
86
+ return this.event.event_id
87
+ }
88
+
89
+ get bus(): EventBus {
90
+ return this.event.event_bus!
91
+ }
92
+
93
+ get handler_id(): string {
94
+ return this.handler.id
95
+ }
96
+
97
+ get handler_name(): string {
98
+ return this.handler.handler_name
99
+ }
100
+
101
+ get handler_file_path(): string | null {
102
+ return this.handler.handler_file_path
103
+ }
104
+
105
+ get eventbus_name(): string {
106
+ return this.handler.eventbus_name
107
+ }
108
+
109
+ get eventbus_id(): string {
110
+ return this.handler.eventbus_id
111
+ }
112
+
113
+ get eventbus_label(): string {
114
+ return `${this.handler.eventbus_name}#${this.handler.eventbus_id.slice(-4)}`
115
+ }
116
+
117
+ private getHookBus(): EventBus | undefined {
118
+ const root_bus = this.event.event_bus
119
+ if (!root_bus) {
120
+ return undefined
121
+ }
122
+ return root_bus.all_instances.findBusById(this.eventbus_id) ?? root_bus
123
+ }
124
+
125
+ private async _notifyStatusHook(status: 'started' | 'completed'): Promise<void> {
126
+ const hook_bus = this.getHookBus()
127
+ if (!hook_bus) {
128
+ return
129
+ }
130
+ const event_for_hook = hook_bus._getEventProxyScopedToThisBus(this.event._event_original ?? this.event, this)
131
+ await hook_bus.onEventResultChange(event_for_hook, this, status)
132
+ }
133
+
134
+ // shortcut for the result value so users can do event_result.value instead of event_result.result
135
+ get value(): EventResultType<TEvent> | undefined {
136
+ return this.result
137
+ }
138
+
139
+ // Per-result schema reference derives from the parent event schema.
140
+ // It is intentionally not serialized with each EventResult to avoid duplication.
141
+ get result_type(): TEvent['event_result_type'] {
142
+ const original_event = this.event._event_original ?? this.event
143
+ return original_event.event_result_type as TEvent['event_result_type']
144
+ }
145
+
146
+ // Link a child event emitted by this handler run to the parent event/result.
147
+ _linkEmittedChildEvent(child_event: BaseEvent): void {
148
+ const original_child = child_event._event_original ?? child_event
149
+ const parent_event = this.event._event_original ?? this.event
150
+ if (original_child.event_id === parent_event.event_id) {
151
+ return
152
+ }
153
+ if (!original_child.event_parent_id) {
154
+ original_child.event_parent_id = parent_event.event_id
155
+ }
156
+ if (!original_child.event_emitted_by_handler_id) {
157
+ original_child.event_emitted_by_handler_id = this.handler_id
158
+ }
159
+ if (!this.event_children.some((child) => child.event_id === original_child.event_id)) {
160
+ this.event_children.push(original_child)
161
+ }
162
+ }
163
+
164
+ // Get the raw return value from the handler, even if it threw an error / failed validation
165
+ get raw_value(): EventResultType<TEvent> | undefined {
166
+ if (this.error && (this.error as any).raw_value !== undefined) {
167
+ return (this.error as any).raw_value
168
+ }
169
+ return this.result
170
+ }
171
+
172
+ // Resolve handler timeout in seconds using precedence: handler -> event -> bus defaults.
173
+ get handler_timeout(): number | null {
174
+ const original = this.event._event_original ?? this.event
175
+ const resolved_event_timeout = original.event_timeout ?? this.bus.event_timeout
176
+
177
+ let resolved_handler_timeout: number | null
178
+ if (this.handler.handler_timeout !== undefined) {
179
+ resolved_handler_timeout = this.handler.handler_timeout
180
+ } else if (original.event_handler_timeout !== undefined) {
181
+ resolved_handler_timeout = original.event_handler_timeout
182
+ } else {
183
+ resolved_handler_timeout = this.bus.event_timeout
184
+ }
185
+
186
+ if (resolved_handler_timeout === null && resolved_event_timeout === null) {
187
+ return null
188
+ }
189
+ if (resolved_handler_timeout === null) {
190
+ return resolved_event_timeout
191
+ }
192
+ if (resolved_event_timeout === null) {
193
+ return resolved_handler_timeout
194
+ }
195
+ return Math.min(resolved_handler_timeout, resolved_event_timeout)
196
+ }
197
+
198
+ // Resolve slow handler warning threshold in seconds using precedence: handler -> event -> bus defaults.
199
+ get handler_slow_timeout(): number | null {
200
+ const original = this.event._event_original ?? this.event
201
+
202
+ if (this.handler.handler_slow_timeout !== undefined) {
203
+ return this.handler.handler_slow_timeout
204
+ }
205
+ if (original.event_handler_slow_timeout !== undefined) {
206
+ return original.event_handler_slow_timeout
207
+ }
208
+ const event_slow_timeout = (original as { event_slow_timeout?: number | null }).event_slow_timeout
209
+ if (event_slow_timeout !== undefined) {
210
+ return event_slow_timeout
211
+ }
212
+ if (this.bus?.event_handler_slow_timeout !== undefined) {
213
+ return this.bus.event_handler_slow_timeout
214
+ }
215
+ return this.bus?.event_slow_timeout ?? null
216
+ }
217
+
218
+ // Create a slow-handler warning timer that logs if the handler runs too long.
219
+ _createSlowHandlerWarningTimer(effective_timeout: number | null): ReturnType<typeof setTimeout> | null {
220
+ const handler_warn_timeout = this.handler_slow_timeout
221
+ const warn_ms = handler_warn_timeout === null ? null : handler_warn_timeout * 1000
222
+ const should_warn = warn_ms !== null && (effective_timeout === null || effective_timeout * 1000 > warn_ms)
223
+ if (!should_warn || warn_ms === null) {
224
+ return null
225
+ }
226
+ const event = this.event._event_original ?? this.event
227
+ const bus_name = this.handler.eventbus_name
228
+ const started_at_ms = performance.now()
229
+ return setTimeout(() => {
230
+ if (this.status !== 'started') {
231
+ return
232
+ }
233
+ const elapsed_ms = performance.now() - started_at_ms
234
+ const elapsed_seconds = (elapsed_ms / 1000).toFixed(1)
235
+ console.warn(
236
+ `[abxbus] Slow event handler: ${bus_name}.on(${event.toString()}, ${this.handler.toString()}) still running after ${elapsed_seconds}s`
237
+ )
238
+ }, warn_ms)
239
+ }
240
+
241
+ _ensureQueueJumpPause(bus: EventBus): void {
242
+ if (!this._queue_jump_pause_releases) {
243
+ this._queue_jump_pause_releases = new Map()
244
+ }
245
+ if (this._queue_jump_pause_releases.has(bus)) {
246
+ return
247
+ }
248
+ this._queue_jump_pause_releases.set(bus, bus.locks._requestRunloopPause())
249
+ }
250
+
251
+ _releaseQueueJumpPauses(): void {
252
+ if (!this._queue_jump_pause_releases) {
253
+ return
254
+ }
255
+ for (const release of this._queue_jump_pause_releases.values()) {
256
+ release()
257
+ }
258
+ this._queue_jump_pause_releases.clear()
259
+ }
260
+
261
+ update(params: { status?: EventResultStatus; result?: EventResultType<TEvent> | BaseEvent | undefined; error?: unknown }): this {
262
+ const has_status = 'status' in params
263
+ const has_result = 'result' in params
264
+ const has_error = 'error' in params
265
+
266
+ if (has_result) {
267
+ const raw_result = params.result
268
+ this.status = 'completed'
269
+ if (
270
+ this.event.event_result_type &&
271
+ raw_result !== undefined &&
272
+ !(raw_result instanceof BaseEvent) &&
273
+ isZodSchema(this.event.event_result_type)
274
+ ) {
275
+ const parsed = this.event.event_result_type.safeParse(raw_result)
276
+ if (parsed.success) {
277
+ this.result = parsed.data as EventResultType<TEvent>
278
+ } else {
279
+ const error = new EventHandlerResultSchemaError(
280
+ `Event handler return value ${JSON.stringify(raw_result).slice(0, 20)}... did not match event_result_type: ${parsed.error.message}`,
281
+ { event_result: this, cause: parsed.error, raw_value: raw_result }
282
+ )
283
+ this.error = error
284
+ this.result = undefined
285
+ this.status = 'error'
286
+ }
287
+ } else {
288
+ this.result = raw_result as EventResultType<TEvent> | undefined
289
+ }
290
+ }
291
+
292
+ if (has_error) {
293
+ this.error = params.error
294
+ this.status = 'error'
295
+ }
296
+
297
+ if (has_status && params.status !== undefined) {
298
+ this.status = params.status
299
+ }
300
+
301
+ if (this.status !== 'pending' && this.started_at === null) {
302
+ this.started_at = monotonicDatetime()
303
+ }
304
+ if ((this.status === 'completed' || this.status === 'error') && this.completed_at === null) {
305
+ this.completed_at = monotonicDatetime()
306
+ }
307
+
308
+ return this
309
+ }
310
+
311
+ private _createHandlerTimeoutError(event: BaseEvent): EventHandlerTimeoutError {
312
+ return new EventHandlerTimeoutError(
313
+ `${this.bus.toString()}.on(${event.toString()}, ${this.handler.toString()}) timed out after ${this.handler_timeout}s`,
314
+ {
315
+ event_result: this,
316
+ timeout_seconds: this.handler_timeout,
317
+ }
318
+ )
319
+ }
320
+
321
+ private _handleHandlerError(event: BaseEvent, error: unknown): void {
322
+ const normalized_error =
323
+ error instanceof RetryTimeoutError
324
+ ? new EventHandlerTimeoutError(error.message, { event_result: this, timeout_seconds: error.timeout_seconds, cause: error })
325
+ : error
326
+ if (normalized_error instanceof EventHandlerTimeoutError) {
327
+ this._markError(normalized_error, false)
328
+ event._cancelPendingChildProcessing(normalized_error)
329
+ } else {
330
+ this._markError(normalized_error, false)
331
+ }
332
+ }
333
+
334
+ private _onHandlerExit(slow_handler_warning_timer: ReturnType<typeof setTimeout> | null): void {
335
+ this._abort = null
336
+ this._lock = null
337
+ this._releaseQueueJumpPauses()
338
+ if (slow_handler_warning_timer) {
339
+ clearTimeout(slow_handler_warning_timer)
340
+ }
341
+ }
342
+
343
+ // Run one handler invocation with timeout/slow-monitor/error handling.
344
+ // Handler lock acquisition is owned by BaseEvent._runHandlers(...).
345
+ async runHandler(handler_lock: HandlerLock | null): Promise<void> {
346
+ if (this.status === 'error' && this.error instanceof EventHandlerCancelledError) {
347
+ return
348
+ }
349
+
350
+ const event = this.event._event_original ?? this.event
351
+ const handler_event = this.bus._getEventProxyScopedToThisBus(event, this)
352
+ if (this._lock) {
353
+ this._lock.exitHandlerRun()
354
+ }
355
+
356
+ let slow_handler_warning_timer: ReturnType<typeof setTimeout> | null = null
357
+ // if the result is already in an error or completed state, exit early
358
+ if (this.status === 'error' || this.status === 'completed') {
359
+ return
360
+ }
361
+
362
+ this._lock = handler_lock
363
+ await this.bus.locks._runWithHandlerDispatchContext(this, async () => {
364
+ await _runWithAsyncContext(event._getDispatchContext() ?? null, async () => {
365
+ try {
366
+ const should_notify_started = this.status === 'pending'
367
+ const abort_signal = this._markStarted(false)
368
+ if (should_notify_started) {
369
+ await this._notifyStatusHook('started')
370
+ }
371
+ slow_handler_warning_timer = this._createSlowHandlerWarningTimer(this.handler_timeout)
372
+ const handler_result = await _runWithTimeout(
373
+ this.handler_timeout,
374
+ () => this._createHandlerTimeoutError(event),
375
+ () =>
376
+ _runWithSlowMonitor(slow_handler_warning_timer, () =>
377
+ _runWithAbortMonitor(() => this.handler._handler_async(handler_event), abort_signal)
378
+ )
379
+ )
380
+ this._markCompleted(handler_result as EventResultType<TEvent> | BaseEvent | undefined, false)
381
+ } catch (error) {
382
+ this._handleHandlerError(event, error)
383
+ } finally {
384
+ if (this.status === 'completed' || this.status === 'error') {
385
+ await this._notifyStatusHook('completed')
386
+ }
387
+ this._onHandlerExit(slow_handler_warning_timer)
388
+ }
389
+ })
390
+ })
391
+ }
392
+
393
+ // Reject the abort promise, causing runHandler's Promise.race to
394
+ // throw immediately — even if the handler has no timeout.
395
+ _signalAbort(error: Error): void {
396
+ if (this._abort) {
397
+ this._abort.reject(error)
398
+ this._abort = null
399
+ }
400
+ }
401
+
402
+ // Mark started and return the abort promise for Promise.race.
403
+ _markStarted(notify_hook: boolean = true): Promise<never> {
404
+ if (!this._abort) {
405
+ this._abort = withResolvers<never>()
406
+ }
407
+ if (this.status === 'pending') {
408
+ this.update({ status: 'started' })
409
+ if (notify_hook) {
410
+ void this._notifyStatusHook('started')
411
+ }
412
+ }
413
+ return this._abort.promise
414
+ }
415
+
416
+ _markCompleted(result: EventResultType<TEvent> | BaseEvent | undefined, notify_hook: boolean = true): void {
417
+ if (this.status === 'completed' || this.status === 'error') return
418
+ this.update({ result })
419
+ if (notify_hook) {
420
+ void this._notifyStatusHook('completed')
421
+ }
422
+ }
423
+
424
+ _markError(error: unknown, notify_hook: boolean = true): void {
425
+ if (this.status === 'completed' || this.status === 'error') return
426
+ this.update({ error })
427
+ if (notify_hook) {
428
+ void this._notifyStatusHook('completed')
429
+ }
430
+ }
431
+
432
+ toJSON(): EventResultJSON {
433
+ return {
434
+ id: this.id,
435
+ status: this.status,
436
+ event_id: this.event.event_id,
437
+ handler_id: this.handler_id,
438
+ handler_name: this.handler_name,
439
+ handler_file_path: this.handler_file_path,
440
+ handler_timeout: this.handler.handler_timeout,
441
+ handler_slow_timeout: this.handler.handler_slow_timeout,
442
+ handler_registered_at: this.handler.handler_registered_at,
443
+ handler_event_pattern: this.handler.event_pattern,
444
+ eventbus_name: this.eventbus_name,
445
+ eventbus_id: this.eventbus_id,
446
+ started_at: this.started_at,
447
+ completed_at: this.completed_at,
448
+ result: this.result,
449
+ error: this.error,
450
+ event_children: this.event_children.map((child) => child.event_id),
451
+ }
452
+ }
453
+
454
+ static fromJSON<TEvent extends BaseEvent>(event: TEvent, data: unknown): EventResult<TEvent> {
455
+ const record = EventResultJSONSchema.parse(data)
456
+ const handler_record = {
457
+ id: record.handler_id,
458
+ eventbus_name: record.eventbus_name,
459
+ eventbus_id: record.eventbus_id,
460
+ event_pattern: record.handler_event_pattern ?? event.event_type,
461
+ handler_name: record.handler_name,
462
+ handler_file_path: record.handler_file_path ?? null,
463
+ handler_timeout: record.handler_timeout,
464
+ handler_slow_timeout: record.handler_slow_timeout,
465
+ handler_registered_at: record.handler_registered_at ?? event.event_created_at,
466
+ } as const
467
+ const handler_stub = EventHandler.fromJSON(handler_record, (() => undefined) as EventHandlerCallable)
468
+
469
+ const result = new EventResult<TEvent>({ event, handler: handler_stub })
470
+ result.id = record.id
471
+ result.status = record.status
472
+ result.started_at = record.started_at === null || record.started_at === undefined ? null : monotonicDatetime(record.started_at)
473
+ result.completed_at = record.completed_at === null || record.completed_at === undefined ? null : monotonicDatetime(record.completed_at)
474
+ if ('result' in record) {
475
+ result.result = record.result as EventResultType<TEvent>
476
+ }
477
+ if ('error' in record) {
478
+ result.error = record.error
479
+ }
480
+ result.event_children = []
481
+ return result
482
+ }
483
+ }
@@ -0,0 +1,96 @@
1
+ import { EventBus } from './event_bus.js'
2
+ import { BaseEvent } from './base_event.js'
3
+
4
+ import type { EventClass, EventResultType } from './types.js'
5
+
6
+ type EventMap = Record<string, EventClass<BaseEvent>>
7
+ type AnyFn = (...args: any[]) => any
8
+ type FunctionMap = Record<string, AnyFn>
9
+ type ExtraDict = Record<string, unknown>
10
+
11
+ type EventFieldsFromFn<TFunc extends AnyFn> =
12
+ Parameters<TFunc> extends [infer TArg] ? (TArg extends Record<string, unknown> ? TArg : ExtraDict) : ExtraDict
13
+
14
+ type GeneratedEvent<TFunc extends AnyFn> = {
15
+ (
16
+ data: EventFieldsFromFn<TFunc> & ExtraDict
17
+ ): BaseEvent & EventFieldsFromFn<TFunc> & { __event_result_type__?: Awaited<ReturnType<TFunc>> }
18
+ new (
19
+ data: EventFieldsFromFn<TFunc> & ExtraDict
20
+ ): BaseEvent & EventFieldsFromFn<TFunc> & { __event_result_type__?: Awaited<ReturnType<TFunc>> }
21
+ event_type?: string
22
+ }
23
+
24
+ export type GeneratedEvents<TEvents extends FunctionMap> = {
25
+ by_name: { [K in keyof TEvents]: GeneratedEvent<TEvents[K]> }
26
+ } & {
27
+ [K in keyof TEvents]: GeneratedEvent<TEvents[K]>
28
+ }
29
+
30
+ type EventInit<TEventClass extends EventClass<BaseEvent>> =
31
+ ConstructorParameters<TEventClass> extends [infer TInit, ...unknown[]] ? TInit : never
32
+
33
+ type EventMethodArgs<TEventClass extends EventClass<BaseEvent>> =
34
+ {} extends EventInit<TEventClass>
35
+ ? [init?: EventInit<TEventClass>, extra?: Record<string, unknown>]
36
+ : [init: EventInit<TEventClass>, extra?: Record<string, unknown>]
37
+
38
+ type EventMethodResult<TEventClass extends EventClass<BaseEvent>> = EventResultType<InstanceType<TEventClass>> | undefined
39
+
40
+ export type EventsSuckClient<TEvents extends EventMap> = {
41
+ bus: EventBus
42
+ } & {
43
+ [K in keyof TEvents]: (...args: EventMethodArgs<TEvents[K]>) => Promise<EventMethodResult<TEvents[K]>>
44
+ }
45
+
46
+ export type EventsSuckClientClass<TEvents extends EventMap> = new (bus?: EventBus) => EventsSuckClient<TEvents>
47
+
48
+ type DynamicWrappedClient = {
49
+ bus: EventBus
50
+ } & Record<string, (...args: unknown[]) => Promise<unknown>>
51
+
52
+ export const make_events = <TEvents extends FunctionMap>(events: TEvents): GeneratedEvents<TEvents> => {
53
+ const by_name = {} as { [K in keyof TEvents]: GeneratedEvent<TEvents[K]> }
54
+ for (const [event_name] of Object.entries(events) as Array<[keyof TEvents, TEvents[keyof TEvents]]>) {
55
+ if (!/^[A-Za-z_$][\w$]*$/.test(String(event_name))) {
56
+ throw new Error(`Invalid event name: ${String(event_name)}`)
57
+ }
58
+ by_name[event_name] = BaseEvent.extend(String(event_name), {}) as unknown as GeneratedEvent<TEvents[keyof TEvents]>
59
+ }
60
+ return Object.assign({ by_name }, by_name) as GeneratedEvents<TEvents>
61
+ }
62
+
63
+ export const wrap = <TEvents extends EventMap>(class_name: string, methods: TEvents): EventsSuckClientClass<TEvents> => {
64
+ class WrappedClient {
65
+ bus: EventBus
66
+
67
+ constructor(bus?: EventBus) {
68
+ this.bus = bus ?? new EventBus(`${class_name}Bus`)
69
+ }
70
+ }
71
+
72
+ Object.defineProperty(WrappedClient, 'name', { value: class_name })
73
+
74
+ for (const [method_name, EventCtor] of Object.entries(methods)) {
75
+ Object.defineProperty(WrappedClient.prototype, method_name, {
76
+ value: async function (this: DynamicWrappedClient, init?: Record<string, unknown>, extra?: Record<string, unknown>) {
77
+ const payload = { ...(init ?? {}), ...(extra ?? {}) }
78
+ return await this.bus.emit(new EventCtor(payload)).first()
79
+ },
80
+ writable: true,
81
+ configurable: true,
82
+ })
83
+ }
84
+
85
+ return WrappedClient as unknown as EventsSuckClientClass<TEvents>
86
+ }
87
+
88
+ // Intentionally no make_event()/make_handler() helpers in TypeScript.
89
+ // Prefer the explicit inline pattern:
90
+ // const FooCreateEvent = BaseEvent.extend('FooCreateEvent', {
91
+ // id: z.string().nullable().optional(),
92
+ // name: z.string(),
93
+ // age: z.number(),
94
+ // })
95
+ // bus.on(FooCreateEvent, ({ id, name, age, ...extra }) => impl.create(id, { name, age }))
96
+ export const events_suck = { make_events, wrap } as const
package/src/helpers.ts ADDED
@@ -0,0 +1,65 @@
1
+ const MONOTONIC_DATETIME_REGEX = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,9}))?(Z|[+-]\d{2}:\d{2})$/
2
+ const MONOTONIC_DATETIME_LENGTH = 30 // YYYY-MM-DDTHH:MM:SS.fffffffffZ
3
+ const NS_PER_MS = 1_000_000n
4
+ const NS_PER_SECOND = 1_000_000_000n
5
+
6
+ const has_performance_now = typeof performance !== 'undefined' && typeof performance.now === 'function'
7
+ const monotonic_clock_anchor_ms = has_performance_now ? performance.now() : 0
8
+ const monotonic_epoch_anchor_ns = BigInt(Date.now()) * NS_PER_MS
9
+ let last_monotonic_datetime_ns = monotonic_epoch_anchor_ns
10
+
11
+ function assertYearRange(date: Date, context: string): void {
12
+ const year = date.getUTCFullYear()
13
+ if (year <= 1990 || year >= 2500) {
14
+ throw new Error(`${context} year must be >1990 and <2500, got ${year}`)
15
+ }
16
+ }
17
+
18
+ function formatEpochNs(epoch_ns: bigint): string {
19
+ const epoch_ms = Number(epoch_ns / NS_PER_MS)
20
+ const date = new Date(epoch_ms)
21
+ if (Number.isNaN(date.getTime())) {
22
+ throw new Error(`Failed to format datetime from epoch ns: ${epoch_ns.toString()}`)
23
+ }
24
+ assertYearRange(date, 'monotonicDatetime()')
25
+ const base = date.toISOString().slice(0, 19)
26
+ const fraction = (epoch_ns % NS_PER_SECOND).toString().padStart(9, '0')
27
+ const normalized = `${base}.${fraction}Z`
28
+ if (normalized.length !== MONOTONIC_DATETIME_LENGTH) {
29
+ throw new Error(`Expected canonical datetime length ${MONOTONIC_DATETIME_LENGTH}, got ${normalized.length}: ${normalized}`)
30
+ }
31
+ return normalized
32
+ }
33
+
34
+ export function monotonicDatetime(isostring?: string): string {
35
+ if (isostring !== undefined) {
36
+ if (typeof isostring !== 'string') {
37
+ throw new Error(`monotonicDatetime(isostring?) requires string | undefined, got ${typeof isostring}`)
38
+ }
39
+ const match = MONOTONIC_DATETIME_REGEX.exec(isostring)
40
+ if (!match) {
41
+ throw new Error(`Invalid ISO datetime: ${isostring}`)
42
+ }
43
+ const parsed = new Date(isostring)
44
+ if (Number.isNaN(parsed.getTime())) {
45
+ throw new Error(`Invalid ISO datetime: ${isostring}`)
46
+ }
47
+ assertYearRange(parsed, 'monotonicDatetime(isostring)')
48
+ const base = parsed.toISOString().slice(0, 19)
49
+ const fraction = (match[7] ?? '').padEnd(9, '0')
50
+ const normalized = `${base}.${fraction}Z`
51
+ if (normalized.length !== MONOTONIC_DATETIME_LENGTH) {
52
+ throw new Error(`Expected canonical datetime length ${MONOTONIC_DATETIME_LENGTH}, got ${normalized.length}: ${normalized}`)
53
+ }
54
+ return normalized
55
+ }
56
+
57
+ const elapsed_ms = has_performance_now ? performance.now() - monotonic_clock_anchor_ms : 0
58
+ const elapsed_ns = BigInt(Math.max(0, Math.floor(elapsed_ms * 1_000_000)))
59
+ let epoch_ns = monotonic_epoch_anchor_ns + elapsed_ns
60
+ if (epoch_ns <= last_monotonic_datetime_ns) {
61
+ epoch_ns = last_monotonic_datetime_ns + 1n
62
+ }
63
+ last_monotonic_datetime_ns = epoch_ns
64
+ return formatEpochNs(epoch_ns)
65
+ }
package/src/index.ts ADDED
@@ -0,0 +1,37 @@
1
+ export { BaseEvent, BaseEventSchema } from './base_event.js'
2
+ export { EventHistory } from './event_history.js'
3
+ export type { EventHistoryFindOptions, EventHistoryTrimOptions } from './event_history.js'
4
+ export { EventResult } from './event_result.js'
5
+ export { EventBus } from './event_bus.js'
6
+ export type { EventBusJSON, EventBusOptions } from './event_bus.js'
7
+ export { monotonicDatetime } from './helpers.js'
8
+ export type { EventBusMiddleware, EventBusMiddlewareCtor, EventBusMiddlewareInput } from './middlewares.js'
9
+ export { OtelTracingMiddleware } from './middleware_otel_tracing.js'
10
+ export type { OtelTracingMiddlewareOptions } from './middleware_otel_tracing.js'
11
+ export {
12
+ EventHandlerTimeoutError,
13
+ EventHandlerCancelledError,
14
+ EventHandlerAbortedError,
15
+ EventHandlerResultSchemaError,
16
+ } from './event_handler.js'
17
+ export type {
18
+ EventConcurrencyMode,
19
+ EventHandlerConcurrencyMode,
20
+ EventHandlerCompletionMode,
21
+ EventBusInterfaceForLockManager,
22
+ } from './lock_manager.js'
23
+ export type { EventClass, EventHandlerCallable as EventHandler, EventPattern, EventStatus, FindOptions, FindWindow } from './types.js'
24
+ export { retry, clearSemaphoreRegistry, RetryTimeoutError, SemaphoreTimeoutError } from './retry.js'
25
+ export type { RetryOptions } from './retry.js'
26
+ export {
27
+ HTTPEventBridge,
28
+ SocketEventBridge,
29
+ NATSEventBridge,
30
+ RedisEventBridge,
31
+ PostgresEventBridge,
32
+ JSONLEventBridge,
33
+ SQLiteEventBridge,
34
+ } from './bridges.js'
35
+ export type { HTTPEventBridgeOptions } from './bridges.js'
36
+ export { events_suck } from './events_suck.js'
37
+ export type { EventsSuckClient, EventsSuckClientClass, GeneratedEvents } from './events_suck.js'