abxbus 2.4.16 → 2.4.19
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.d.ts +9 -1
- package/dist/cjs/middleware_otel_tracing.js +73 -6
- 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 +78 -7
- 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/dist/types/middleware_otel_tracing.d.ts +9 -1
- 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 +290 -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,290 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ROOT_CONTEXT,
|
|
3
|
+
SpanStatusCode,
|
|
4
|
+
trace,
|
|
5
|
+
type Context,
|
|
6
|
+
type Span,
|
|
7
|
+
type SpanAttributeValue,
|
|
8
|
+
type SpanAttributes,
|
|
9
|
+
type Tracer,
|
|
10
|
+
} from '@opentelemetry/api'
|
|
11
|
+
|
|
12
|
+
import type { BaseEvent } from './base_event.js'
|
|
13
|
+
import type { EventBus } from './event_bus.js'
|
|
14
|
+
import type { EventResult } from './event_result.js'
|
|
15
|
+
import type { EventBusMiddleware } from './middlewares.js'
|
|
16
|
+
import type { EventStatus } from './types.js'
|
|
17
|
+
|
|
18
|
+
type OpenTelemetryTraceApi = Pick<typeof trace, 'getTracer' | 'setSpan'>
|
|
19
|
+
|
|
20
|
+
export type OtelTracingMiddlewareOptions = {
|
|
21
|
+
tracer?: Tracer
|
|
22
|
+
trace_api?: OpenTelemetryTraceApi
|
|
23
|
+
root_span_name?: string | ((eventbus: EventBus, event: BaseEvent) => string)
|
|
24
|
+
root_span_attributes?: SpanAttributes | ((eventbus: EventBus, event: BaseEvent) => SpanAttributes)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class OtelTracingMiddleware implements EventBusMiddleware {
|
|
28
|
+
private readonly tracer: Tracer
|
|
29
|
+
private readonly trace_api: OpenTelemetryTraceApi
|
|
30
|
+
private readonly root_span_name: OtelTracingMiddlewareOptions['root_span_name']
|
|
31
|
+
private readonly root_span_attributes: OtelTracingMiddlewareOptions['root_span_attributes']
|
|
32
|
+
private readonly root_spans = new Map<string, Span>()
|
|
33
|
+
private readonly root_contexts = new Map<string, Context>()
|
|
34
|
+
private readonly event_spans = new Map<string, Span>()
|
|
35
|
+
private readonly event_contexts = new Map<string, Context>()
|
|
36
|
+
private readonly handler_spans = new Map<string, Span>()
|
|
37
|
+
private readonly handler_contexts = new Map<string, Context>()
|
|
38
|
+
|
|
39
|
+
constructor(options: OtelTracingMiddlewareOptions = {}) {
|
|
40
|
+
this.trace_api = options.trace_api ?? trace
|
|
41
|
+
this.tracer = options.tracer ?? this.trace_api.getTracer('abxbus')
|
|
42
|
+
this.root_span_name = options.root_span_name
|
|
43
|
+
this.root_span_attributes = options.root_span_attributes
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
onEventChange(eventbus: EventBus, event: BaseEvent, status: EventStatus): void {
|
|
47
|
+
if (status === 'started') {
|
|
48
|
+
this.startEventSpan(eventbus, event)
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (status === 'completed') {
|
|
53
|
+
this.completeEventSpan(eventbus, event)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
onEventResultChange(eventbus: EventBus, event: BaseEvent, event_result: EventResult, status: EventStatus): void {
|
|
58
|
+
if (status === 'started') {
|
|
59
|
+
this.startHandlerSpan(eventbus, event, event_result)
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (status === 'completed') {
|
|
64
|
+
this.completeHandlerSpan(event_result)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private startEventSpan(eventbus: EventBus, event: BaseEvent): Span {
|
|
69
|
+
const existing = this.event_spans.get(event.event_id)
|
|
70
|
+
if (existing) {
|
|
71
|
+
return existing
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const parent_context = this.parentContextForEvent(event) ?? this.startRootSpan(eventbus, event)
|
|
75
|
+
const start_time = dateFromIso(event.event_started_at)
|
|
76
|
+
const span = this.tracer.startSpan(
|
|
77
|
+
`abxbus.event ${event.event_type}`,
|
|
78
|
+
{
|
|
79
|
+
attributes: compactAttributes({
|
|
80
|
+
'abxbus.bus.id': eventbus.id,
|
|
81
|
+
'abxbus.bus.name': eventbus.name,
|
|
82
|
+
'abxbus.event.id': event.event_id,
|
|
83
|
+
'abxbus.event.type': event.event_type,
|
|
84
|
+
'abxbus.event.version': event.event_version,
|
|
85
|
+
'abxbus.event.session_id': stringValue((event as { session_id?: unknown }).session_id),
|
|
86
|
+
'abxbus.event.parent_id': event.event_parent_id,
|
|
87
|
+
'abxbus.event.emitted_by_handler_id': event.event_emitted_by_handler_id,
|
|
88
|
+
'abxbus.event.path': event.event_path.join(' '),
|
|
89
|
+
}),
|
|
90
|
+
startTime: start_time,
|
|
91
|
+
},
|
|
92
|
+
parent_context
|
|
93
|
+
)
|
|
94
|
+
const span_context = this.trace_api.setSpan(parent_context, span)
|
|
95
|
+
this.event_spans.set(event.event_id, span)
|
|
96
|
+
this.event_contexts.set(event.event_id, span_context)
|
|
97
|
+
return span
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private completeEventSpan(eventbus: EventBus, event: BaseEvent): void {
|
|
101
|
+
const span = this.event_spans.get(event.event_id) ?? this.startEventSpan(eventbus, event)
|
|
102
|
+
if (event.event_errors.length > 0) {
|
|
103
|
+
recordSpanError(span, event.event_errors[0])
|
|
104
|
+
} else {
|
|
105
|
+
span.setStatus({ code: SpanStatusCode.OK })
|
|
106
|
+
}
|
|
107
|
+
span.setAttributes(
|
|
108
|
+
compactAttributes({
|
|
109
|
+
'abxbus.event.status': event.event_status,
|
|
110
|
+
'abxbus.event.result_count': event.event_results.size,
|
|
111
|
+
'abxbus.event.error_count': event.event_errors.length,
|
|
112
|
+
'abxbus.event.child_count': event.event_children.length,
|
|
113
|
+
})
|
|
114
|
+
)
|
|
115
|
+
const start_time = dateFromIso(event.event_started_at)
|
|
116
|
+
const end_time = endTimeAfterStart(start_time, dateFromIso(event.event_completed_at))
|
|
117
|
+
span.end(end_time)
|
|
118
|
+
this.event_spans.delete(event.event_id)
|
|
119
|
+
this.event_contexts.delete(event.event_id)
|
|
120
|
+
this.completeRootSpan(event.event_id, start_time, end_time)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private startHandlerSpan(eventbus: EventBus, event: BaseEvent, event_result: EventResult): Span {
|
|
124
|
+
const existing = this.handler_spans.get(event_result.id)
|
|
125
|
+
if (existing) {
|
|
126
|
+
return existing
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const parent_context =
|
|
130
|
+
this.event_contexts.get(event.event_id) ?? this.trace_api.setSpan(ROOT_CONTEXT, this.startEventSpan(eventbus, event))
|
|
131
|
+
const span = this.tracer.startSpan(
|
|
132
|
+
`abxbus.handler ${event.event_type} ${event_result.handler_name}`,
|
|
133
|
+
{
|
|
134
|
+
attributes: compactAttributes({
|
|
135
|
+
'abxbus.bus.id': eventbus.id,
|
|
136
|
+
'abxbus.bus.name': eventbus.name,
|
|
137
|
+
'abxbus.event.id': event.event_id,
|
|
138
|
+
'abxbus.event.type': event.event_type,
|
|
139
|
+
'abxbus.handler.id': event_result.handler_id,
|
|
140
|
+
'abxbus.handler.name': event_result.handler_name,
|
|
141
|
+
'abxbus.handler.file_path': event_result.handler_file_path,
|
|
142
|
+
'abxbus.handler.event_pattern': event_result.handler.event_pattern,
|
|
143
|
+
'abxbus.event_result.id': event_result.id,
|
|
144
|
+
}),
|
|
145
|
+
startTime: dateFromIso(event_result.started_at),
|
|
146
|
+
},
|
|
147
|
+
parent_context
|
|
148
|
+
)
|
|
149
|
+
const span_context = this.trace_api.setSpan(parent_context, span)
|
|
150
|
+
this.handler_spans.set(event_result.id, span)
|
|
151
|
+
this.handler_contexts.set(handlerSpanKey(event_result.event_id, event_result.handler_id), span_context)
|
|
152
|
+
return span
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private completeHandlerSpan(event_result: EventResult): void {
|
|
156
|
+
const span = this.handler_spans.get(event_result.id)
|
|
157
|
+
if (!span) {
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (event_result.error !== undefined) {
|
|
162
|
+
recordSpanError(span, event_result.error)
|
|
163
|
+
} else {
|
|
164
|
+
span.setStatus({ code: SpanStatusCode.OK })
|
|
165
|
+
}
|
|
166
|
+
span.setAttributes(
|
|
167
|
+
compactAttributes({
|
|
168
|
+
'abxbus.event_result.status': event_result.status,
|
|
169
|
+
'abxbus.handler.child_count': event_result.event_children.length,
|
|
170
|
+
})
|
|
171
|
+
)
|
|
172
|
+
span.end(endTimeAfterStart(dateFromIso(event_result.started_at), dateFromIso(event_result.completed_at)))
|
|
173
|
+
this.handler_spans.delete(event_result.id)
|
|
174
|
+
this.handler_contexts.delete(handlerSpanKey(event_result.event_id, event_result.handler_id))
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private startRootSpan(eventbus: EventBus, event: BaseEvent): Context {
|
|
178
|
+
const existing = this.root_contexts.get(event.event_id)
|
|
179
|
+
if (existing) {
|
|
180
|
+
return existing
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const session_id = stringValue((event as { session_id?: unknown }).session_id)
|
|
184
|
+
const root_attributes = resolveAttributes(this.root_span_attributes, eventbus, event)
|
|
185
|
+
const root_span = this.tracer.startSpan(
|
|
186
|
+
resolveRootSpanName(this.root_span_name, eventbus, event),
|
|
187
|
+
{
|
|
188
|
+
attributes: compactAttributes({
|
|
189
|
+
...root_attributes,
|
|
190
|
+
'abxbus.trace.root': true,
|
|
191
|
+
'abxbus.bus.id': eventbus.id,
|
|
192
|
+
'abxbus.bus.name': eventbus.name,
|
|
193
|
+
'abxbus.root_event.id': event.event_id,
|
|
194
|
+
'abxbus.root_event.type': event.event_type,
|
|
195
|
+
'abxbus.root_event.session_id': session_id,
|
|
196
|
+
}),
|
|
197
|
+
startTime: dateFromIso(event.event_started_at),
|
|
198
|
+
},
|
|
199
|
+
ROOT_CONTEXT
|
|
200
|
+
)
|
|
201
|
+
const root_context = this.trace_api.setSpan(ROOT_CONTEXT, root_span)
|
|
202
|
+
this.root_spans.set(event.event_id, root_span)
|
|
203
|
+
this.root_contexts.set(event.event_id, root_context)
|
|
204
|
+
return root_context
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private completeRootSpan(event_id: string, start_time: Date | undefined, end_time: Date | undefined): void {
|
|
208
|
+
const root_span = this.root_spans.get(event_id)
|
|
209
|
+
if (!root_span) {
|
|
210
|
+
return
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
root_span.setStatus({ code: SpanStatusCode.OK })
|
|
214
|
+
root_span.end(endTimeAfterStart(start_time, end_time))
|
|
215
|
+
this.root_spans.delete(event_id)
|
|
216
|
+
this.root_contexts.delete(event_id)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private parentContextForEvent(event: BaseEvent): Context | undefined {
|
|
220
|
+
if (event.event_parent_id && event.event_emitted_by_handler_id) {
|
|
221
|
+
const handler_context = this.handler_contexts.get(handlerSpanKey(event.event_parent_id, event.event_emitted_by_handler_id))
|
|
222
|
+
if (handler_context) {
|
|
223
|
+
return handler_context
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return event.event_parent_id ? this.event_contexts.get(event.event_parent_id) : undefined
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function handlerSpanKey(event_id: string, handler_id: string): string {
|
|
232
|
+
return `${event_id}:${handler_id}`
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function dateFromIso(value: string | null | undefined): Date | undefined {
|
|
236
|
+
if (value == null) {
|
|
237
|
+
return undefined
|
|
238
|
+
}
|
|
239
|
+
const date = new Date(value)
|
|
240
|
+
return Number.isNaN(date.getTime()) ? undefined : date
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function endTimeAfterStart(start_time: Date | undefined, end_time: Date | undefined): Date | undefined {
|
|
244
|
+
if (!start_time || !end_time) {
|
|
245
|
+
return end_time
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return end_time.getTime() > start_time.getTime() ? end_time : new Date(start_time.getTime() + 1)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function resolveRootSpanName(root_span_name: OtelTracingMiddlewareOptions['root_span_name'], eventbus: EventBus, event: BaseEvent): string {
|
|
252
|
+
if (typeof root_span_name === 'function') {
|
|
253
|
+
return root_span_name(eventbus, event)
|
|
254
|
+
}
|
|
255
|
+
return root_span_name ?? `abxbus.trace ${eventbus.name}`
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function resolveAttributes(
|
|
259
|
+
attributes: OtelTracingMiddlewareOptions['root_span_attributes'],
|
|
260
|
+
eventbus: EventBus,
|
|
261
|
+
event: BaseEvent
|
|
262
|
+
): SpanAttributes {
|
|
263
|
+
return typeof attributes === 'function' ? attributes(eventbus, event) : (attributes ?? {})
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function stringValue(value: unknown): string | undefined {
|
|
267
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function compactAttributes(attributes: Record<string, SpanAttributeValue | null | undefined>): SpanAttributes {
|
|
271
|
+
const compacted: SpanAttributes = {}
|
|
272
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
273
|
+
if (value !== null && value !== undefined) {
|
|
274
|
+
compacted[key] = value
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return compacted
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function recordSpanError(span: Span, error: unknown): void {
|
|
281
|
+
if (error instanceof Error) {
|
|
282
|
+
span.recordException(error)
|
|
283
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: error.message })
|
|
284
|
+
return
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const message = typeof error === 'string' ? error : 'Unknown abxbus handler error'
|
|
288
|
+
span.recordException(message)
|
|
289
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message })
|
|
290
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { BaseEvent } from './base_event.js'
|
|
2
|
+
import type { EventBus } from './event_bus.js'
|
|
3
|
+
import type { EventHandler } from './event_handler.js'
|
|
4
|
+
import type { EventResult } from './event_result.js'
|
|
5
|
+
import type { EventStatus } from './types.js'
|
|
6
|
+
|
|
7
|
+
export type { EventStatus } from './types.js'
|
|
8
|
+
|
|
9
|
+
export interface EventBusMiddleware {
|
|
10
|
+
onEventChange?(eventbus: EventBus, event: BaseEvent, status: EventStatus): void | Promise<void>
|
|
11
|
+
onEventResultChange?(eventbus: EventBus, event: BaseEvent, event_result: EventResult, status: EventStatus): void | Promise<void>
|
|
12
|
+
onBusHandlersChange?(eventbus: EventBus, handler: EventHandler, registered: boolean): void | Promise<void>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type EventBusMiddlewareCtor = new () => EventBusMiddleware
|
|
16
|
+
export type EventBusMiddlewareInput = EventBusMiddleware | EventBusMiddlewareCtor
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export const isNodeRuntime = (): boolean => {
|
|
2
|
+
const maybe_process = (globalThis as { process?: { versions?: { node?: string } } }).process
|
|
3
|
+
return typeof maybe_process?.versions?.node === 'string'
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const missingDependencyError = (bridge_name: string, package_name: string): Error =>
|
|
7
|
+
new Error(`${bridge_name} requires optional dependency "${package_name}". Install it with: npm install ${package_name}`)
|
|
8
|
+
|
|
9
|
+
export const assertOptionalDependencyAvailable = (bridge_name: string, package_name: string): void => {
|
|
10
|
+
if (!isNodeRuntime()) return
|
|
11
|
+
|
|
12
|
+
const maybe_process = (
|
|
13
|
+
globalThis as {
|
|
14
|
+
process?: { getBuiltinModule?: (name: string) => any; cwd?: () => string }
|
|
15
|
+
}
|
|
16
|
+
).process
|
|
17
|
+
const get_builtin_module = maybe_process?.getBuiltinModule
|
|
18
|
+
|
|
19
|
+
let require_fn: { resolve: (specifier: string) => string } | undefined
|
|
20
|
+
try {
|
|
21
|
+
require_fn = Function('return typeof require === "function" ? require : undefined')() as
|
|
22
|
+
| { resolve: (specifier: string) => string }
|
|
23
|
+
| undefined
|
|
24
|
+
} catch {
|
|
25
|
+
require_fn = undefined
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!require_fn && typeof get_builtin_module === 'function') {
|
|
29
|
+
const module_builtin = get_builtin_module('module')
|
|
30
|
+
const create_require = module_builtin?.createRequire
|
|
31
|
+
if (typeof create_require === 'function') {
|
|
32
|
+
const cwd = typeof maybe_process?.cwd === 'function' ? maybe_process.cwd() : '.'
|
|
33
|
+
require_fn = create_require(`${cwd}/package.json`) as { resolve: (specifier: string) => string }
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!require_fn) return
|
|
38
|
+
try {
|
|
39
|
+
require_fn.resolve(package_name)
|
|
40
|
+
} catch {
|
|
41
|
+
throw missingDependencyError(bridge_name, package_name)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const importOptionalDependency = async (bridge_name: string, package_name: string): Promise<any> => {
|
|
46
|
+
const dynamic_import = Function('module_name', 'return import(module_name)') as (module_name: string) => Promise<unknown>
|
|
47
|
+
try {
|
|
48
|
+
return (await dynamic_import(package_name)) as any
|
|
49
|
+
} catch {
|
|
50
|
+
throw missingDependencyError(bridge_name, package_name)
|
|
51
|
+
}
|
|
52
|
+
}
|