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,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
|
+
}
|