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,201 @@
1
+ import { ROOT_CONTEXT, SpanStatusCode, trace, type Context, type Span, type Tracer } from '@opentelemetry/api'
2
+
3
+ import type { BaseEvent } from './base_event.js'
4
+ import type { EventBus } from './event_bus.js'
5
+ import type { EventResult } from './event_result.js'
6
+ import type { EventBusMiddleware } from './middlewares.js'
7
+ import type { EventStatus } from './types.js'
8
+
9
+ type OpenTelemetryTraceApi = Pick<typeof trace, 'getTracer' | 'setSpan'>
10
+
11
+ export type OtelTracingMiddlewareOptions = {
12
+ tracer?: Tracer
13
+ trace_api?: OpenTelemetryTraceApi
14
+ }
15
+
16
+ export class OtelTracingMiddleware implements EventBusMiddleware {
17
+ private readonly tracer: Tracer
18
+ private readonly trace_api: OpenTelemetryTraceApi
19
+ private readonly event_spans = new Map<string, Span>()
20
+ private readonly event_contexts = new Map<string, Context>()
21
+ private readonly handler_spans = new Map<string, Span>()
22
+ private readonly handler_contexts = new Map<string, Context>()
23
+
24
+ constructor(options: OtelTracingMiddlewareOptions = {}) {
25
+ this.trace_api = options.trace_api ?? trace
26
+ this.tracer = options.tracer ?? this.trace_api.getTracer('abxbus')
27
+ }
28
+
29
+ onEventChange(eventbus: EventBus, event: BaseEvent, status: EventStatus): void {
30
+ if (status === 'started') {
31
+ this.startEventSpan(eventbus, event)
32
+ return
33
+ }
34
+
35
+ if (status === 'completed') {
36
+ this.completeEventSpan(eventbus, event)
37
+ }
38
+ }
39
+
40
+ onEventResultChange(eventbus: EventBus, event: BaseEvent, event_result: EventResult, status: EventStatus): void {
41
+ if (status === 'started') {
42
+ this.startHandlerSpan(eventbus, event, event_result)
43
+ return
44
+ }
45
+
46
+ if (status === 'completed') {
47
+ this.completeHandlerSpan(event_result)
48
+ }
49
+ }
50
+
51
+ private startEventSpan(eventbus: EventBus, event: BaseEvent): Span {
52
+ const existing = this.event_spans.get(event.event_id)
53
+ if (existing) {
54
+ return existing
55
+ }
56
+
57
+ const parent_context = this.parentContextForEvent(event) ?? ROOT_CONTEXT
58
+ const span = this.tracer.startSpan(
59
+ `abxbus.event ${event.event_type}`,
60
+ {
61
+ attributes: compactAttributes({
62
+ 'abxbus.bus.id': eventbus.id,
63
+ 'abxbus.bus.name': eventbus.name,
64
+ 'abxbus.event.id': event.event_id,
65
+ 'abxbus.event.type': event.event_type,
66
+ 'abxbus.event.version': event.event_version,
67
+ 'abxbus.event.parent_id': event.event_parent_id,
68
+ 'abxbus.event.emitted_by_handler_id': event.event_emitted_by_handler_id,
69
+ 'abxbus.event.path': event.event_path.join(' '),
70
+ }),
71
+ startTime: dateFromIso(event.event_started_at),
72
+ },
73
+ parent_context
74
+ )
75
+ const span_context = this.trace_api.setSpan(parent_context, span)
76
+ this.event_spans.set(event.event_id, span)
77
+ this.event_contexts.set(event.event_id, span_context)
78
+ return span
79
+ }
80
+
81
+ private completeEventSpan(eventbus: EventBus, event: BaseEvent): void {
82
+ const span = this.event_spans.get(event.event_id) ?? this.startEventSpan(eventbus, event)
83
+ if (event.event_errors.length > 0) {
84
+ recordSpanError(span, event.event_errors[0])
85
+ } else {
86
+ span.setStatus({ code: SpanStatusCode.OK })
87
+ }
88
+ span.setAttributes(
89
+ compactAttributes({
90
+ 'abxbus.event.status': event.event_status,
91
+ 'abxbus.event.result_count': event.event_results.size,
92
+ 'abxbus.event.error_count': event.event_errors.length,
93
+ 'abxbus.event.child_count': event.event_children.length,
94
+ })
95
+ )
96
+ span.end(dateFromIso(event.event_completed_at))
97
+ this.event_spans.delete(event.event_id)
98
+ this.event_contexts.delete(event.event_id)
99
+ }
100
+
101
+ private startHandlerSpan(eventbus: EventBus, event: BaseEvent, event_result: EventResult): Span {
102
+ const existing = this.handler_spans.get(event_result.id)
103
+ if (existing) {
104
+ return existing
105
+ }
106
+
107
+ const parent_context =
108
+ this.event_contexts.get(event.event_id) ?? this.trace_api.setSpan(ROOT_CONTEXT, this.startEventSpan(eventbus, event))
109
+ const span = this.tracer.startSpan(
110
+ `abxbus.handler ${event.event_type} ${event_result.handler_name}`,
111
+ {
112
+ attributes: compactAttributes({
113
+ 'abxbus.bus.id': eventbus.id,
114
+ 'abxbus.bus.name': eventbus.name,
115
+ 'abxbus.event.id': event.event_id,
116
+ 'abxbus.event.type': event.event_type,
117
+ 'abxbus.handler.id': event_result.handler_id,
118
+ 'abxbus.handler.name': event_result.handler_name,
119
+ 'abxbus.handler.file_path': event_result.handler_file_path,
120
+ 'abxbus.handler.event_pattern': event_result.handler.event_pattern,
121
+ 'abxbus.event_result.id': event_result.id,
122
+ }),
123
+ startTime: dateFromIso(event_result.started_at),
124
+ },
125
+ parent_context
126
+ )
127
+ const span_context = this.trace_api.setSpan(parent_context, span)
128
+ this.handler_spans.set(event_result.id, span)
129
+ this.handler_contexts.set(handlerSpanKey(event_result.event_id, event_result.handler_id), span_context)
130
+ return span
131
+ }
132
+
133
+ private completeHandlerSpan(event_result: EventResult): void {
134
+ const span = this.handler_spans.get(event_result.id)
135
+ if (!span) {
136
+ return
137
+ }
138
+
139
+ if (event_result.error !== undefined) {
140
+ recordSpanError(span, event_result.error)
141
+ } else {
142
+ span.setStatus({ code: SpanStatusCode.OK })
143
+ }
144
+ span.setAttributes(
145
+ compactAttributes({
146
+ 'abxbus.event_result.status': event_result.status,
147
+ 'abxbus.handler.child_count': event_result.event_children.length,
148
+ })
149
+ )
150
+ span.end(dateFromIso(event_result.completed_at))
151
+ this.handler_spans.delete(event_result.id)
152
+ this.handler_contexts.delete(handlerSpanKey(event_result.event_id, event_result.handler_id))
153
+ }
154
+
155
+ private parentContextForEvent(event: BaseEvent): Context | undefined {
156
+ if (event.event_parent_id && event.event_emitted_by_handler_id) {
157
+ const handler_context = this.handler_contexts.get(handlerSpanKey(event.event_parent_id, event.event_emitted_by_handler_id))
158
+ if (handler_context) {
159
+ return handler_context
160
+ }
161
+ }
162
+
163
+ return event.event_parent_id ? this.event_contexts.get(event.event_parent_id) : undefined
164
+ }
165
+ }
166
+
167
+ function handlerSpanKey(event_id: string, handler_id: string): string {
168
+ return `${event_id}:${handler_id}`
169
+ }
170
+
171
+ function dateFromIso(value: string | null | undefined): Date | undefined {
172
+ if (value == null) {
173
+ return undefined
174
+ }
175
+ const date = new Date(value)
176
+ return Number.isNaN(date.getTime()) ? undefined : date
177
+ }
178
+
179
+ function compactAttributes(
180
+ attributes: Record<string, string | number | boolean | null | undefined>
181
+ ): Record<string, string | number | boolean> {
182
+ const compacted: Record<string, string | number | boolean> = {}
183
+ for (const [key, value] of Object.entries(attributes)) {
184
+ if (value !== null && value !== undefined) {
185
+ compacted[key] = value
186
+ }
187
+ }
188
+ return compacted
189
+ }
190
+
191
+ function recordSpanError(span: Span, error: unknown): void {
192
+ if (error instanceof Error) {
193
+ span.recordException(error)
194
+ span.setStatus({ code: SpanStatusCode.ERROR, message: error.message })
195
+ return
196
+ }
197
+
198
+ const message = typeof error === 'string' ? error : 'Unknown abxbus handler error'
199
+ span.recordException(message)
200
+ span.setStatus({ code: SpanStatusCode.ERROR, message })
201
+ }
@@ -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
+ }