abxbus 2.5.0 → 2.5.3
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/BaseEvent.d.ts +2 -1
- package/dist/cjs/BaseEvent.js.map +2 -2
- package/dist/cjs/CoreClient.d.ts +167 -0
- package/dist/cjs/CoreEventBus.d.ts +334 -0
- package/dist/cjs/base_event.d.ts +2 -2
- package/dist/cjs/event_handler.d.ts +0 -1
- package/dist/cjs/retry.d.ts +8 -1
- package/dist/cjs/retry.js +283 -14
- package/dist/cjs/retry.js.map +2 -2
- package/dist/esm/BaseEvent.js.map +2 -2
- package/dist/esm/retry.js +283 -14
- package/dist/esm/retry.js.map +2 -2
- package/dist/types/BaseEvent.d.ts +2 -1
- package/dist/types/CoreClient.d.ts +167 -0
- package/dist/types/CoreEventBus.d.ts +334 -0
- package/dist/types/base_event.d.ts +2 -2
- package/dist/types/event_handler.d.ts +0 -1
- package/dist/types/retry.d.ts +8 -1
- package/package.json +1 -1
- package/src/BaseEvent.ts +5 -1
- package/src/retry.ts +365 -22
- package/dist/cjs/bridge_ipc.d.ts +0 -45
- package/dist/cjs/middleware_otel_tracing.d.ts +0 -49
- package/dist/types/bridge_ipc.d.ts +0 -45
- package/dist/types/middleware_otel_tracing.d.ts +0 -49
package/src/retry.ts
CHANGED
|
@@ -7,6 +7,18 @@ type MultiprocessLockHandle = {
|
|
|
7
7
|
release: () => Promise<void>
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
type SyncMultiprocessLockHandle = {
|
|
11
|
+
release: () => void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type AnyFunction = (this: any, ...args: any[]) => any
|
|
15
|
+
type LegacyMethodDescriptor = TypedPropertyDescriptor<AnyFunction>
|
|
16
|
+
type RetryDecorator = {
|
|
17
|
+
<T extends AnyFunction>(target: T): T
|
|
18
|
+
<T extends AnyFunction>(target: T, context: ClassMethodDecoratorContext): T
|
|
19
|
+
(target: object, property_key: string | symbol, descriptor: LegacyMethodDescriptor): LegacyMethodDescriptor
|
|
20
|
+
}
|
|
21
|
+
|
|
10
22
|
const MULTIPROCESS_SEMAPHORE_DIRNAME = 'browser_use_semaphores'
|
|
11
23
|
const MULTIPROCESS_STALE_LOCK_MS = 5 * 60 * 1000
|
|
12
24
|
|
|
@@ -144,12 +156,19 @@ class RetrySemaphore {
|
|
|
144
156
|
this.waiters = []
|
|
145
157
|
}
|
|
146
158
|
|
|
147
|
-
|
|
159
|
+
tryAcquire(): boolean {
|
|
148
160
|
if (this.size === Infinity) {
|
|
149
|
-
return
|
|
161
|
+
return true
|
|
150
162
|
}
|
|
151
163
|
if (this.inUse < this.size) {
|
|
152
164
|
this.inUse += 1
|
|
165
|
+
return true
|
|
166
|
+
}
|
|
167
|
+
return false
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async acquire(): Promise<void> {
|
|
171
|
+
if (this.tryAcquire()) {
|
|
153
172
|
return
|
|
154
173
|
}
|
|
155
174
|
await new Promise<void>((resolve) => {
|
|
@@ -189,13 +208,17 @@ export function clearSemaphoreRegistry(): void {
|
|
|
189
208
|
|
|
190
209
|
// ─── retry() decorator / higher-order wrapper ────────────────────────────────
|
|
191
210
|
//
|
|
192
|
-
// Usage as a higher-order function (works on
|
|
211
|
+
// Usage as a higher-order function (works on async and sync functions):
|
|
193
212
|
//
|
|
194
213
|
// const fetchWithRetry = retry({ max_attempts: 3, retry_after: 1 })(async (url: string) => {
|
|
195
214
|
// return await fetch(url)
|
|
196
215
|
// })
|
|
197
216
|
//
|
|
198
|
-
//
|
|
217
|
+
// const readWithRetry = retry({ max_attempts: 3, retry_after: 1 })((path: string) => {
|
|
218
|
+
// return readFileSync(path, 'utf8')
|
|
219
|
+
// })
|
|
220
|
+
//
|
|
221
|
+
// Usage as a TC39 Stage 3 or legacy experimental decorator on class methods:
|
|
199
222
|
//
|
|
200
223
|
// class ApiClient {
|
|
201
224
|
// @retry({ max_attempts: 3, retry_after: 1 })
|
|
@@ -208,7 +231,7 @@ export function clearSemaphoreRegistry(): void {
|
|
|
208
231
|
// await riskyOperation(event.data)
|
|
209
232
|
// }))
|
|
210
233
|
|
|
211
|
-
export function retry(options: RetryOptions = {}) {
|
|
234
|
+
export function retry(options: RetryOptions = {}): RetryDecorator {
|
|
212
235
|
const {
|
|
213
236
|
max_attempts = 1,
|
|
214
237
|
retry_after = 0,
|
|
@@ -222,12 +245,60 @@ export function retry(options: RetryOptions = {}) {
|
|
|
222
245
|
semaphore_timeout,
|
|
223
246
|
} = options
|
|
224
247
|
|
|
225
|
-
|
|
248
|
+
const decorateFunction = <T extends AnyFunction>(target: T, _context?: ClassMethodDecoratorContext): T => {
|
|
226
249
|
const fn_name = target.name || (_context?.name as string) || 'anonymous'
|
|
227
250
|
const effective_max_attempts = Math.max(1, max_attempts)
|
|
228
251
|
const effective_retry_after = Math.max(0, retry_after)
|
|
229
252
|
|
|
230
|
-
|
|
253
|
+
const shouldRetry = (error: unknown): boolean => {
|
|
254
|
+
if (!retry_on_errors || retry_on_errors.length === 0) return true
|
|
255
|
+
return retry_on_errors.some((matcher) =>
|
|
256
|
+
typeof matcher === 'string'
|
|
257
|
+
? (error as Error)?.name === matcher
|
|
258
|
+
: matcher instanceof RegExp
|
|
259
|
+
? matcher.test(String(error))
|
|
260
|
+
: error instanceof matcher
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const asyncRetryDelay = async (attempt: number): Promise<void> => {
|
|
265
|
+
const delay_seconds = effective_retry_after * Math.pow(retry_backoff_factor, attempt - 1)
|
|
266
|
+
if (delay_seconds > 0) {
|
|
267
|
+
await sleep(delay_seconds * 1000)
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const syncRetryDelay = (attempt: number): void => {
|
|
272
|
+
const delay_seconds = effective_retry_after * Math.pow(retry_backoff_factor, attempt - 1)
|
|
273
|
+
if (delay_seconds > 0) {
|
|
274
|
+
sleepSync(delay_seconds * 1000)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const runRetryLoopFromThenable = async (this_arg: any, args: any[], first_thenable: PromiseLike<any>, first_attempt: number): Promise<any> => {
|
|
279
|
+
let current_result: any = first_thenable
|
|
280
|
+
for (let attempt = first_attempt; attempt <= effective_max_attempts; attempt++) {
|
|
281
|
+
try {
|
|
282
|
+
if (attempt !== first_attempt) {
|
|
283
|
+
current_result = target.apply(this_arg, args)
|
|
284
|
+
}
|
|
285
|
+
if (isThenable(current_result)) {
|
|
286
|
+
if (timeout != null && timeout > 0) {
|
|
287
|
+
return await _runWithTimeout(() => Promise.resolve(current_result), timeout * 1000, attempt)
|
|
288
|
+
}
|
|
289
|
+
return await current_result
|
|
290
|
+
}
|
|
291
|
+
return current_result
|
|
292
|
+
} catch (error) {
|
|
293
|
+
if (!shouldRetry(error)) throw error
|
|
294
|
+
if (attempt >= effective_max_attempts) throw error
|
|
295
|
+
await asyncRetryDelay(attempt)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
throw new Error(`retry(${fn_name}): unexpected end of retry loop`)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function asyncRetryWrapper(this: any, ...args: any[]): Promise<any> {
|
|
231
302
|
const base_name = typeof semaphore_name_option === 'function' ? semaphore_name_option(...args) : (semaphore_name_option ?? fn_name)
|
|
232
303
|
const sem_name = typeof base_name === 'string' ? base_name : String(base_name)
|
|
233
304
|
// ── Resolve scoped semaphore key at call time (uses `this` for class/instance scopes) ──
|
|
@@ -305,26 +376,13 @@ export function retry(options: RetryOptions = {}) {
|
|
|
305
376
|
return await Promise.resolve(target.apply(this, args))
|
|
306
377
|
}
|
|
307
378
|
} catch (error) {
|
|
308
|
-
|
|
309
|
-
if (retry_on_errors && retry_on_errors.length > 0) {
|
|
310
|
-
const is_retryable = retry_on_errors.some((matcher) =>
|
|
311
|
-
typeof matcher === 'string'
|
|
312
|
-
? (error as Error)?.name === matcher
|
|
313
|
-
: matcher instanceof RegExp
|
|
314
|
-
? matcher.test(String(error))
|
|
315
|
-
: error instanceof matcher
|
|
316
|
-
)
|
|
317
|
-
if (!is_retryable) throw error
|
|
318
|
-
}
|
|
379
|
+
if (!shouldRetry(error)) throw error
|
|
319
380
|
|
|
320
381
|
// Last attempt: rethrow
|
|
321
382
|
if (attempt >= effective_max_attempts) throw error
|
|
322
383
|
|
|
323
384
|
// Wait before next attempt with exponential backoff
|
|
324
|
-
|
|
325
|
-
if (delay_seconds > 0) {
|
|
326
|
-
await sleep(delay_seconds * 1000)
|
|
327
|
-
}
|
|
385
|
+
await asyncRetryDelay(attempt)
|
|
328
386
|
}
|
|
329
387
|
}
|
|
330
388
|
|
|
@@ -343,6 +401,89 @@ export function retry(options: RetryOptions = {}) {
|
|
|
343
401
|
}
|
|
344
402
|
}
|
|
345
403
|
|
|
404
|
+
function syncRetryWrapper(this: any, ...args: any[]): any {
|
|
405
|
+
const base_name = typeof semaphore_name_option === 'function' ? semaphore_name_option(...args) : (semaphore_name_option ?? fn_name)
|
|
406
|
+
const sem_name = typeof base_name === 'string' ? base_name : String(base_name)
|
|
407
|
+
const scoped_key = scopedSemaphoreKey(sem_name, semaphore_scope, this)
|
|
408
|
+
|
|
409
|
+
const held = getHeldSemaphores()
|
|
410
|
+
const needs_semaphore = semaphore_limit != null && semaphore_limit > 0
|
|
411
|
+
const is_reentrant = needs_semaphore && held.has(scoped_key)
|
|
412
|
+
|
|
413
|
+
let semaphore: RetrySemaphore | null = null
|
|
414
|
+
let multiprocess_lock: SyncMultiprocessLockHandle | null = null
|
|
415
|
+
let semaphore_acquired = false
|
|
416
|
+
|
|
417
|
+
if (needs_semaphore && !is_reentrant) {
|
|
418
|
+
const effective_sem_timeout =
|
|
419
|
+
semaphore_timeout != null ? semaphore_timeout : timeout != null ? timeout * Math.max(1, semaphore_limit! - 1) : null
|
|
420
|
+
|
|
421
|
+
if (semaphore_scope === 'multiprocess') {
|
|
422
|
+
if (isNodeRuntime()) {
|
|
423
|
+
multiprocess_lock = acquireMultiprocessSemaphoreSync(scoped_key, semaphore_limit!, effective_sem_timeout, semaphore_lax)
|
|
424
|
+
semaphore_acquired = multiprocess_lock !== null
|
|
425
|
+
} else {
|
|
426
|
+
logMultiprocessFallbackOnce('multiprocess semaphores require a Node.js runtime; falling back to in-process global scope')
|
|
427
|
+
semaphore = getOrCreateSemaphore(scoped_key, semaphore_limit!)
|
|
428
|
+
semaphore_acquired = acquireSemaphoreSyncOrThrow(semaphore, scoped_key, semaphore_limit!, effective_sem_timeout, semaphore_lax)
|
|
429
|
+
}
|
|
430
|
+
} else {
|
|
431
|
+
semaphore = getOrCreateSemaphore(scoped_key, semaphore_limit!)
|
|
432
|
+
semaphore_acquired = acquireSemaphoreSyncOrThrow(semaphore, scoped_key, semaphore_limit!, effective_sem_timeout, semaphore_lax)
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const new_held = new Set(held)
|
|
437
|
+
if (semaphore_acquired) {
|
|
438
|
+
new_held.add(scoped_key)
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const release = (): void => {
|
|
442
|
+
if (semaphore_acquired && multiprocess_lock) {
|
|
443
|
+
multiprocess_lock.release()
|
|
444
|
+
} else if (semaphore_acquired && semaphore) {
|
|
445
|
+
semaphore.release()
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const runRetryLoop = (): any => {
|
|
450
|
+
for (let attempt = 1; attempt <= effective_max_attempts; attempt++) {
|
|
451
|
+
const attempt_started_at = Date.now()
|
|
452
|
+
try {
|
|
453
|
+
const result = target.apply(this, args)
|
|
454
|
+
if (isThenable(result)) {
|
|
455
|
+
return runRetryLoopFromThenable(this, args, result, attempt)
|
|
456
|
+
}
|
|
457
|
+
if (timeout != null && timeout > 0 && Date.now() - attempt_started_at > timeout * 1000) {
|
|
458
|
+
throw new RetryTimeoutError(`Timed out after ${timeout}s (attempt ${attempt})`, {
|
|
459
|
+
timeout_seconds: timeout,
|
|
460
|
+
attempt,
|
|
461
|
+
})
|
|
462
|
+
}
|
|
463
|
+
return result
|
|
464
|
+
} catch (error) {
|
|
465
|
+
if (!shouldRetry(error)) throw error
|
|
466
|
+
if (attempt >= effective_max_attempts) throw error
|
|
467
|
+
syncRetryDelay(attempt)
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
throw new Error(`retry(${fn_name}): unexpected end of retry loop`)
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
try {
|
|
474
|
+
const result = runWithHeldSemaphores(new_held, runRetryLoop)
|
|
475
|
+
if (isThenable(result)) {
|
|
476
|
+
return Promise.resolve(result).finally(release)
|
|
477
|
+
}
|
|
478
|
+
release()
|
|
479
|
+
return result
|
|
480
|
+
} catch (error) {
|
|
481
|
+
release()
|
|
482
|
+
throw error
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const retryWrapper = isAsyncFunction(target) ? asyncRetryWrapper : syncRetryWrapper
|
|
346
487
|
Object.defineProperty(retryWrapper, 'name', { value: fn_name, configurable: true })
|
|
347
488
|
if (_context?.kind === 'method' && typeof _context.addInitializer === 'function') {
|
|
348
489
|
_context.addInitializer(function (this: unknown) {
|
|
@@ -354,6 +495,22 @@ export function retry(options: RetryOptions = {}) {
|
|
|
354
495
|
}
|
|
355
496
|
return retryWrapper as unknown as T
|
|
356
497
|
}
|
|
498
|
+
|
|
499
|
+
function decorator<T extends AnyFunction>(
|
|
500
|
+
target: T | object,
|
|
501
|
+
context_or_property_key?: ClassMethodDecoratorContext | string | symbol,
|
|
502
|
+
descriptor?: LegacyMethodDescriptor
|
|
503
|
+
): T | LegacyMethodDescriptor {
|
|
504
|
+
if (descriptor?.value && typeof descriptor.value === 'function') {
|
|
505
|
+
descriptor.value = decorateFunction(descriptor.value)
|
|
506
|
+
return descriptor
|
|
507
|
+
}
|
|
508
|
+
if (typeof target === 'function') {
|
|
509
|
+
return decorateFunction(target as T, typeof context_or_property_key === 'object' ? context_or_property_key : undefined)
|
|
510
|
+
}
|
|
511
|
+
throw new TypeError('retry() can only decorate functions and class methods')
|
|
512
|
+
}
|
|
513
|
+
return decorator as RetryDecorator
|
|
357
514
|
}
|
|
358
515
|
|
|
359
516
|
// ─── Internal helpers ────────────────────────────────────────────────────────
|
|
@@ -397,6 +554,18 @@ function findDecoratedMethodOwnerName(
|
|
|
397
554
|
return null
|
|
398
555
|
}
|
|
399
556
|
|
|
557
|
+
function isAsyncFunction(fn: AnyFunction): boolean {
|
|
558
|
+
return Object.prototype.toString.call(fn) === '[object AsyncFunction]'
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function isThenable(value: unknown): value is PromiseLike<unknown> {
|
|
562
|
+
return (
|
|
563
|
+
(typeof value === 'object' || typeof value === 'function') &&
|
|
564
|
+
value !== null &&
|
|
565
|
+
typeof (value as { then?: unknown }).then === 'function'
|
|
566
|
+
)
|
|
567
|
+
}
|
|
568
|
+
|
|
400
569
|
/**
|
|
401
570
|
* Try to acquire a semaphore within a timeout. Returns true if acquired, false if timed out.
|
|
402
571
|
* If the semaphore is acquired after the timeout (due to the waiter remaining queued),
|
|
@@ -426,12 +595,70 @@ async function acquireWithTimeout(semaphore: RetrySemaphore, timeout_ms: number)
|
|
|
426
595
|
})
|
|
427
596
|
}
|
|
428
597
|
|
|
598
|
+
function acquireSemaphoreSyncOrThrow(
|
|
599
|
+
semaphore: RetrySemaphore,
|
|
600
|
+
scoped_key: string,
|
|
601
|
+
semaphore_limit: number,
|
|
602
|
+
timeout_seconds: number | null,
|
|
603
|
+
semaphore_lax: boolean
|
|
604
|
+
): boolean {
|
|
605
|
+
const acquired = acquireSemaphoreSync(semaphore, timeout_seconds == null ? null : timeout_seconds * 1000)
|
|
606
|
+
if (acquired) return true
|
|
607
|
+
|
|
608
|
+
if (!semaphore_lax) {
|
|
609
|
+
throw new SemaphoreTimeoutError(`Failed to acquire semaphore "${scoped_key}" within ${timeout_seconds}s (limit=${semaphore_limit})`, {
|
|
610
|
+
semaphore_name: scoped_key,
|
|
611
|
+
semaphore_limit,
|
|
612
|
+
timeout_seconds: timeout_seconds ?? 0,
|
|
613
|
+
})
|
|
614
|
+
}
|
|
615
|
+
return false
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function acquireSemaphoreSync(semaphore: RetrySemaphore, timeout_ms: number | null): boolean {
|
|
619
|
+
if (semaphore.tryAcquire()) return true
|
|
620
|
+
|
|
621
|
+
const start = Date.now()
|
|
622
|
+
while (true) {
|
|
623
|
+
if (timeout_ms != null && timeout_ms > 0 && Date.now() - start >= timeout_ms) {
|
|
624
|
+
return false
|
|
625
|
+
}
|
|
626
|
+
sleepSync(10)
|
|
627
|
+
if (semaphore.tryAcquire()) return true
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
429
631
|
function logMultiprocessFallbackOnce(reason: string): void {
|
|
430
632
|
if (multiprocess_fallback_reason_logged === reason) return
|
|
431
633
|
multiprocess_fallback_reason_logged = reason
|
|
432
634
|
console.warn(`[abxbus.retry] ${reason}`)
|
|
433
635
|
}
|
|
434
636
|
|
|
637
|
+
function importNodeModuleSync(specifier: string): any {
|
|
638
|
+
const maybe_process = (
|
|
639
|
+
globalThis as {
|
|
640
|
+
process?: { getBuiltinModule?: (name: string) => any }
|
|
641
|
+
}
|
|
642
|
+
).process
|
|
643
|
+
const get_builtin_module = maybe_process?.getBuiltinModule
|
|
644
|
+
const bare_specifier = specifier.startsWith('node:') ? specifier.slice('node:'.length) : specifier
|
|
645
|
+
|
|
646
|
+
if (typeof get_builtin_module === 'function') {
|
|
647
|
+
const builtin = get_builtin_module(bare_specifier) ?? get_builtin_module(specifier)
|
|
648
|
+
if (builtin) return builtin
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
let require_fn: ((name: string) => any) | undefined
|
|
652
|
+
try {
|
|
653
|
+
require_fn = Function('return typeof require === "function" ? require : undefined')() as ((name: string) => any) | undefined
|
|
654
|
+
} catch {
|
|
655
|
+
require_fn = undefined
|
|
656
|
+
}
|
|
657
|
+
if (require_fn) return require_fn(specifier)
|
|
658
|
+
|
|
659
|
+
throw new Error('[abxbus.retry] synchronous Node.js module loading is unavailable; cannot use sync multiprocess semaphores')
|
|
660
|
+
}
|
|
661
|
+
|
|
435
662
|
async function importNodeModule(specifier: string): Promise<any> {
|
|
436
663
|
const dynamic_import = Function('module_name', 'return import(module_name)') as (module_name: string) => Promise<unknown>
|
|
437
664
|
return dynamic_import(specifier) as Promise<any>
|
|
@@ -537,6 +764,104 @@ async function acquireMultiprocessSemaphore(
|
|
|
537
764
|
return null
|
|
538
765
|
}
|
|
539
766
|
|
|
767
|
+
function acquireMultiprocessSemaphoreSync(
|
|
768
|
+
scoped_key: string,
|
|
769
|
+
semaphore_limit: number,
|
|
770
|
+
semaphore_timeout_seconds: number | null,
|
|
771
|
+
semaphore_lax: boolean
|
|
772
|
+
): SyncMultiprocessLockHandle | null {
|
|
773
|
+
const crypto = importNodeModuleSync('node:crypto')
|
|
774
|
+
const fs = importNodeModuleSync('node:fs')
|
|
775
|
+
const os = importNodeModuleSync('node:os')
|
|
776
|
+
const path = importNodeModuleSync('node:path')
|
|
777
|
+
const semaphore_directory = path.join(os.tmpdir(), MULTIPROCESS_SEMAPHORE_DIRNAME)
|
|
778
|
+
const lock_prefix = crypto.createHash('sha256').update(scoped_key).digest('hex').slice(0, 40)
|
|
779
|
+
fs.mkdirSync(semaphore_directory, { recursive: true })
|
|
780
|
+
|
|
781
|
+
const start = Date.now()
|
|
782
|
+
let retry_delay_ms = 100
|
|
783
|
+
|
|
784
|
+
while (true) {
|
|
785
|
+
const elapsed_ms = Date.now() - start
|
|
786
|
+
const remaining_ms =
|
|
787
|
+
semaphore_timeout_seconds != null && semaphore_timeout_seconds > 0 ? semaphore_timeout_seconds * 1000 - elapsed_ms : null
|
|
788
|
+
|
|
789
|
+
if (remaining_ms != null && remaining_ms <= 0) {
|
|
790
|
+
break
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
for (let slot = 0; slot < semaphore_limit; slot++) {
|
|
794
|
+
const slot_file = path.join(semaphore_directory, `${lock_prefix}.${String(slot).padStart(2, '0')}.lock`)
|
|
795
|
+
const token = `${process.pid}:${Date.now()}:${Math.random().toString(16).slice(2)}`
|
|
796
|
+
|
|
797
|
+
try {
|
|
798
|
+
const fd = fs.openSync(slot_file, 'wx', 0o600)
|
|
799
|
+
try {
|
|
800
|
+
fs.writeFileSync(
|
|
801
|
+
fd,
|
|
802
|
+
JSON.stringify({
|
|
803
|
+
token,
|
|
804
|
+
pid: process.pid,
|
|
805
|
+
semaphore_name: scoped_key,
|
|
806
|
+
created_at_ms: Date.now(),
|
|
807
|
+
}),
|
|
808
|
+
'utf8'
|
|
809
|
+
)
|
|
810
|
+
} finally {
|
|
811
|
+
fs.closeSync(fd)
|
|
812
|
+
}
|
|
813
|
+
return {
|
|
814
|
+
release: () => {
|
|
815
|
+
try {
|
|
816
|
+
const raw = String(fs.readFileSync(slot_file, 'utf8') || '').trim()
|
|
817
|
+
const current_owner = raw ? (JSON.parse(raw) as { token?: unknown }) : null
|
|
818
|
+
if (current_owner?.token === token) {
|
|
819
|
+
fs.unlinkSync(slot_file)
|
|
820
|
+
}
|
|
821
|
+
} catch {}
|
|
822
|
+
},
|
|
823
|
+
}
|
|
824
|
+
} catch (error) {
|
|
825
|
+
if (!(error instanceof Error) || (error as NodeJS.ErrnoException).code !== 'EEXIST') {
|
|
826
|
+
throw error
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
try {
|
|
830
|
+
const raw = String(fs.readFileSync(slot_file, 'utf8') || '').trim()
|
|
831
|
+
const current_owner = raw ? (JSON.parse(raw) as { pid?: unknown }) : null
|
|
832
|
+
const current_pid = typeof current_owner?.pid === 'number' ? current_owner.pid : null
|
|
833
|
+
if (current_pid != null) {
|
|
834
|
+
try {
|
|
835
|
+
process.kill(current_pid, 0)
|
|
836
|
+
continue
|
|
837
|
+
} catch {}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
const slot_age_ms = Date.now() - fs.statSync(slot_file).mtimeMs
|
|
841
|
+
if (current_pid != null || slot_age_ms >= MULTIPROCESS_STALE_LOCK_MS) {
|
|
842
|
+
fs.unlinkSync(slot_file)
|
|
843
|
+
}
|
|
844
|
+
} catch {}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
const sleep_ms = Math.min(retry_delay_ms, remaining_ms ?? retry_delay_ms)
|
|
849
|
+
if (sleep_ms > 0) {
|
|
850
|
+
sleepSync(sleep_ms)
|
|
851
|
+
}
|
|
852
|
+
retry_delay_ms = Math.min(retry_delay_ms * 2, 1000)
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
if (!semaphore_lax) {
|
|
856
|
+
throw new SemaphoreTimeoutError(
|
|
857
|
+
`Failed to acquire semaphore "${scoped_key}" within ${semaphore_timeout_seconds}s (limit=${semaphore_limit})`,
|
|
858
|
+
{ semaphore_name: scoped_key, semaphore_limit, timeout_seconds: semaphore_timeout_seconds ?? 0 }
|
|
859
|
+
)
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
return null
|
|
863
|
+
}
|
|
864
|
+
|
|
540
865
|
/** Run fn() with a timeout. Rejects with RetryTimeoutError if the timeout fires first. */
|
|
541
866
|
async function _runWithTimeout<T>(fn: () => Promise<T>, timeout_ms: number, attempt: number): Promise<T> {
|
|
542
867
|
return new Promise<T>((resolve, reject) => {
|
|
@@ -576,3 +901,21 @@ async function _runWithTimeout<T>(fn: () => Promise<T>, timeout_ms: number, atte
|
|
|
576
901
|
function sleep(ms: number): Promise<void> {
|
|
577
902
|
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
578
903
|
}
|
|
904
|
+
|
|
905
|
+
function sleepSync(ms: number): void {
|
|
906
|
+
if (ms <= 0) return
|
|
907
|
+
|
|
908
|
+
const shared_array_buffer = (globalThis as { SharedArrayBuffer?: typeof SharedArrayBuffer }).SharedArrayBuffer
|
|
909
|
+
const atomics = (globalThis as { Atomics?: typeof Atomics }).Atomics
|
|
910
|
+
if (typeof shared_array_buffer === 'function' && typeof atomics?.wait === 'function') {
|
|
911
|
+
try {
|
|
912
|
+
const buffer = new shared_array_buffer(4)
|
|
913
|
+
const view = new Int32Array(buffer)
|
|
914
|
+
atomics.wait(view, 0, 0, ms)
|
|
915
|
+
return
|
|
916
|
+
} catch {}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
const deadline = Date.now() + ms
|
|
920
|
+
while (Date.now() < deadline) {}
|
|
921
|
+
}
|
package/dist/cjs/bridge_ipc.d.ts
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { BaseEvent } from './base_event.js';
|
|
2
|
-
import { EventBus } from './event_bus.js';
|
|
3
|
-
import type { EventClass, EventHandlerCallable, UntypedEventHandlerFunction } from './types.js';
|
|
4
|
-
type EndpointScheme = 'unix' | 'http' | 'https';
|
|
5
|
-
type ParsedEndpoint = {
|
|
6
|
-
raw: string;
|
|
7
|
-
scheme: EndpointScheme;
|
|
8
|
-
host?: string;
|
|
9
|
-
port?: number;
|
|
10
|
-
path?: string;
|
|
11
|
-
};
|
|
12
|
-
export type HTTPEventBridgeOptions = {
|
|
13
|
-
send_to?: string | null;
|
|
14
|
-
listen_on?: string | null;
|
|
15
|
-
name?: string;
|
|
16
|
-
};
|
|
17
|
-
export declare class EventBridge {
|
|
18
|
-
readonly send_to: ParsedEndpoint | null;
|
|
19
|
-
readonly listen_on: ParsedEndpoint | null;
|
|
20
|
-
readonly name: string;
|
|
21
|
-
protected readonly inbound_bus: EventBus;
|
|
22
|
-
private start_promise;
|
|
23
|
-
private node_server;
|
|
24
|
-
constructor(send_to?: string | null, listen_on?: string | null, name?: string);
|
|
25
|
-
on<T extends BaseEvent>(event_pattern: EventClass<T>, handler: EventHandlerCallable<T>): void;
|
|
26
|
-
on<T extends BaseEvent>(event_pattern: string | '*', handler: UntypedEventHandlerFunction<T>): void;
|
|
27
|
-
emit<T extends BaseEvent>(event: T): Promise<void>;
|
|
28
|
-
dispatch<T extends BaseEvent>(event: T): Promise<void>;
|
|
29
|
-
start(): Promise<void>;
|
|
30
|
-
close(): Promise<void>;
|
|
31
|
-
private ensureListenerStarted;
|
|
32
|
-
private handleIncomingPayload;
|
|
33
|
-
private sendHttp;
|
|
34
|
-
private sendUnix;
|
|
35
|
-
private startHttpListener;
|
|
36
|
-
private startUnixListener;
|
|
37
|
-
}
|
|
38
|
-
export declare class HTTPEventBridge extends EventBridge {
|
|
39
|
-
constructor(send_to?: string | null, listen_on?: string | null, name?: string);
|
|
40
|
-
constructor(options?: HTTPEventBridgeOptions);
|
|
41
|
-
}
|
|
42
|
-
export declare class SocketEventBridge extends EventBridge {
|
|
43
|
-
constructor(path?: string | null, name?: string);
|
|
44
|
-
}
|
|
45
|
-
export {};
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { trace, type Span, type SpanAttributes, type SpanContext, type TimeInput, type Tracer } from '@opentelemetry/api';
|
|
2
|
-
import type { BaseEvent } from './base_event.js';
|
|
3
|
-
import type { EventBus } from './event_bus.js';
|
|
4
|
-
import type { EventResult } from './event_result.js';
|
|
5
|
-
import type { EventBusMiddleware } from './middlewares.js';
|
|
6
|
-
import type { EventStatus } from './types.js';
|
|
7
|
-
type OpenTelemetryTraceApi = Pick<typeof trace, 'getTracer' | 'setSpan'> & Partial<Pick<typeof trace, 'setSpanContext'>>;
|
|
8
|
-
export type OtelTracingSpanFactoryInput = {
|
|
9
|
-
name: string;
|
|
10
|
-
span_context: SpanContext;
|
|
11
|
-
parent_span_context?: SpanContext;
|
|
12
|
-
attributes: SpanAttributes;
|
|
13
|
-
start_time?: TimeInput;
|
|
14
|
-
};
|
|
15
|
-
export type OtelTracingSpanFactory = (input: OtelTracingSpanFactoryInput) => Span;
|
|
16
|
-
export type OtelTracingSpanProvider = object;
|
|
17
|
-
export type OtelTracingMiddlewareOptions = {
|
|
18
|
-
tracer?: Tracer;
|
|
19
|
-
trace_api?: OpenTelemetryTraceApi;
|
|
20
|
-
span_provider?: OtelTracingSpanProvider;
|
|
21
|
-
span_factory?: OtelTracingSpanFactory;
|
|
22
|
-
otlp_endpoint?: string;
|
|
23
|
-
service_name?: string;
|
|
24
|
-
instrumentation_name?: string;
|
|
25
|
-
root_span_attributes?: SpanAttributes | ((eventbus: EventBus, event: BaseEvent) => SpanAttributes);
|
|
26
|
-
};
|
|
27
|
-
export declare class OtelTracingMiddleware implements EventBusMiddleware {
|
|
28
|
-
private readonly tracer;
|
|
29
|
-
private readonly trace_api;
|
|
30
|
-
private readonly span_factory?;
|
|
31
|
-
private readonly span_provider?;
|
|
32
|
-
private readonly root_span_attributes;
|
|
33
|
-
private readonly event_spans;
|
|
34
|
-
private readonly event_contexts;
|
|
35
|
-
private readonly handler_spans;
|
|
36
|
-
private readonly handler_contexts;
|
|
37
|
-
constructor(options?: OtelTracingMiddlewareOptions);
|
|
38
|
-
onEventChange(eventbus: EventBus, event: BaseEvent, status: EventStatus): void;
|
|
39
|
-
onEventResultChange(eventbus: EventBus, event: BaseEvent, event_result: EventResult, status: EventStatus): void;
|
|
40
|
-
private startEventSpan;
|
|
41
|
-
private completeEventSpan;
|
|
42
|
-
private startHandlerSpan;
|
|
43
|
-
private completeHandlerSpan;
|
|
44
|
-
private parentContextForEvent;
|
|
45
|
-
private completeEventSpanWithFactory;
|
|
46
|
-
private exportEventTreeWithFactory;
|
|
47
|
-
private exportHandlerSpanWithFactory;
|
|
48
|
-
}
|
|
49
|
-
export {};
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { BaseEvent } from './base_event.js';
|
|
2
|
-
import { EventBus } from './event_bus.js';
|
|
3
|
-
import type { EventClass, EventHandlerCallable, UntypedEventHandlerFunction } from './types.js';
|
|
4
|
-
type EndpointScheme = 'unix' | 'http' | 'https';
|
|
5
|
-
type ParsedEndpoint = {
|
|
6
|
-
raw: string;
|
|
7
|
-
scheme: EndpointScheme;
|
|
8
|
-
host?: string;
|
|
9
|
-
port?: number;
|
|
10
|
-
path?: string;
|
|
11
|
-
};
|
|
12
|
-
export type HTTPEventBridgeOptions = {
|
|
13
|
-
send_to?: string | null;
|
|
14
|
-
listen_on?: string | null;
|
|
15
|
-
name?: string;
|
|
16
|
-
};
|
|
17
|
-
export declare class EventBridge {
|
|
18
|
-
readonly send_to: ParsedEndpoint | null;
|
|
19
|
-
readonly listen_on: ParsedEndpoint | null;
|
|
20
|
-
readonly name: string;
|
|
21
|
-
protected readonly inbound_bus: EventBus;
|
|
22
|
-
private start_promise;
|
|
23
|
-
private node_server;
|
|
24
|
-
constructor(send_to?: string | null, listen_on?: string | null, name?: string);
|
|
25
|
-
on<T extends BaseEvent>(event_pattern: EventClass<T>, handler: EventHandlerCallable<T>): void;
|
|
26
|
-
on<T extends BaseEvent>(event_pattern: string | '*', handler: UntypedEventHandlerFunction<T>): void;
|
|
27
|
-
emit<T extends BaseEvent>(event: T): Promise<void>;
|
|
28
|
-
dispatch<T extends BaseEvent>(event: T): Promise<void>;
|
|
29
|
-
start(): Promise<void>;
|
|
30
|
-
close(): Promise<void>;
|
|
31
|
-
private ensureListenerStarted;
|
|
32
|
-
private handleIncomingPayload;
|
|
33
|
-
private sendHttp;
|
|
34
|
-
private sendUnix;
|
|
35
|
-
private startHttpListener;
|
|
36
|
-
private startUnixListener;
|
|
37
|
-
}
|
|
38
|
-
export declare class HTTPEventBridge extends EventBridge {
|
|
39
|
-
constructor(send_to?: string | null, listen_on?: string | null, name?: string);
|
|
40
|
-
constructor(options?: HTTPEventBridgeOptions);
|
|
41
|
-
}
|
|
42
|
-
export declare class SocketEventBridge extends EventBridge {
|
|
43
|
-
constructor(path?: string | null, name?: string);
|
|
44
|
-
}
|
|
45
|
-
export {};
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { trace, type Span, type SpanAttributes, type SpanContext, type TimeInput, type Tracer } from '@opentelemetry/api';
|
|
2
|
-
import type { BaseEvent } from './base_event.js';
|
|
3
|
-
import type { EventBus } from './event_bus.js';
|
|
4
|
-
import type { EventResult } from './event_result.js';
|
|
5
|
-
import type { EventBusMiddleware } from './middlewares.js';
|
|
6
|
-
import type { EventStatus } from './types.js';
|
|
7
|
-
type OpenTelemetryTraceApi = Pick<typeof trace, 'getTracer' | 'setSpan'> & Partial<Pick<typeof trace, 'setSpanContext'>>;
|
|
8
|
-
export type OtelTracingSpanFactoryInput = {
|
|
9
|
-
name: string;
|
|
10
|
-
span_context: SpanContext;
|
|
11
|
-
parent_span_context?: SpanContext;
|
|
12
|
-
attributes: SpanAttributes;
|
|
13
|
-
start_time?: TimeInput;
|
|
14
|
-
};
|
|
15
|
-
export type OtelTracingSpanFactory = (input: OtelTracingSpanFactoryInput) => Span;
|
|
16
|
-
export type OtelTracingSpanProvider = object;
|
|
17
|
-
export type OtelTracingMiddlewareOptions = {
|
|
18
|
-
tracer?: Tracer;
|
|
19
|
-
trace_api?: OpenTelemetryTraceApi;
|
|
20
|
-
span_provider?: OtelTracingSpanProvider;
|
|
21
|
-
span_factory?: OtelTracingSpanFactory;
|
|
22
|
-
otlp_endpoint?: string;
|
|
23
|
-
service_name?: string;
|
|
24
|
-
instrumentation_name?: string;
|
|
25
|
-
root_span_attributes?: SpanAttributes | ((eventbus: EventBus, event: BaseEvent) => SpanAttributes);
|
|
26
|
-
};
|
|
27
|
-
export declare class OtelTracingMiddleware implements EventBusMiddleware {
|
|
28
|
-
private readonly tracer;
|
|
29
|
-
private readonly trace_api;
|
|
30
|
-
private readonly span_factory?;
|
|
31
|
-
private readonly span_provider?;
|
|
32
|
-
private readonly root_span_attributes;
|
|
33
|
-
private readonly event_spans;
|
|
34
|
-
private readonly event_contexts;
|
|
35
|
-
private readonly handler_spans;
|
|
36
|
-
private readonly handler_contexts;
|
|
37
|
-
constructor(options?: OtelTracingMiddlewareOptions);
|
|
38
|
-
onEventChange(eventbus: EventBus, event: BaseEvent, status: EventStatus): void;
|
|
39
|
-
onEventResultChange(eventbus: EventBus, event: BaseEvent, event_result: EventResult, status: EventStatus): void;
|
|
40
|
-
private startEventSpan;
|
|
41
|
-
private completeEventSpan;
|
|
42
|
-
private startHandlerSpan;
|
|
43
|
-
private completeHandlerSpan;
|
|
44
|
-
private parentContextForEvent;
|
|
45
|
-
private completeEventSpanWithFactory;
|
|
46
|
-
private exportEventTreeWithFactory;
|
|
47
|
-
private exportHandlerSpanWithFactory;
|
|
48
|
-
}
|
|
49
|
-
export {};
|