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,379 @@
1
+ import { z } from 'zod'
2
+ import { v5 as uuidv5 } from 'uuid'
3
+
4
+ import { normalizeEventPattern, type EventHandlerCallable, type EventPattern } from './types.js'
5
+ import { BaseEvent } from './base_event.js'
6
+ import type { EventResult } from './event_result.js'
7
+ import { monotonicDatetime } from './helpers.js'
8
+
9
+ const HANDLER_ID_NAMESPACE = uuidv5('abxbus-handler', uuidv5.DNS)
10
+ const BOUND_FUNCTION_PREFIX = 'bound '
11
+
12
+ const normalizeCallableName = (name: string | undefined): string => {
13
+ if (!name) {
14
+ return 'anonymous'
15
+ }
16
+ if (name.startsWith(BOUND_FUNCTION_PREFIX) && name.length > BOUND_FUNCTION_PREFIX.length) {
17
+ return name.slice(BOUND_FUNCTION_PREFIX.length)
18
+ }
19
+ return name
20
+ }
21
+
22
+ export type EphemeralFindEventHandler = {
23
+ // Similar to a handler, except it's for .find() calls.
24
+ // Resolved on dispatch, ephemeral, and never shows up in the processing tree.
25
+ event_pattern: string | '*'
26
+ matches: (event: BaseEvent) => boolean
27
+ resolve: (event: BaseEvent) => void
28
+ timeout_id?: ReturnType<typeof setTimeout>
29
+ }
30
+
31
+ export const FindWaiterJSONSchema = z
32
+ .object({
33
+ event_pattern: z.union([z.string(), z.literal('*')]),
34
+ has_timeout: z.boolean(),
35
+ })
36
+ .strict()
37
+
38
+ export type FindWaiterJSON = z.infer<typeof FindWaiterJSONSchema>
39
+
40
+ export class FindWaiter {
41
+ static toJSON(waiter: EphemeralFindEventHandler): FindWaiterJSON {
42
+ return {
43
+ event_pattern: waiter.event_pattern,
44
+ has_timeout: waiter.timeout_id !== undefined,
45
+ }
46
+ }
47
+
48
+ static fromJSON(
49
+ data: unknown,
50
+ overrides: {
51
+ matches?: (event: BaseEvent) => boolean
52
+ resolve?: (event: BaseEvent) => void
53
+ } = {}
54
+ ): EphemeralFindEventHandler {
55
+ const record = FindWaiterJSONSchema.parse(data)
56
+ const event_pattern = record.event_pattern
57
+ const defaultMatches = (event: BaseEvent): boolean => event_pattern === '*' || event.event_type === event_pattern
58
+ return {
59
+ event_pattern,
60
+ matches: overrides.matches ?? defaultMatches,
61
+ resolve: overrides.resolve ?? (() => {}),
62
+ }
63
+ }
64
+
65
+ static toJSONArray(waiters: Iterable<EphemeralFindEventHandler>): FindWaiterJSON[] {
66
+ return Array.from(waiters, (waiter) => FindWaiter.toJSON(waiter))
67
+ }
68
+
69
+ static fromJSONArray(
70
+ data: unknown,
71
+ overrides: {
72
+ matches?: (event: BaseEvent) => boolean
73
+ resolve?: (event: BaseEvent) => void
74
+ } = {}
75
+ ): EphemeralFindEventHandler[] {
76
+ if (!Array.isArray(data)) {
77
+ return []
78
+ }
79
+ return data.map((item) => FindWaiter.fromJSON(item, overrides))
80
+ }
81
+ }
82
+
83
+ export const EventHandlerJSONSchema = z
84
+ .object({
85
+ id: z.string(),
86
+ eventbus_name: z.string(),
87
+ eventbus_id: z.string().uuid(),
88
+ event_pattern: z.union([z.string(), z.literal('*')]),
89
+ handler_name: z.string(),
90
+ handler_file_path: z.string().nullable().optional(),
91
+ handler_timeout: z.number().nullable().optional(),
92
+ handler_slow_timeout: z.number().nullable().optional(),
93
+ handler_registered_at: z.string().datetime(),
94
+ })
95
+ .strict()
96
+
97
+ export type EventHandlerJSON = z.infer<typeof EventHandlerJSONSchema>
98
+
99
+ // an entry in the list of event handlers that are registered on a bus
100
+ export class EventHandler {
101
+ id: string // unique uuidv5 based on hash of bus name, handler name, handler file path:lineno, registered at timestamp, and event key
102
+ handler: EventHandlerCallable // original callable passed to on()
103
+ handler_name: string // name of the handler function, or 'anonymous' if the handler is an anonymous/arrow function
104
+ handler_file_path: string | null // ~/path/to/source/file.ts:123, or null when unknown
105
+ handler_timeout?: number | null // maximum time in seconds that the handler is allowed to run before it is aborted, resolved at runtime if not set
106
+ handler_slow_timeout?: number | null // warning threshold in seconds for slow handler execution
107
+ handler_registered_at: string // ISO datetime used in the deterministic handler-id seed
108
+ event_pattern: string | '*' // event_type string to match against, or '*' to match all events
109
+ eventbus_name: string // name of the event bus that the handler is registered on
110
+ eventbus_id: string // uuidv7 identifier of the event bus that the handler is registered on
111
+
112
+ constructor(params: {
113
+ id?: string
114
+ handler: EventHandlerCallable
115
+ handler_name: string
116
+ handler_file_path?: string | null
117
+ handler_timeout?: number | null
118
+ handler_slow_timeout?: number | null
119
+ handler_registered_at: string
120
+ event_pattern: string | '*'
121
+ eventbus_name: string
122
+ eventbus_id: string
123
+ }) {
124
+ const handler_registered_at = monotonicDatetime(params.handler_registered_at)
125
+ this.id =
126
+ params.id ??
127
+ EventHandler.computeHandlerId({
128
+ eventbus_id: params.eventbus_id,
129
+ handler_name: params.handler_name,
130
+ handler_file_path: params.handler_file_path,
131
+ handler_registered_at,
132
+ event_pattern: params.event_pattern,
133
+ })
134
+ this.handler = params.handler
135
+ this.handler_name = params.handler_name
136
+ this.handler_file_path = params.handler_file_path ?? null
137
+ this.handler_timeout = params.handler_timeout
138
+ this.handler_slow_timeout = params.handler_slow_timeout
139
+ this.handler_registered_at = handler_registered_at
140
+ this.event_pattern = params.event_pattern
141
+ this.eventbus_name = params.eventbus_name
142
+ this.eventbus_id = params.eventbus_id
143
+ }
144
+
145
+ get _handler_async(): EventHandlerCallable {
146
+ const handler = this.handler
147
+ if (Object.prototype.toString.call(handler) === '[object AsyncFunction]') {
148
+ return handler
149
+ }
150
+ return async (event: BaseEvent) => await handler(event)
151
+ }
152
+
153
+ static handlerNameFromCallable(handler: EventHandlerCallable): string {
154
+ return normalizeCallableName(handler.name)
155
+ }
156
+
157
+ // compute globally unique handler uuid as a hash of the bus name, handler name, handler file path, registered at timestamp, and event key
158
+ static computeHandlerId(params: {
159
+ eventbus_id: string
160
+ handler_name: string
161
+ handler_file_path?: string | null
162
+ handler_registered_at: string
163
+ event_pattern: string | '*'
164
+ }): string {
165
+ const file_path = params.handler_file_path ?? 'unknown'
166
+ const seed = `${params.eventbus_id}|${params.handler_name}|${file_path}|${params.handler_registered_at}|${params.event_pattern}`
167
+ return uuidv5(seed, HANDLER_ID_NAMESPACE)
168
+ }
169
+
170
+ static fromCallable<TEvent extends BaseEvent = BaseEvent>(params: {
171
+ handler: EventHandlerCallable<TEvent>
172
+ event_pattern: EventPattern | '*'
173
+ eventbus_name: string
174
+ eventbus_id: string
175
+ detect_handler_file_path?: boolean
176
+ id?: string
177
+ handler_file_path?: string | null
178
+ handler_timeout?: number | null
179
+ handler_slow_timeout?: number | null
180
+ handler_registered_at?: string
181
+ }): EventHandler {
182
+ const entry = new EventHandler({
183
+ id: params.id,
184
+ handler: params.handler as EventHandlerCallable,
185
+ handler_name: EventHandler.handlerNameFromCallable(params.handler as EventHandlerCallable),
186
+ handler_file_path: params.handler_file_path ?? null,
187
+ handler_timeout: params.handler_timeout,
188
+ handler_slow_timeout: params.handler_slow_timeout,
189
+ handler_registered_at: monotonicDatetime(params.handler_registered_at),
190
+ event_pattern: normalizeEventPattern(params.event_pattern),
191
+ eventbus_name: params.eventbus_name,
192
+ eventbus_id: params.eventbus_id,
193
+ })
194
+ const should_detect_handler_file_path = params.detect_handler_file_path ?? true
195
+ if (should_detect_handler_file_path && entry.handler_file_path === null) {
196
+ entry._detectHandlerFilePath()
197
+ if (params.id === undefined) {
198
+ entry.id = EventHandler.computeHandlerId({
199
+ eventbus_id: entry.eventbus_id,
200
+ handler_name: entry.handler_name,
201
+ handler_file_path: entry.handler_file_path,
202
+ handler_registered_at: entry.handler_registered_at,
203
+ event_pattern: entry.event_pattern,
204
+ })
205
+ }
206
+ }
207
+ return entry
208
+ }
209
+
210
+ // "someHandlerName() @ ~/path/to/source/file.ts:123" <- best case when file path is available and its a named function
211
+ // "function#1234()" <- worst case when no file path is available and its an anonymous/arrow function defined inline
212
+ toString(): string {
213
+ const label = this.handler_name && this.handler_name !== 'anonymous' ? `${this.handler_name}()` : `function#${this.id.slice(-4)}()`
214
+ return this.handler_file_path ? `${label} @ ${this.handler_file_path}` : label
215
+ }
216
+
217
+ // autodetect the path/to/source/file.ts:lineno where the handler is defined for better logs
218
+ // optional (controlled by EventBus.event_handler_detect_file_paths) because it can slow down performance to introspect stack traces and find file paths
219
+ _detectHandlerFilePath(): void {
220
+ const line = new Error().stack
221
+ ?.split('\n')
222
+ .map((l) => l.trim())
223
+ .filter(Boolean)[4]
224
+ if (!line) return
225
+ const resolved_path =
226
+ line.trim().match(/\(([^)]+)\)$/)?.[1] ??
227
+ line.trim().match(/^\s*at\s+(.+)$/)?.[1] ??
228
+ line.trim().match(/^[^@]+@(.+)$/)?.[1] ??
229
+ line.trim()
230
+ const match = resolved_path.match(/^(.*?):(\d+)(?::\d+)?$/)
231
+ let normalized = match ? match[1] : resolved_path
232
+ const line_number = match?.[2]
233
+ if (normalized.startsWith('file://')) {
234
+ let path = normalized.slice('file://'.length)
235
+ if (path.startsWith('localhost/')) path = path.slice('localhost'.length)
236
+ if (!path.startsWith('/')) path = `/${path}`
237
+ try {
238
+ normalized = decodeURIComponent(path)
239
+ } catch {
240
+ normalized = path
241
+ }
242
+ }
243
+ normalized = normalized.replace(/\/users\/[^/]+\//i, '~/').replace(/\/home\/[^/]+\//i, '~/')
244
+ this.handler_file_path = line_number ? `${normalized}:${line_number}` : normalized
245
+ }
246
+
247
+ toJSON(): EventHandlerJSON {
248
+ return {
249
+ id: this.id,
250
+ eventbus_name: this.eventbus_name,
251
+ eventbus_id: this.eventbus_id,
252
+ event_pattern: this.event_pattern,
253
+ handler_name: this.handler_name,
254
+ handler_file_path: this.handler_file_path,
255
+ handler_timeout: this.handler_timeout,
256
+ handler_slow_timeout: this.handler_slow_timeout,
257
+ handler_registered_at: this.handler_registered_at,
258
+ }
259
+ }
260
+
261
+ static fromJSON(data: unknown, handler?: EventHandlerCallable): EventHandler {
262
+ const record = EventHandlerJSONSchema.parse(data)
263
+ const handler_fn = handler ?? ((() => undefined) as EventHandlerCallable)
264
+ const handler_name = record.handler_name || handler_fn.name || 'anonymous' // 'anonymous' is the default name for anonymous/arrow functions
265
+ return new EventHandler({
266
+ id: record.id,
267
+ handler: handler_fn,
268
+ handler_name,
269
+ handler_file_path: record.handler_file_path ?? null,
270
+ handler_timeout: record.handler_timeout,
271
+ handler_slow_timeout: record.handler_slow_timeout,
272
+ handler_registered_at: record.handler_registered_at,
273
+ event_pattern: record.event_pattern,
274
+ eventbus_name: record.eventbus_name,
275
+ eventbus_id: record.eventbus_id,
276
+ })
277
+ }
278
+
279
+ static toJSONArray(handlers: Iterable<EventHandler>): EventHandlerJSON[] {
280
+ return Array.from(handlers, (handler) => handler.toJSON())
281
+ }
282
+
283
+ static fromJSONArray(data: unknown, handler?: EventHandlerCallable): EventHandler[] {
284
+ if (!Array.isArray(data)) {
285
+ return []
286
+ }
287
+ return data.map((item) => EventHandler.fromJSON(item, handler))
288
+ }
289
+
290
+ get eventbus_label(): string {
291
+ return `${this.eventbus_name}#${this.eventbus_id.slice(-4)}`
292
+ }
293
+ }
294
+
295
+ // Generic base TimeoutError used for EventHandlerTimeoutError.cause default value if
296
+ export class TimeoutError extends Error {
297
+ constructor(message: string) {
298
+ super(message)
299
+ this.name = 'TimeoutError'
300
+ }
301
+ }
302
+
303
+ // Base class for all errors that can occur while running an event handler
304
+ export class EventHandlerError extends Error {
305
+ event_result: EventResult
306
+ timeout_seconds: number | null
307
+ cause: Error
308
+
309
+ constructor(message: string, params: { event_result: EventResult; timeout_seconds?: number | null; cause: Error }) {
310
+ super(message)
311
+ this.name = 'EventHandlerError'
312
+ this.event_result = params.event_result
313
+ this.cause = params.cause
314
+ this.timeout_seconds = params.timeout_seconds ?? this.event_result.event.event_timeout ?? null
315
+ }
316
+
317
+ get event(): BaseEvent {
318
+ return this.event_result.event
319
+ }
320
+
321
+ get event_type(): string {
322
+ return this.event.event_type
323
+ }
324
+
325
+ get handler_name(): string {
326
+ return this.event_result.handler_name
327
+ }
328
+
329
+ get handler_id(): string {
330
+ return this.event_result.handler_id
331
+ }
332
+
333
+ get event_timeout(): number | null {
334
+ return this.event.event_timeout
335
+ }
336
+ }
337
+
338
+ // When the handler itself timed out while executing (due to handler.handler_timeout being exceeded)
339
+ export class EventHandlerTimeoutError extends EventHandlerError {
340
+ constructor(message: string, params: { event_result: EventResult; timeout_seconds?: number | null; cause?: Error }) {
341
+ super(message, {
342
+ event_result: params.event_result,
343
+ timeout_seconds: params.timeout_seconds,
344
+ cause: params.cause ?? new TimeoutError(message),
345
+ })
346
+ this.name = 'EventHandlerTimeoutError'
347
+ }
348
+ }
349
+
350
+ // When a pending handler was cancelled and never run due to an error (e.g. timeout) in a parent scope
351
+ export class EventHandlerCancelledError extends EventHandlerError {
352
+ constructor(message: string, params: { event_result: EventResult; timeout_seconds?: number | null; cause: Error }) {
353
+ super(message, params)
354
+ this.name = 'EventHandlerCancelledError'
355
+ }
356
+ }
357
+
358
+ // When a handler that was already running was aborted due to an error in the parent scope, not due to an error in its own logic / exceeding its own timeout
359
+ export class EventHandlerAbortedError extends EventHandlerError {
360
+ constructor(message: string, params: { event_result: EventResult; timeout_seconds?: number | null; cause: Error }) {
361
+ super(message, params)
362
+ this.name = 'EventHandlerAbortedError'
363
+ }
364
+ }
365
+
366
+ // When a handler run successfully but returned a value that failed event_result_type validation
367
+ export class EventHandlerResultSchemaError extends EventHandlerError {
368
+ raw_value: unknown
369
+
370
+ constructor(message: string, params: { event_result: EventResult; timeout_seconds?: number | null; cause: Error; raw_value: unknown }) {
371
+ super(message, params)
372
+ this.name = 'EventHandlerResultSchemaError'
373
+ this.raw_value = params.raw_value
374
+ }
375
+
376
+ get expected_schema(): any {
377
+ return this.event_result.event.event_result_type
378
+ }
379
+ }
@@ -0,0 +1,247 @@
1
+ import { BaseEvent } from './base_event.js'
2
+ import type { EventPattern, FindWindow } from './types.js'
3
+ import { normalizeEventPattern } from './types.js'
4
+ import { monotonicDatetime } from './helpers.js'
5
+
6
+ export type EventHistoryFindOptions = {
7
+ past?: FindWindow
8
+ future?: FindWindow
9
+ child_of?: BaseEvent | null
10
+ event_is_child_of?: (event: BaseEvent, ancestor: BaseEvent) => boolean
11
+ wait_for_future_match?: (
12
+ event_pattern: string | '*',
13
+ matches: (event: BaseEvent) => boolean,
14
+ future: FindWindow
15
+ ) => Promise<BaseEvent | null>
16
+ } & Record<string, unknown>
17
+
18
+ export type EventHistoryTrimOptions<TEvent extends BaseEvent = BaseEvent> = {
19
+ is_event_complete?: (event: TEvent) => boolean
20
+ on_remove?: (event: TEvent) => void
21
+ owner_label?: string
22
+ max_history_size?: number | null
23
+ max_history_drop?: boolean
24
+ }
25
+
26
+ export class EventHistory<TEvent extends BaseEvent = BaseEvent> implements Iterable<[string, TEvent]> {
27
+ max_history_size: number | null
28
+ max_history_drop: boolean
29
+
30
+ private _events: Map<string, TEvent>
31
+ private _warned_about_dropping_uncompleted_events: boolean
32
+
33
+ constructor(options: { max_history_size?: number | null; max_history_drop?: boolean } = {}) {
34
+ this.max_history_size = options.max_history_size === undefined ? 100 : options.max_history_size
35
+ this.max_history_drop = options.max_history_drop ?? false
36
+ this._events = new Map()
37
+ this._warned_about_dropping_uncompleted_events = false
38
+ }
39
+
40
+ get size(): number {
41
+ return this._events.size
42
+ }
43
+
44
+ [Symbol.iterator](): Iterator<[string, TEvent]> {
45
+ return this._events[Symbol.iterator]()
46
+ }
47
+
48
+ entries(): IterableIterator<[string, TEvent]> {
49
+ return this._events.entries()
50
+ }
51
+
52
+ keys(): IterableIterator<string> {
53
+ return this._events.keys()
54
+ }
55
+
56
+ values(): IterableIterator<TEvent> {
57
+ return this._events.values()
58
+ }
59
+
60
+ clear(): void {
61
+ this._events.clear()
62
+ }
63
+
64
+ get(event_id: string): TEvent | undefined {
65
+ return this._events.get(event_id)
66
+ }
67
+
68
+ set(event_id: string, event: TEvent): this {
69
+ this._events.set(event_id, event)
70
+ return this
71
+ }
72
+
73
+ has(event_id: string): boolean {
74
+ return this._events.has(event_id)
75
+ }
76
+
77
+ delete(event_id: string): boolean {
78
+ return this._events.delete(event_id)
79
+ }
80
+
81
+ addEvent(event: TEvent): void {
82
+ this._events.set(event.event_id, event)
83
+ }
84
+
85
+ getEvent(event_id: string): TEvent | undefined {
86
+ return this._events.get(event_id)
87
+ }
88
+
89
+ removeEvent(event_id: string): boolean {
90
+ return this._events.delete(event_id)
91
+ }
92
+
93
+ hasEvent(event_id: string): boolean {
94
+ return this._events.has(event_id)
95
+ }
96
+
97
+ static normalizeEventPattern(event_pattern: EventPattern | '*'): string | '*' {
98
+ return normalizeEventPattern(event_pattern)
99
+ }
100
+
101
+ find(event_pattern: '*', where?: (event: TEvent) => boolean, options?: EventHistoryFindOptions): Promise<TEvent | null>
102
+ find<TMatch extends TEvent>(
103
+ event_pattern: EventPattern<TMatch>,
104
+ where?: (event: TMatch) => boolean,
105
+ options?: EventHistoryFindOptions
106
+ ): Promise<TMatch | null>
107
+ async find(
108
+ event_pattern: EventPattern<TEvent> | '*',
109
+ where: (event: TEvent) => boolean = () => true,
110
+ options: EventHistoryFindOptions = {}
111
+ ): Promise<TEvent | null> {
112
+ const past = options.past ?? true
113
+ const future = options.future ?? false
114
+ const child_of = options.child_of ?? null
115
+ const eventIsChildOf = options.event_is_child_of ?? ((event: BaseEvent, ancestor: BaseEvent) => this.eventIsChildOf(event, ancestor))
116
+ const waitForFutureMatch = options.wait_for_future_match
117
+ if (past === false && future === false) {
118
+ return null
119
+ }
120
+
121
+ const event_key = EventHistory.normalizeEventPattern(event_pattern)
122
+ const cutoff_at = past === true ? null : monotonicDatetime(new Date(Date.now() - Math.max(0, Number(past)) * 1000).toISOString())
123
+
124
+ const event_field_filters = Object.entries(options).filter(
125
+ ([key, value]) =>
126
+ key !== 'past' &&
127
+ key !== 'future' &&
128
+ key !== 'child_of' &&
129
+ key !== 'event_is_child_of' &&
130
+ key !== 'wait_for_future_match' &&
131
+ value !== undefined
132
+ )
133
+
134
+ const matches = (event: BaseEvent): boolean =>
135
+ (event_key === '*' || event.event_type === event_key) &&
136
+ (!child_of || eventIsChildOf(event, child_of)) &&
137
+ event_field_filters.every(([field_name, expected]) => (event as unknown as Record<string, unknown>)[field_name] === expected) &&
138
+ where(event as TEvent)
139
+
140
+ if (past !== false) {
141
+ const history_values = Array.from(this._events.values())
142
+ for (let i = history_values.length - 1; i >= 0; i -= 1) {
143
+ const event = history_values[i]
144
+ if (cutoff_at !== null && event.event_created_at < cutoff_at) {
145
+ continue
146
+ }
147
+ if (matches(event)) {
148
+ return event
149
+ }
150
+ }
151
+ }
152
+
153
+ if (future === false || !waitForFutureMatch) {
154
+ return null
155
+ }
156
+
157
+ return (await waitForFutureMatch(event_key, matches, future)) as TEvent | null
158
+ }
159
+
160
+ trimEventHistory(options: EventHistoryTrimOptions<TEvent> = {}): number {
161
+ const max_history_size = options.max_history_size ?? this.max_history_size
162
+ const max_history_drop = options.max_history_drop ?? this.max_history_drop
163
+ if (max_history_size === null) {
164
+ return 0
165
+ }
166
+
167
+ const is_event_complete = options.is_event_complete ?? ((event: TEvent) => event.event_status === 'completed')
168
+ const on_remove = options.on_remove
169
+
170
+ if (max_history_size === 0) {
171
+ let removed_count = 0
172
+ for (const [event_id, event] of Array.from(this._events.entries())) {
173
+ if (!is_event_complete(event)) {
174
+ continue
175
+ }
176
+ this._events.delete(event_id)
177
+ on_remove?.(event)
178
+ removed_count += 1
179
+ }
180
+ return removed_count
181
+ }
182
+
183
+ if (!max_history_drop || this.size <= max_history_size) {
184
+ return 0
185
+ }
186
+
187
+ let remaining_overage = this.size - max_history_size
188
+ let removed_count = 0
189
+ const remove_event = (event_id: string, event: TEvent): void => {
190
+ this._events.delete(event_id)
191
+ on_remove?.(event)
192
+ removed_count += 1
193
+ }
194
+
195
+ for (const [event_id, event] of Array.from(this._events.entries())) {
196
+ if (remaining_overage <= 0) {
197
+ break
198
+ }
199
+ if (!is_event_complete(event)) {
200
+ continue
201
+ }
202
+ remove_event(event_id, event)
203
+ remaining_overage -= 1
204
+ }
205
+
206
+ let dropped_uncompleted = 0
207
+ for (const [event_id, event] of Array.from(this._events.entries())) {
208
+ if (remaining_overage <= 0) {
209
+ break
210
+ }
211
+ if (!is_event_complete(event)) {
212
+ dropped_uncompleted += 1
213
+ }
214
+ remove_event(event_id, event)
215
+ remaining_overage -= 1
216
+ }
217
+
218
+ if (dropped_uncompleted > 0 && !this._warned_about_dropping_uncompleted_events) {
219
+ this._warned_about_dropping_uncompleted_events = true
220
+ const owner_label = options.owner_label ?? 'EventBus'
221
+ console.error(
222
+ `[abxbus] ⚠️ Bus ${owner_label} has exceeded max_history_size=${max_history_size} and is dropping oldest history entries (even uncompleted events). Increase max_history_size or set max_history_drop=false to reject.`
223
+ )
224
+ }
225
+
226
+ return removed_count
227
+ }
228
+
229
+ private eventIsChildOf(event: BaseEvent, ancestor: BaseEvent): boolean {
230
+ let current_parent_id = event.event_parent_id
231
+ const visited = new Set<string>()
232
+
233
+ while (current_parent_id && !visited.has(current_parent_id)) {
234
+ if (current_parent_id === ancestor.event_id) {
235
+ return true
236
+ }
237
+ visited.add(current_parent_id)
238
+ const parent = this._events.get(current_parent_id)
239
+ if (!parent) {
240
+ return false
241
+ }
242
+ current_parent_id = parent.event_parent_id
243
+ }
244
+
245
+ return false
246
+ }
247
+ }