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
package/src/bridges.ts ADDED
@@ -0,0 +1,376 @@
1
+ import { BaseEvent } from './base_event.js'
2
+ import { EventBus } from './event_bus.js'
3
+ import type { EventClass, EventHandlerCallable, EventPattern, UntypedEventHandlerFunction } from './types.js'
4
+
5
+ type EndpointScheme = 'unix' | 'http' | 'https'
6
+
7
+ type ParsedEndpoint = {
8
+ raw: string
9
+ scheme: EndpointScheme
10
+ host?: string
11
+ port?: number
12
+ path?: string
13
+ }
14
+
15
+ export type HTTPEventBridgeOptions = {
16
+ send_to?: string | null
17
+ listen_on?: string | null
18
+ name?: string
19
+ }
20
+
21
+ const isNodeRuntime = (): boolean => {
22
+ const maybe_process = (globalThis as { process?: { versions?: { node?: string } } }).process
23
+ return typeof maybe_process?.versions?.node === 'string'
24
+ }
25
+
26
+ const isBrowserRuntime = (): boolean => !isNodeRuntime() && typeof globalThis.window !== 'undefined'
27
+
28
+ const randomSuffix = (): string => Math.random().toString(36).slice(2, 10)
29
+ const UNIX_SOCKET_MAX_PATH_CHARS = 90
30
+
31
+ const parseEndpoint = (raw_endpoint: string): ParsedEndpoint => {
32
+ let parsed: URL
33
+ try {
34
+ parsed = new URL(raw_endpoint)
35
+ } catch {
36
+ throw new Error(`Invalid endpoint URL: ${raw_endpoint}`)
37
+ }
38
+
39
+ const protocol = parsed.protocol.replace(/:$/, '').toLowerCase()
40
+ if (protocol !== 'unix' && protocol !== 'http' && protocol !== 'https') {
41
+ throw new Error(`Unsupported endpoint scheme: ${raw_endpoint}`)
42
+ }
43
+
44
+ if (protocol === 'unix') {
45
+ const socket_path = decodeURIComponent(parsed.pathname || '')
46
+ if (!socket_path) {
47
+ throw new Error(`Invalid unix endpoint (missing socket path): ${raw_endpoint}`)
48
+ }
49
+ const socket_path_len = new TextEncoder().encode(socket_path).length
50
+ if (socket_path_len > UNIX_SOCKET_MAX_PATH_CHARS) {
51
+ throw new Error(`Unix socket path is too long (${socket_path_len} chars), max is ${UNIX_SOCKET_MAX_PATH_CHARS}: ${socket_path}`)
52
+ }
53
+ return { raw: raw_endpoint, scheme: 'unix', path: socket_path }
54
+ }
55
+
56
+ if (!parsed.hostname) {
57
+ throw new Error(`Invalid HTTP endpoint (missing hostname): ${raw_endpoint}`)
58
+ }
59
+
60
+ const default_port = protocol === 'https' ? 443 : 80
61
+ return {
62
+ raw: raw_endpoint,
63
+ scheme: protocol,
64
+ host: parsed.hostname,
65
+ port: parsed.port ? Number(parsed.port) : default_port,
66
+ path: `${parsed.pathname || '/'}${parsed.search || ''}`,
67
+ }
68
+ }
69
+
70
+ const importNodeModule = async (specifier: string): Promise<any> => {
71
+ const dynamic_import = Function('module_name', 'return import(module_name)') as (module_name: string) => Promise<unknown>
72
+ return dynamic_import(specifier) as Promise<any>
73
+ }
74
+
75
+ class _EventBridge {
76
+ readonly send_to: ParsedEndpoint | null
77
+ readonly listen_on: ParsedEndpoint | null
78
+ readonly name: string
79
+
80
+ protected readonly inbound_bus: EventBus
81
+ private start_promise: Promise<void> | null
82
+ private node_server: any | null
83
+
84
+ constructor(send_to?: string | null, listen_on?: string | null, name?: string) {
85
+ this.send_to = send_to ? parseEndpoint(send_to) : null
86
+ this.listen_on = listen_on ? parseEndpoint(listen_on) : null
87
+ this.name = name ?? `EventBridge_${randomSuffix()}`
88
+ this.inbound_bus = new EventBus(this.name, { max_history_size: 0 })
89
+ this.start_promise = null
90
+ this.node_server = null
91
+
92
+ if (this.listen_on && isBrowserRuntime()) {
93
+ throw new Error(`${this.constructor.name} listen_on is not supported in browser runtimes`)
94
+ }
95
+
96
+ this.dispatch = this.dispatch.bind(this)
97
+ this.emit = this.emit.bind(this)
98
+ this.on = this.on.bind(this)
99
+ }
100
+
101
+ on<T extends BaseEvent>(event_pattern: EventClass<T>, handler: EventHandlerCallable<T>): void
102
+ on<T extends BaseEvent>(event_pattern: string | '*', handler: UntypedEventHandlerFunction<T>): void
103
+ on(event_pattern: EventPattern | '*', handler: EventHandlerCallable | UntypedEventHandlerFunction): void {
104
+ this.ensureListenerStarted()
105
+ if (typeof event_pattern === 'string') {
106
+ this.inbound_bus.on(event_pattern, handler as UntypedEventHandlerFunction<BaseEvent>)
107
+ return
108
+ }
109
+ this.inbound_bus.on(event_pattern as EventClass<BaseEvent>, handler as EventHandlerCallable<BaseEvent>)
110
+ }
111
+
112
+ async emit<T extends BaseEvent>(event: T): Promise<void> {
113
+ if (!this.send_to) {
114
+ throw new Error(`${this.constructor.name}.emit() requires send_to`)
115
+ }
116
+
117
+ const payload = event.toJSON()
118
+
119
+ if (this.send_to.scheme === 'unix') {
120
+ await this.sendUnix(this.send_to, payload)
121
+ return
122
+ }
123
+
124
+ await this.sendHttp(this.send_to, payload)
125
+ }
126
+
127
+ async dispatch<T extends BaseEvent>(event: T): Promise<void> {
128
+ return this.emit(event)
129
+ }
130
+
131
+ async start(): Promise<void> {
132
+ if (!this.listen_on) return
133
+ if (this.node_server) return
134
+ if (this.start_promise) {
135
+ await this.start_promise
136
+ return
137
+ }
138
+
139
+ if (!isNodeRuntime()) {
140
+ throw new Error(`${this.constructor.name} listen_on is only supported in Node.js runtimes`)
141
+ }
142
+
143
+ const launch = (async () => {
144
+ const endpoint = this.listen_on
145
+ if (!endpoint) return
146
+
147
+ if (endpoint.scheme === 'unix') {
148
+ await this.startUnixListener(endpoint)
149
+ return
150
+ }
151
+
152
+ if (endpoint.scheme !== 'http') {
153
+ throw new Error(`listen_on only supports unix:// or http:// endpoints, got: ${endpoint.raw}`)
154
+ }
155
+
156
+ await this.startHttpListener(endpoint)
157
+ })()
158
+ this.start_promise = launch
159
+
160
+ try {
161
+ await launch
162
+ } finally {
163
+ if (this.start_promise === launch) {
164
+ this.start_promise = null
165
+ }
166
+ }
167
+ }
168
+
169
+ async close(): Promise<void> {
170
+ if (this.start_promise) {
171
+ await Promise.allSettled([this.start_promise])
172
+ this.start_promise = null
173
+ }
174
+
175
+ if (this.node_server) {
176
+ const server = this.node_server
177
+ await new Promise<void>((resolve) => {
178
+ server.close(() => resolve())
179
+ })
180
+ this.node_server = null
181
+ }
182
+
183
+ this.inbound_bus.destroy()
184
+ }
185
+
186
+ private ensureListenerStarted(): void {
187
+ if (!this.listen_on || this.node_server || this.start_promise) {
188
+ return
189
+ }
190
+ void this.start().catch((error: unknown) => {
191
+ console.error('[abxbus] EventBridge failed to start listener', error)
192
+ })
193
+ }
194
+
195
+ private async handleIncomingPayload(payload: unknown): Promise<void> {
196
+ const event = BaseEvent.fromJSON(payload).eventReset()
197
+ this.inbound_bus.emit(event)
198
+ }
199
+
200
+ private async sendHttp(endpoint: ParsedEndpoint, payload: unknown): Promise<void> {
201
+ const response = await fetch(endpoint.raw, {
202
+ method: 'POST',
203
+ headers: { 'content-type': 'application/json' },
204
+ body: JSON.stringify(payload),
205
+ })
206
+ if (!response.ok) {
207
+ throw new Error(`IPC HTTP send failed with status ${response.status}: ${endpoint.raw}`)
208
+ }
209
+ }
210
+
211
+ private async sendUnix(endpoint: ParsedEndpoint, payload: unknown): Promise<void> {
212
+ if (!isNodeRuntime()) {
213
+ throw new Error('unix:// send_to is only supported in Node.js runtimes')
214
+ }
215
+
216
+ const socket_path = endpoint.path
217
+ if (!socket_path) {
218
+ throw new Error(`Invalid unix endpoint: ${endpoint.raw}`)
219
+ }
220
+
221
+ const node_net = await importNodeModule('node:net')
222
+ await new Promise<void>((resolve, reject) => {
223
+ const socket = node_net.createConnection(socket_path, () => {
224
+ socket.end(`${JSON.stringify(payload)}\n`)
225
+ })
226
+ socket.on('error', (error: unknown) => reject(error))
227
+ socket.on('close', () => resolve())
228
+ })
229
+ }
230
+
231
+ private async startHttpListener(endpoint: ParsedEndpoint): Promise<void> {
232
+ const node_http = await importNodeModule('node:http')
233
+ const expected_path = endpoint.path || '/'
234
+
235
+ this.node_server = node_http.createServer((req: any, res: any) => {
236
+ const method = (req.method || '').toUpperCase()
237
+ const request_url = String(req.url || '/')
238
+
239
+ if (method !== 'POST') {
240
+ res.statusCode = 405
241
+ res.end('method not allowed')
242
+ return
243
+ }
244
+ if (request_url !== expected_path) {
245
+ res.statusCode = 404
246
+ res.end('not found')
247
+ return
248
+ }
249
+
250
+ let body = ''
251
+ req.setEncoding('utf8')
252
+ req.on('data', (chunk: string) => {
253
+ body += chunk
254
+ })
255
+ req.on('end', () => {
256
+ let parsed_payload: unknown
257
+ try {
258
+ parsed_payload = JSON.parse(body)
259
+ } catch {
260
+ res.statusCode = 400
261
+ res.end('invalid json')
262
+ return
263
+ }
264
+
265
+ void this.handleIncomingPayload(parsed_payload)
266
+ .then(() => {
267
+ res.statusCode = 202
268
+ res.end('accepted')
269
+ })
270
+ .catch((error: unknown) => {
271
+ res.statusCode = 500
272
+ res.end('failed to process event')
273
+ console.error('[abxbus] EventBridge HTTP listener error', error)
274
+ })
275
+ })
276
+ })
277
+
278
+ await new Promise<void>((resolve, reject) => {
279
+ this.node_server.once('error', (error: unknown) => reject(error))
280
+ this.node_server.listen(endpoint.port, endpoint.host, () => resolve())
281
+ })
282
+ }
283
+
284
+ private async startUnixListener(endpoint: ParsedEndpoint): Promise<void> {
285
+ const socket_path = endpoint.path
286
+ if (!socket_path) {
287
+ throw new Error(`Invalid unix endpoint: ${endpoint.raw}`)
288
+ }
289
+
290
+ const node_net = await importNodeModule('node:net')
291
+ const node_fs = await importNodeModule('node:fs')
292
+
293
+ try {
294
+ await node_fs.promises.unlink(socket_path)
295
+ } catch (error: unknown) {
296
+ const code = (error as { code?: string }).code
297
+ if (code !== 'ENOENT') {
298
+ throw error
299
+ }
300
+ }
301
+
302
+ this.node_server = node_net.createServer((socket: any) => {
303
+ let buffer = ''
304
+ socket.setEncoding('utf8')
305
+ socket.on('data', (chunk: string) => {
306
+ buffer += chunk
307
+ while (true) {
308
+ const newline_index = buffer.indexOf('\n')
309
+ if (newline_index < 0) break
310
+ const line = buffer.slice(0, newline_index).trim()
311
+ buffer = buffer.slice(newline_index + 1)
312
+ if (!line) continue
313
+ try {
314
+ const parsed_payload = JSON.parse(line)
315
+ void this.handleIncomingPayload(parsed_payload)
316
+ } catch {
317
+ // Ignore malformed lines and continue reading next frames.
318
+ }
319
+ }
320
+ })
321
+ socket.on('end', () => {
322
+ const remainder = buffer.trim()
323
+ if (!remainder) return
324
+ try {
325
+ const parsed_payload = JSON.parse(remainder)
326
+ void this.handleIncomingPayload(parsed_payload)
327
+ } catch {
328
+ // Ignore malformed trailing frame.
329
+ }
330
+ })
331
+ })
332
+
333
+ await new Promise<void>((resolve, reject) => {
334
+ this.node_server.once('error', (error: unknown) => reject(error))
335
+ this.node_server.listen(socket_path, () => resolve())
336
+ })
337
+ }
338
+ }
339
+
340
+ export class HTTPEventBridge extends _EventBridge {
341
+ constructor(send_to?: string | null, listen_on?: string | null, name?: string)
342
+ constructor(options?: HTTPEventBridgeOptions)
343
+ constructor(send_to_or_options?: string | null | HTTPEventBridgeOptions, listen_on?: string | null, name?: string) {
344
+ const options: HTTPEventBridgeOptions =
345
+ typeof send_to_or_options === 'object'
346
+ ? (send_to_or_options ?? {})
347
+ : { send_to: send_to_or_options ?? undefined, listen_on: listen_on ?? undefined, name }
348
+
349
+ if (options.send_to && parseEndpoint(options.send_to).scheme === 'unix') {
350
+ throw new Error('HTTPEventBridge send_to must be http:// or https://')
351
+ }
352
+ if (options.listen_on && parseEndpoint(options.listen_on).scheme !== 'http') {
353
+ throw new Error('HTTPEventBridge listen_on must be http://')
354
+ }
355
+
356
+ super(options.send_to, options.listen_on, options.name ?? `HTTPEventBridge_${randomSuffix()}`)
357
+ }
358
+ }
359
+
360
+ export class SocketEventBridge extends _EventBridge {
361
+ constructor(path?: string | null, name?: string) {
362
+ const normalized = path ? (path.startsWith('unix://') ? path.slice(7) : path) : null
363
+ if (normalized === '') {
364
+ throw new Error('SocketEventBridge path must not be empty')
365
+ }
366
+
367
+ const endpoint = normalized ? `unix://${normalized}` : null
368
+ super(endpoint, endpoint, name ?? `SocketEventBridge_${randomSuffix()}`)
369
+ }
370
+ }
371
+
372
+ export { NATSEventBridge } from './bridge_nats.js'
373
+ export { RedisEventBridge } from './bridge_redis.js'
374
+ export { PostgresEventBridge } from './bridge_postgres.js'
375
+ export { JSONLEventBridge } from './bridge_jsonl.js'
376
+ export { SQLiteEventBridge } from './bridge_sqlite.js'