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.
Files changed (44) 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.d.ts +9 -1
  7. package/dist/cjs/middleware_otel_tracing.js +73 -6
  8. package/dist/cjs/middleware_otel_tracing.js.map +2 -2
  9. package/dist/cjs/retry.js +39 -0
  10. package/dist/cjs/retry.js.map +2 -2
  11. package/dist/esm/event_bus.js +1 -1
  12. package/dist/esm/event_bus.js.map +2 -2
  13. package/dist/esm/event_handler.js +14 -1
  14. package/dist/esm/event_handler.js.map +2 -2
  15. package/dist/esm/middleware_otel_tracing.js +78 -7
  16. package/dist/esm/middleware_otel_tracing.js.map +2 -2
  17. package/dist/esm/retry.js +39 -0
  18. package/dist/esm/retry.js.map +2 -2
  19. package/dist/types/event_handler.d.ts +1 -0
  20. package/dist/types/middleware_otel_tracing.d.ts +9 -1
  21. package/package.json +5 -1
  22. package/src/async_context.ts +70 -0
  23. package/src/base_event.ts +1201 -0
  24. package/src/bridge_jsonl.ts +174 -0
  25. package/src/bridge_nats.ts +104 -0
  26. package/src/bridge_postgres.ts +277 -0
  27. package/src/bridge_redis.ts +194 -0
  28. package/src/bridge_sqlite.ts +289 -0
  29. package/src/bridges.ts +376 -0
  30. package/src/event_bus.ts +1263 -0
  31. package/src/event_handler.ts +379 -0
  32. package/src/event_history.ts +247 -0
  33. package/src/event_result.ts +483 -0
  34. package/src/events_suck.ts +96 -0
  35. package/src/helpers.ts +65 -0
  36. package/src/index.ts +37 -0
  37. package/src/lock_manager.ts +401 -0
  38. package/src/logging.ts +261 -0
  39. package/src/middleware_otel_tracing.ts +290 -0
  40. package/src/middlewares.ts +16 -0
  41. package/src/optional_deps.ts +52 -0
  42. package/src/retry.ts +578 -0
  43. package/src/timing.ts +52 -0
  44. 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
+ }