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.
- package/dist/cjs/event_bus.js +1 -1
- package/dist/cjs/event_bus.js.map +2 -2
- package/dist/cjs/event_handler.d.ts +1 -0
- package/dist/cjs/event_handler.js +14 -1
- package/dist/cjs/event_handler.js.map +2 -2
- package/dist/cjs/middleware_otel_tracing.js +3 -3
- package/dist/cjs/middleware_otel_tracing.js.map +2 -2
- package/dist/cjs/retry.js +39 -0
- package/dist/cjs/retry.js.map +2 -2
- package/dist/esm/event_bus.js +1 -1
- package/dist/esm/event_bus.js.map +2 -2
- package/dist/esm/event_handler.js +14 -1
- package/dist/esm/event_handler.js.map +2 -2
- package/dist/esm/middleware_otel_tracing.js +4 -4
- package/dist/esm/middleware_otel_tracing.js.map +2 -2
- package/dist/esm/retry.js +39 -0
- package/dist/esm/retry.js.map +2 -2
- package/dist/types/event_handler.d.ts +1 -0
- package/package.json +5 -1
- package/src/async_context.ts +70 -0
- package/src/base_event.ts +1201 -0
- package/src/bridge_jsonl.ts +174 -0
- package/src/bridge_nats.ts +104 -0
- package/src/bridge_postgres.ts +277 -0
- package/src/bridge_redis.ts +194 -0
- package/src/bridge_sqlite.ts +289 -0
- package/src/bridges.ts +376 -0
- package/src/event_bus.ts +1263 -0
- package/src/event_handler.ts +379 -0
- package/src/event_history.ts +247 -0
- package/src/event_result.ts +483 -0
- package/src/events_suck.ts +96 -0
- package/src/helpers.ts +65 -0
- package/src/index.ts +37 -0
- package/src/lock_manager.ts +401 -0
- package/src/logging.ts +261 -0
- package/src/middleware_otel_tracing.ts +201 -0
- package/src/middlewares.ts +16 -0
- package/src/optional_deps.ts +52 -0
- package/src/retry.ts +578 -0
- package/src/timing.ts +52 -0
- 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'
|