@julr/tenace 1.0.0-next.0

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 (41) hide show
  1. package/README.md +1034 -0
  2. package/build/src/adapters/cache/memory.d.ts +23 -0
  3. package/build/src/adapters/cache/memory.js +2 -0
  4. package/build/src/adapters/cache/types.d.ts +56 -0
  5. package/build/src/adapters/cache/types.js +1 -0
  6. package/build/src/adapters/lock/types.d.ts +104 -0
  7. package/build/src/adapters/lock/types.js +1 -0
  8. package/build/src/adapters/rate_limiter/memory.d.ts +14 -0
  9. package/build/src/adapters/rate_limiter/memory.js +2 -0
  10. package/build/src/adapters/rate_limiter/types.d.ts +101 -0
  11. package/build/src/adapters/rate_limiter/types.js +1 -0
  12. package/build/src/backoff.d.ts +79 -0
  13. package/build/src/chaos/manager.d.ts +29 -0
  14. package/build/src/chaos/policies.d.ts +10 -0
  15. package/build/src/chaos/types.d.ts +75 -0
  16. package/build/src/collection.d.ts +81 -0
  17. package/build/src/config.d.ts +38 -0
  18. package/build/src/errors/errors.d.ts +79 -0
  19. package/build/src/errors/main.d.ts +1 -0
  20. package/build/src/errors/main.js +2 -0
  21. package/build/src/errors-BODHnryv.js +67 -0
  22. package/build/src/internal/adapter_policies.d.ts +31 -0
  23. package/build/src/internal/cockatiel_factories.d.ts +18 -0
  24. package/build/src/internal/telemetry.d.ts +50 -0
  25. package/build/src/main.d.ts +176 -0
  26. package/build/src/main.js +1125 -0
  27. package/build/src/memory-DWyezb1O.js +37 -0
  28. package/build/src/memory-DXkg8s6y.js +60 -0
  29. package/build/src/plugin.d.ts +30 -0
  30. package/build/src/policy_configurator.d.ts +108 -0
  31. package/build/src/semaphore.d.ts +71 -0
  32. package/build/src/tenace_builder.d.ts +22 -0
  33. package/build/src/tenace_policy.d.ts +41 -0
  34. package/build/src/types/backoff.d.ts +57 -0
  35. package/build/src/types/collection.d.ts +46 -0
  36. package/build/src/types/main.d.ts +5 -0
  37. package/build/src/types/main.js +1 -0
  38. package/build/src/types/plugin.d.ts +61 -0
  39. package/build/src/types/types.d.ts +241 -0
  40. package/build/src/wait_for.d.ts +23 -0
  41. package/package.json +135 -0
@@ -0,0 +1,1125 @@
1
+ import { a as CircuitOpenError, i as CircuitIsolatedError, l as TimeoutError$1, n as BulkheadFullError, o as LockNotAcquiredError, r as CancelledError, s as RateLimitError, u as WaitForTimeoutError } from "./errors-BODHnryv.js";
2
+ import { t as MemoryRateLimiterAdapter } from "./memory-DXkg8s6y.js";
3
+ import { t as MemoryCacheAdapter } from "./memory-DWyezb1O.js";
4
+ import pWaitFor, { TimeoutError } from "p-wait-for";
5
+ import { ms } from "@julr/utils/string/ms";
6
+ import { BrokenCircuitError, BulkheadRejectedError, CircuitState, ConsecutiveBreaker, CountBreaker, DelegateBackoff, IsolatedCircuitError, SamplingBreaker, TaskCancelledError, TimeoutStrategy, bulkhead, circuitBreaker, fallback, handleAll, handleWhen, retry, timeout, wrap } from "cockatiel";
7
+ function parseDuration(duration) {
8
+ return ms.parse(duration);
9
+ }
10
+ async function waitFor(condition, options = {}) {
11
+ const intervalMs = options.interval ? parseDuration(options.interval) : 100;
12
+ const timeoutMs = options.timeout ? parseDuration(options.timeout) : 1e4;
13
+ try {
14
+ await pWaitFor(condition, {
15
+ interval: intervalMs,
16
+ timeout: timeoutMs,
17
+ ...options.before !== void 0 && { before: options.before },
18
+ ...options.signal !== void 0 && { signal: options.signal }
19
+ });
20
+ } catch (error) {
21
+ if (error instanceof TimeoutError) throw new WaitForTimeoutError(options.message);
22
+ throw error;
23
+ }
24
+ }
25
+ let otelApi;
26
+ async function loadOtelApi() {
27
+ if (otelApi) return otelApi;
28
+ try {
29
+ otelApi = await import("@opentelemetry/api");
30
+ return otelApi;
31
+ } catch {
32
+ return;
33
+ }
34
+ }
35
+ function getActiveSpan() {
36
+ return otelApi?.trace.getActiveSpan();
37
+ }
38
+ function emitEvent(name, attributes) {
39
+ const span = getActiveSpan();
40
+ if (!span) return;
41
+ span.addEvent(name, attributes);
42
+ }
43
+ function recordSpan(name, fn, attributes) {
44
+ if (!otelApi) return fn();
45
+ return otelApi.trace.getTracer("tenace").startActiveSpan(name, attributes ? { attributes } : {}, (span) => {
46
+ try {
47
+ const result = fn();
48
+ if (result instanceof Promise) return result.then((value) => {
49
+ span.setStatus({ code: otelApi.SpanStatusCode.OK });
50
+ span.end();
51
+ return value;
52
+ }).catch((error) => {
53
+ span.setStatus({
54
+ code: otelApi.SpanStatusCode.ERROR,
55
+ message: error instanceof Error ? error.message : String(error)
56
+ });
57
+ span.recordException(error instanceof Error ? error : new Error(String(error)));
58
+ span.end();
59
+ throw error;
60
+ });
61
+ span.setStatus({ code: otelApi.SpanStatusCode.OK });
62
+ span.end();
63
+ return result;
64
+ } catch (error) {
65
+ span.setStatus({
66
+ code: otelApi.SpanStatusCode.ERROR,
67
+ message: error instanceof Error ? error.message : String(error)
68
+ });
69
+ span.recordException(error instanceof Error ? error : new Error(String(error)));
70
+ span.end();
71
+ throw error;
72
+ }
73
+ });
74
+ }
75
+ const TelemetryEvents = {
76
+ RETRY_ATTEMPT: "tenace.retry",
77
+ RETRY_SUCCESS: "tenace.retry.success",
78
+ RETRY_EXHAUSTED: "tenace.retry.exhausted",
79
+ TIMEOUT: "tenace.timeout",
80
+ CIRCUIT_OPEN: "tenace.circuit_breaker.open",
81
+ CIRCUIT_CLOSE: "tenace.circuit_breaker.close",
82
+ CIRCUIT_HALF_OPEN: "tenace.circuit_breaker.half_open",
83
+ BULKHEAD_REJECTED: "tenace.bulkhead.rejected",
84
+ CACHE_HIT: "tenace.cache.hit",
85
+ CACHE_MISS: "tenace.cache.miss",
86
+ RATE_LIMIT_REJECTED: "tenace.rate_limit.rejected",
87
+ LOCK_ACQUIRED: "tenace.lock.acquired",
88
+ LOCK_NOT_ACQUIRED: "tenace.lock.not_acquired",
89
+ LOCK_RELEASED: "tenace.lock.released",
90
+ FALLBACK_TRIGGERED: "tenace.fallback"
91
+ };
92
+ const plugins = [];
93
+ function use(plugin) {
94
+ plugins.push(plugin);
95
+ }
96
+ function getPlugins() {
97
+ return plugins;
98
+ }
99
+ function clearPlugins() {
100
+ plugins.length = 0;
101
+ }
102
+ function registerHook(hook, fn) {
103
+ const plugin = { [hook]: fn };
104
+ use(plugin);
105
+ return () => {
106
+ const index = plugins.indexOf(plugin);
107
+ if (index !== -1) plugins.splice(index, 1);
108
+ };
109
+ }
110
+ function notifyPlugins(hook, event) {
111
+ for (const plugin of plugins) {
112
+ const handler = plugin[hook];
113
+ if (typeof handler === "function") handler(event);
114
+ }
115
+ }
116
+ function wrapError(error) {
117
+ if (error instanceof IsolatedCircuitError) return new CircuitIsolatedError(error.message);
118
+ if (error instanceof BrokenCircuitError) return new CircuitOpenError(error.message);
119
+ if (error instanceof TaskCancelledError) {
120
+ if (error.message.includes("timed out")) return new TimeoutError$1(error.message);
121
+ return new CancelledError(error.message);
122
+ }
123
+ if (error instanceof BulkheadRejectedError) return new BulkheadFullError(error.message);
124
+ if (error instanceof Error) return error;
125
+ return new Error(String(error));
126
+ }
127
+ function createBreaker(config) {
128
+ switch (config.kind) {
129
+ case "consecutive": return new ConsecutiveBreaker(config.threshold);
130
+ case "count": {
131
+ const opts = {
132
+ threshold: config.threshold,
133
+ size: config.size
134
+ };
135
+ if (config.minimumNumberOfCalls !== void 0) opts.minimumNumberOfCalls = config.minimumNumberOfCalls;
136
+ return new CountBreaker(opts);
137
+ }
138
+ case "sampling": {
139
+ const opts = {
140
+ threshold: config.threshold,
141
+ duration: config.durationMs
142
+ };
143
+ if (config.minimumRps !== void 0) opts.minimumRps = config.minimumRps;
144
+ return new SamplingBreaker(opts);
145
+ }
146
+ }
147
+ }
148
+ function createCircuitBreakerPolicy(config) {
149
+ const cb = circuitBreaker(handleAll, {
150
+ halfOpenAfter: config.halfOpenAfterMs,
151
+ breaker: createBreaker(config.breaker)
152
+ });
153
+ if (config.hooks) {
154
+ cb.onBreak(() => {
155
+ config.hooks.onOpen?.();
156
+ config.hooks.onStateChange?.("open");
157
+ });
158
+ cb.onReset(() => {
159
+ config.hooks.onClose?.();
160
+ config.hooks.onStateChange?.("closed");
161
+ });
162
+ cb.onHalfOpen(() => {
163
+ config.hooks.onHalfOpen?.();
164
+ config.hooks.onStateChange?.("half-open");
165
+ });
166
+ }
167
+ cb.onBreak(() => {
168
+ emitEvent(TelemetryEvents.CIRCUIT_OPEN);
169
+ notifyPlugins("onCircuitOpened", {});
170
+ });
171
+ cb.onReset(() => {
172
+ emitEvent(TelemetryEvents.CIRCUIT_CLOSE);
173
+ notifyPlugins("onCircuitClosed", {});
174
+ });
175
+ cb.onHalfOpen(() => {
176
+ emitEvent(TelemetryEvents.CIRCUIT_HALF_OPEN);
177
+ notifyPlugins("onCircuitHalfOpened", {});
178
+ });
179
+ return cb;
180
+ }
181
+ function createRetryPolicy(config) {
182
+ const times = config.times ?? 3;
183
+ const delay = config.delay;
184
+ const retryOpts = { maxAttempts: times };
185
+ if (delay !== void 0) {
186
+ const delayFn = typeof delay === "function" ? delay : () => parseDuration(delay);
187
+ retryOpts.backoff = new DelegateBackoff((context) => delayFn(context.attempt, context.result.error));
188
+ }
189
+ const retryIf = config.retryIf;
190
+ const abortIf = config.abortIf;
191
+ const policy = retry(handleWhen((error) => {
192
+ if (error instanceof BrokenCircuitError) return false;
193
+ if (error instanceof BulkheadRejectedError) return false;
194
+ if (!(error instanceof Error)) return true;
195
+ if (abortIf?.(error)) return false;
196
+ if (retryIf) return retryIf(error);
197
+ return true;
198
+ }), retryOpts);
199
+ policy.onRetry((event) => {
200
+ const error = "error" in event ? event.error : /* @__PURE__ */ new Error("Unknown error");
201
+ emitEvent(TelemetryEvents.RETRY_ATTEMPT, {
202
+ attempt: event.attempt,
203
+ delay_ms: event.delay,
204
+ error: error.message
205
+ });
206
+ notifyPlugins("onRetry", {
207
+ attempt: event.attempt,
208
+ delay: event.delay,
209
+ error
210
+ });
211
+ config.onRetry?.({
212
+ attempt: event.attempt,
213
+ delay: event.delay,
214
+ error
215
+ });
216
+ });
217
+ policy.onGiveUp((event) => {
218
+ const error = "error" in event ? event.error : /* @__PURE__ */ new Error("Unknown error");
219
+ emitEvent(TelemetryEvents.RETRY_EXHAUSTED, { error: error.message });
220
+ notifyPlugins("onRetryExhausted", { error });
221
+ config.onRetryExhausted?.({ error });
222
+ });
223
+ return policy;
224
+ }
225
+ var PolicyConfigurator = class {
226
+ layers = [];
227
+ #addLayer(layer) {
228
+ return this.createInstance([...this.layers, layer]);
229
+ }
230
+ withTimeout(duration, strategyOrOptions) {
231
+ const isOptions = typeof strategyOrOptions === "object";
232
+ const strategy = isOptions ? strategyOrOptions.strategy ?? "cooperative" : strategyOrOptions ?? "cooperative";
233
+ const onTimeout = isOptions ? strategyOrOptions.onTimeout : void 0;
234
+ const config = {
235
+ durationMs: parseDuration(duration),
236
+ strategy
237
+ };
238
+ if (onTimeout) config.onTimeout = onTimeout;
239
+ return this.#addLayer({
240
+ type: "timeout",
241
+ config
242
+ });
243
+ }
244
+ withRetry(options) {
245
+ return this.#addLayer({
246
+ type: "retry",
247
+ config: options ?? {}
248
+ });
249
+ }
250
+ withCircuitBreaker(options) {
251
+ const config = {
252
+ halfOpenAfterMs: parseDuration(options.halfOpenAfter),
253
+ breaker: {
254
+ kind: "consecutive",
255
+ threshold: options.failureThreshold
256
+ }
257
+ };
258
+ if (options.hooks) config.hooks = options.hooks;
259
+ const instance = createCircuitBreakerPolicy(config);
260
+ return this.#addLayer({
261
+ type: "circuitBreaker",
262
+ config,
263
+ instance
264
+ });
265
+ }
266
+ withSamplingCircuitBreaker(options) {
267
+ const durationMs = parseDuration(options.duration);
268
+ const halfOpenAfterMs = parseDuration(options.halfOpenAfter);
269
+ const breaker = {
270
+ kind: "sampling",
271
+ threshold: options.threshold,
272
+ durationMs
273
+ };
274
+ if (options.minimumRps !== void 0) breaker.minimumRps = options.minimumRps;
275
+ const config = {
276
+ halfOpenAfterMs,
277
+ breaker
278
+ };
279
+ if (options.hooks) config.hooks = options.hooks;
280
+ const instance = createCircuitBreakerPolicy(config);
281
+ return this.#addLayer({
282
+ type: "circuitBreaker",
283
+ config,
284
+ instance
285
+ });
286
+ }
287
+ withFallback(fn, options) {
288
+ const config = { fn };
289
+ if (options?.onFallback) config.onFallback = options.onFallback;
290
+ return this.#addLayer({
291
+ type: "fallback",
292
+ config
293
+ });
294
+ }
295
+ withSpan(name, attributes) {
296
+ const config = { name };
297
+ if (attributes) config.attributes = attributes;
298
+ return this.#addLayer({
299
+ type: "span",
300
+ config
301
+ });
302
+ }
303
+ withBulkhead(limit, queueOrOptions) {
304
+ const isOptions = typeof queueOrOptions === "object";
305
+ const queue = isOptions ? queueOrOptions.queue : queueOrOptions;
306
+ const onRejected = isOptions ? queueOrOptions.onRejected : void 0;
307
+ const config = { limit };
308
+ if (queue !== void 0) config.queue = queue;
309
+ if (onRejected) config.onRejected = onRejected;
310
+ const instance = bulkhead(config.limit, config.queue);
311
+ return this.#addLayer({
312
+ type: "bulkhead",
313
+ config,
314
+ instance
315
+ });
316
+ }
317
+ withCache(options) {
318
+ return this.#addLayer({
319
+ type: "cache",
320
+ config: options
321
+ });
322
+ }
323
+ withRateLimit(options) {
324
+ return this.#addLayer({
325
+ type: "rateLimit",
326
+ config: options
327
+ });
328
+ }
329
+ withDistributedLock(options) {
330
+ return this.#addLayer({
331
+ type: "distributedLock",
332
+ config: options
333
+ });
334
+ }
335
+ withChaosFault(options) {
336
+ const config = {
337
+ rate: options.rate,
338
+ error: options.error,
339
+ errors: options.errors
340
+ };
341
+ return this.#addLayer({
342
+ type: "chaosFault",
343
+ config
344
+ });
345
+ }
346
+ withChaosLatency(options) {
347
+ const delay = typeof options.delay === "number" ? {
348
+ min: options.delay,
349
+ max: options.delay
350
+ } : options.delay;
351
+ const config = {
352
+ rate: options.rate,
353
+ delay
354
+ };
355
+ return this.#addLayer({
356
+ type: "chaosLatency",
357
+ config
358
+ });
359
+ }
360
+ };
361
+ var ConfigStore = class {
362
+ #cache;
363
+ #rateLimiter;
364
+ #lock;
365
+ #defaultCache;
366
+ #defaultRateLimiter;
367
+ configure(config) {
368
+ if (config.cache) this.#cache = config.cache;
369
+ if (config.rateLimiter) this.#rateLimiter = config.rateLimiter;
370
+ if (config.lock) this.#lock = config.lock;
371
+ }
372
+ getCache() {
373
+ if (this.#cache) return this.#cache;
374
+ if (!this.#defaultCache) this.#defaultCache = new MemoryCacheAdapter();
375
+ return this.#defaultCache;
376
+ }
377
+ getRateLimiter() {
378
+ if (this.#rateLimiter) return this.#rateLimiter;
379
+ if (!this.#defaultRateLimiter) this.#defaultRateLimiter = new MemoryRateLimiterAdapter();
380
+ return this.#defaultRateLimiter;
381
+ }
382
+ getLock() {
383
+ return this.#lock;
384
+ }
385
+ reset() {
386
+ this.#cache = void 0;
387
+ this.#rateLimiter = void 0;
388
+ this.#lock = void 0;
389
+ this.#defaultCache = void 0;
390
+ this.#defaultRateLimiter = void 0;
391
+ }
392
+ };
393
+ const configStore = new ConfigStore();
394
+ function createCachePolicy(options) {
395
+ const adapter = options.adapter ?? configStore.getCache();
396
+ return { execute: async (fn, signal) => {
397
+ try {
398
+ const cached = await adapter.get(options.key);
399
+ if (cached !== void 0) {
400
+ emitEvent(TelemetryEvents.CACHE_HIT, { key: options.key });
401
+ notifyPlugins("onCacheHit", { key: options.key });
402
+ options.onHit?.({ key: options.key });
403
+ return cached;
404
+ }
405
+ emitEvent(TelemetryEvents.CACHE_MISS, { key: options.key });
406
+ notifyPlugins("onCacheMiss", { key: options.key });
407
+ options.onMiss?.({ key: options.key });
408
+ } catch (error) {
409
+ if (!options.optional) throw error;
410
+ }
411
+ const result = await fn({
412
+ signal: signal ?? new AbortController().signal,
413
+ attempt: 0
414
+ });
415
+ try {
416
+ await adapter.set(options.key, result, options.ttl);
417
+ } catch {}
418
+ return result;
419
+ } };
420
+ }
421
+ function createRateLimitPolicy(options) {
422
+ const adapter = options.adapter ?? configStore.getRateLimiter();
423
+ return { execute: async (fn, signal) => {
424
+ try {
425
+ const result = await adapter.acquire(options.key, {
426
+ maxCalls: options.maxCalls,
427
+ windowMs: options.windowMs,
428
+ ...options.strategy !== void 0 && { strategy: options.strategy }
429
+ });
430
+ if (!result.allowed) {
431
+ emitEvent(TelemetryEvents.RATE_LIMIT_REJECTED, {
432
+ key: options.key,
433
+ retry_after_ms: result.retryAfterMs ?? 0
434
+ });
435
+ notifyPlugins("onRateLimitRejected", {
436
+ key: options.key,
437
+ retryAfterMs: result.retryAfterMs
438
+ });
439
+ if (result.retryAfterMs !== void 0) options.onRejected?.({
440
+ key: options.key,
441
+ retryAfterMs: result.retryAfterMs
442
+ });
443
+ else options.onRejected?.({ key: options.key });
444
+ throw new RateLimitError({
445
+ message: `Rate limit exceeded for key "${options.key}". Retry after ${result.retryAfterMs}ms`,
446
+ retryAfterMs: result.retryAfterMs ?? 0,
447
+ remaining: result.remaining
448
+ });
449
+ }
450
+ } catch (error) {
451
+ if (error instanceof RateLimitError) throw error;
452
+ if (!options.optional) throw error;
453
+ }
454
+ return fn({
455
+ signal: signal ?? new AbortController().signal,
456
+ attempt: 0
457
+ });
458
+ } };
459
+ }
460
+ function createLockPolicy(options) {
461
+ const adapter = options.adapter ?? configStore.getLock();
462
+ if (!adapter) throw new Error("No lock adapter configured. Set one globally with configStore.configure({ lock }) or pass it in options.");
463
+ return { execute: async (fn, signal) => {
464
+ const context = {
465
+ signal: signal ?? new AbortController().signal,
466
+ attempt: 0
467
+ };
468
+ const [acquired, result] = await adapter.run(options.key, options.ttl, () => fn(context), options.retry ? { retry: options.retry } : {});
469
+ if (!acquired) {
470
+ emitEvent(TelemetryEvents.LOCK_NOT_ACQUIRED, { key: options.key });
471
+ notifyPlugins("onLockRejected", { key: options.key });
472
+ options.onRejected?.({ key: options.key });
473
+ throw new LockNotAcquiredError({ key: options.key });
474
+ }
475
+ emitEvent(TelemetryEvents.LOCK_ACQUIRED, { key: options.key });
476
+ notifyPlugins("onLockAcquired", { key: options.key });
477
+ options.onAcquired?.({ key: options.key });
478
+ return result;
479
+ } };
480
+ }
481
+ function createChaosFaultPolicy(config) {
482
+ return { execute: async (fn, signal) => {
483
+ if (Math.random() < config.rate) throw config.errors ? config.errors[Math.floor(Math.random() * config.errors.length)] : config.error ?? /* @__PURE__ */ new Error("Chaos fault");
484
+ return fn({ signal: signal ?? new AbortController().signal });
485
+ } };
486
+ }
487
+ function createChaosLatencyPolicy(config) {
488
+ return { execute: async (fn, signal) => {
489
+ if (Math.random() < config.rate) await sleep(config.delay.min === config.delay.max ? config.delay.min : Math.floor(Math.random() * (config.delay.max - config.delay.min) + config.delay.min));
490
+ return fn({ signal: signal ?? new AbortController().signal });
491
+ } };
492
+ }
493
+ function sleep(ms$1) {
494
+ return new Promise((resolve) => setTimeout(resolve, ms$1));
495
+ }
496
+ const DEFAULT_CHAOS_ERROR = /* @__PURE__ */ new Error("Chaos fault");
497
+ var ChaosManager = class {
498
+ #config = null;
499
+ enable(config) {
500
+ this.#config = this.#normalizeConfig(config);
501
+ }
502
+ disable() {
503
+ this.#config = null;
504
+ }
505
+ isEnabled() {
506
+ return this.#config !== null;
507
+ }
508
+ getConfig() {
509
+ return this.#config;
510
+ }
511
+ #normalizeConfig(config) {
512
+ const result = {};
513
+ if (config.fault !== void 0) result.fault = this.#normalizeFaultConfig(config.fault);
514
+ if (config.latency !== void 0) result.latency = this.#normalizeLatencyConfig(config.latency);
515
+ return result;
516
+ }
517
+ #normalizeFaultConfig(config) {
518
+ if (typeof config === "number") return {
519
+ rate: config,
520
+ error: DEFAULT_CHAOS_ERROR
521
+ };
522
+ return {
523
+ rate: config.rate,
524
+ error: config.error,
525
+ errors: config.errors
526
+ };
527
+ }
528
+ #normalizeLatencyConfig(config) {
529
+ if (typeof config === "number") return {
530
+ rate: 1,
531
+ delay: {
532
+ min: config,
533
+ max: config
534
+ }
535
+ };
536
+ const delay = config.delay;
537
+ if (typeof delay === "number") return {
538
+ rate: config.rate,
539
+ delay: {
540
+ min: delay,
541
+ max: delay
542
+ }
543
+ };
544
+ return {
545
+ rate: config.rate,
546
+ delay
547
+ };
548
+ }
549
+ };
550
+ const chaosManager = new ChaosManager();
551
+ var TenaceBuilder = class TenaceBuilder extends PolicyConfigurator {
552
+ #fn;
553
+ constructor(options = {}) {
554
+ super();
555
+ this.#fn = options.fn;
556
+ this.layers = options.layers ?? [];
557
+ }
558
+ createInstance(layers) {
559
+ return new TenaceBuilder({
560
+ fn: this.#fn,
561
+ layers
562
+ });
563
+ }
564
+ async execute(fn) {
565
+ const targetFn = fn ?? this.#fn;
566
+ if (!targetFn) throw new Error("No function provided to execute");
567
+ const policy = this.#buildOrderedPolicy();
568
+ const wrappedFn = this.#wrapFn(targetFn);
569
+ try {
570
+ const spanLayer = this.layers.find((l) => l.type === "span");
571
+ if (spanLayer) return await this.#executeWithSpan(policy, wrappedFn, spanLayer.config);
572
+ return await policy.execute(wrappedFn);
573
+ } catch (error) {
574
+ const wrappedError = wrapError(error);
575
+ this.#emitErrorHooks(wrappedError);
576
+ throw wrappedError;
577
+ }
578
+ }
579
+ #buildOrderedPolicy() {
580
+ const pipelineLayers = this.#mergeWithGlobalChaos(this.layers).filter((l) => [
581
+ "timeout",
582
+ "retry",
583
+ "circuitBreaker",
584
+ "bulkhead",
585
+ "fallback",
586
+ "chaosFault",
587
+ "chaosLatency",
588
+ "cache",
589
+ "rateLimit",
590
+ "distributedLock"
591
+ ].includes(l.type));
592
+ if (pipelineLayers.length === 0) return this.#createNoopPolicy();
593
+ const policies = pipelineLayers.map((layer) => this.#createCockatielPolicy(layer));
594
+ if (policies.length === 1) return policies[0];
595
+ return wrap(...[...policies].reverse());
596
+ }
597
+ #mergeWithGlobalChaos(layers) {
598
+ const globalConfig = chaosManager.getConfig();
599
+ if (!globalConfig) return layers;
600
+ const result = [...layers];
601
+ if (globalConfig.latency && !layers.some((l) => l.type === "chaosLatency")) result.unshift({
602
+ type: "chaosLatency",
603
+ config: globalConfig.latency
604
+ });
605
+ if (globalConfig.fault && !layers.some((l) => l.type === "chaosFault")) result.unshift({
606
+ type: "chaosFault",
607
+ config: globalConfig.fault
608
+ });
609
+ return result;
610
+ }
611
+ #createNoopPolicy() {
612
+ return { execute: (fn) => Promise.resolve(fn({
613
+ signal: new AbortController().signal,
614
+ attempt: 0
615
+ })) };
616
+ }
617
+ #createCockatielPolicy(layer) {
618
+ if (layer.instance) return layer.instance;
619
+ switch (layer.type) {
620
+ case "timeout": {
621
+ const config = layer.config;
622
+ const strategy = config.strategy === "aggressive" ? TimeoutStrategy.Aggressive : TimeoutStrategy.Cooperative;
623
+ return timeout(config.durationMs, strategy);
624
+ }
625
+ case "retry": return createRetryPolicy(layer.config);
626
+ case "circuitBreaker": return createCircuitBreakerPolicy(layer.config);
627
+ case "bulkhead": {
628
+ const config = layer.config;
629
+ return bulkhead(config.limit, config.queue);
630
+ }
631
+ case "fallback": {
632
+ const config = layer.config;
633
+ const fallbackFn = () => {
634
+ emitEvent(TelemetryEvents.FALLBACK_TRIGGERED);
635
+ notifyPlugins("onFallback", {});
636
+ config.onFallback?.();
637
+ return config.fn();
638
+ };
639
+ return fallback(handleAll, fallbackFn);
640
+ }
641
+ case "chaosFault": return createChaosFaultPolicy(layer.config);
642
+ case "chaosLatency": return createChaosLatencyPolicy(layer.config);
643
+ case "cache": return createCachePolicy(layer.config);
644
+ case "rateLimit": return createRateLimitPolicy(layer.config);
645
+ case "distributedLock": return createLockPolicy(layer.config);
646
+ default: throw new Error(`Unknown policy type: ${layer.type}`);
647
+ }
648
+ }
649
+ #wrapFn(fn) {
650
+ return (ctx) => fn({
651
+ signal: ctx.signal,
652
+ attempt: ctx.attempt ?? 0
653
+ });
654
+ }
655
+ #emitErrorHooks(error) {
656
+ if (error instanceof TimeoutError$1) {
657
+ emitEvent(TelemetryEvents.TIMEOUT);
658
+ notifyPlugins("onTimeout", {});
659
+ const timeoutLayer = this.layers.find((l) => l.type === "timeout");
660
+ if (timeoutLayer) timeoutLayer.config.onTimeout?.();
661
+ } else if (error instanceof BulkheadFullError) {
662
+ emitEvent(TelemetryEvents.BULKHEAD_REJECTED);
663
+ notifyPlugins("onBulkheadRejected", {});
664
+ const bulkheadLayer = this.layers.find((l) => l.type === "bulkhead");
665
+ if (bulkheadLayer) bulkheadLayer.config.onRejected?.();
666
+ }
667
+ }
668
+ async #executeWithSpan(policy, fn, spanConfig) {
669
+ const otelApi$1 = await loadOtelApi();
670
+ if (!otelApi$1) return policy.execute(fn);
671
+ return otelApi$1.trace.getTracer("tenace").startActiveSpan(spanConfig.name, async (span) => {
672
+ try {
673
+ if (spanConfig.attributes) for (const [key, value] of Object.entries(spanConfig.attributes)) span.setAttribute(key, value);
674
+ this.#addPolicyAttributes(span);
675
+ const wrappedFn = this.layers.some((l) => l.type === "retry") ? this.#wrapWithAttemptSpans(fn) : fn;
676
+ const result = await policy.execute(wrappedFn);
677
+ span.setStatus({ code: otelApi$1.SpanStatusCode.OK });
678
+ return result;
679
+ } catch (error) {
680
+ const wrappedError = wrapError(error);
681
+ this.#emitErrorHooks(wrappedError);
682
+ span.setStatus({
683
+ code: otelApi$1.SpanStatusCode.ERROR,
684
+ message: wrappedError instanceof Error ? wrappedError.message : "Unknown error"
685
+ });
686
+ span.recordException(wrappedError instanceof Error ? wrappedError : new Error(String(wrappedError)));
687
+ throw wrappedError;
688
+ } finally {
689
+ span.end();
690
+ }
691
+ });
692
+ }
693
+ #wrapWithAttemptSpans(fn) {
694
+ return (ctx) => recordSpan("tenace.retry.attempt", () => fn(ctx), { "tenace.attempt": ctx.attempt ?? 0 });
695
+ }
696
+ #addPolicyAttributes(span) {
697
+ const policies = this.layers.filter((l) => l.type !== "span").map((l) => l.type);
698
+ if (policies.length > 0) span.setAttribute("tenace.policies", policies.join(","));
699
+ for (const layer of this.layers) switch (layer.type) {
700
+ case "retry": {
701
+ const config = layer.config;
702
+ span.setAttribute("tenace.retry.max_attempts", config.times ?? 3);
703
+ break;
704
+ }
705
+ case "timeout": {
706
+ const config = layer.config;
707
+ span.setAttribute("tenace.timeout.duration_ms", config.durationMs);
708
+ break;
709
+ }
710
+ case "bulkhead": {
711
+ const config = layer.config;
712
+ span.setAttribute("tenace.bulkhead.limit", config.limit);
713
+ break;
714
+ }
715
+ }
716
+ }
717
+ wrap(fn) {
718
+ return () => this.execute(fn);
719
+ }
720
+ };
721
+ var TenacePolicy = class TenacePolicy extends PolicyConfigurator {
722
+ #resolvedLayers = null;
723
+ constructor(layers = []) {
724
+ super();
725
+ this.layers = layers;
726
+ }
727
+ createInstance(layers) {
728
+ return new TenacePolicy(layers);
729
+ }
730
+ #resolveSharedLayers() {
731
+ if (this.#resolvedLayers) return this.#resolvedLayers;
732
+ this.#resolvedLayers = this.layers.map((layer) => {
733
+ if (layer.type === "circuitBreaker" && !layer.instance) return {
734
+ ...layer,
735
+ instance: createCircuitBreakerPolicy(layer.config)
736
+ };
737
+ if (layer.type === "bulkhead" && !layer.instance) {
738
+ const config = layer.config;
739
+ return {
740
+ ...layer,
741
+ instance: bulkhead(config.limit, config.queue)
742
+ };
743
+ }
744
+ return layer;
745
+ });
746
+ return this.#resolvedLayers;
747
+ }
748
+ get circuitBreaker() {
749
+ const cbLayer = this.#resolveSharedLayers().find((l) => l.type === "circuitBreaker");
750
+ if (!cbLayer?.instance) return null;
751
+ const cb = cbLayer.instance;
752
+ return {
753
+ get state() {
754
+ if (cb.state === CircuitState.Open) return "open";
755
+ if (cb.state === CircuitState.HalfOpen) return "half-open";
756
+ return "closed";
757
+ },
758
+ get isOpen() {
759
+ return cb.state === CircuitState.Open;
760
+ },
761
+ get isClosed() {
762
+ return cb.state === CircuitState.Closed;
763
+ },
764
+ get isHalfOpen() {
765
+ return cb.state === CircuitState.HalfOpen;
766
+ },
767
+ isolate() {
768
+ return cb.isolate();
769
+ }
770
+ };
771
+ }
772
+ wrap(fn) {
773
+ return () => this.call(fn).execute();
774
+ }
775
+ call(fn) {
776
+ return new TenaceBuilder({
777
+ fn,
778
+ layers: this.#resolveSharedLayers()
779
+ });
780
+ }
781
+ };
782
+ var Semaphore = class {
783
+ #permits;
784
+ #queue = [];
785
+ #activeCount = 0;
786
+ constructor(permits) {
787
+ if (permits < 1) throw new Error("Semaphore permits must be at least 1");
788
+ this.#permits = permits;
789
+ }
790
+ get availablePermits() {
791
+ return this.#permits - this.#activeCount;
792
+ }
793
+ get activeCount() {
794
+ return this.#activeCount;
795
+ }
796
+ get pendingCount() {
797
+ return this.#queue.length;
798
+ }
799
+ get permits() {
800
+ return this.#permits;
801
+ }
802
+ async acquire() {
803
+ return new Promise((resolve) => {
804
+ const tryAcquire = () => {
805
+ if (this.#activeCount < this.#permits) {
806
+ this.#activeCount++;
807
+ resolve(() => this.#release());
808
+ } else this.#queue.push(tryAcquire);
809
+ };
810
+ tryAcquire();
811
+ });
812
+ }
813
+ #release() {
814
+ this.#activeCount--;
815
+ const next = this.#queue.shift();
816
+ if (next) next();
817
+ }
818
+ async run(fn) {
819
+ const release = await this.acquire();
820
+ try {
821
+ return await fn();
822
+ } finally {
823
+ release();
824
+ }
825
+ }
826
+ wrap(fn) {
827
+ return async (...args) => {
828
+ return this.run(() => fn(...args));
829
+ };
830
+ }
831
+ async map(items, fn) {
832
+ const itemsArray = Array.from(items);
833
+ return Promise.all(itemsArray.map((item, index) => this.run(() => fn(item, index))));
834
+ }
835
+ clearQueue() {
836
+ this.#queue = [];
837
+ }
838
+ };
839
+ function semaphore(permits) {
840
+ return new Semaphore(permits);
841
+ }
842
+ var CollectionBuilder = class {
843
+ #tasks;
844
+ #concurrency = Infinity;
845
+ #retryAttempts = 0;
846
+ #retryOptions;
847
+ #timeoutMs;
848
+ #timeoutStrategy = "cooperative";
849
+ #stopOnFirstError = true;
850
+ #signal;
851
+ #onTaskComplete;
852
+ #onTaskError;
853
+ #onProgress;
854
+ constructor(tasks) {
855
+ this.#tasks = tasks;
856
+ }
857
+ withConcurrency(limit) {
858
+ this.#concurrency = limit;
859
+ return this;
860
+ }
861
+ withRetryPerTask(attempts, options) {
862
+ this.#retryAttempts = attempts;
863
+ if (options) this.#retryOptions = options;
864
+ return this;
865
+ }
866
+ withTimeoutPerTask(duration, strategy = "cooperative") {
867
+ this.#timeoutMs = parseDuration(duration);
868
+ this.#timeoutStrategy = strategy;
869
+ return this;
870
+ }
871
+ stopOnError(stop) {
872
+ this.#stopOnFirstError = stop;
873
+ return this;
874
+ }
875
+ withSignal(signal) {
876
+ this.#signal = signal;
877
+ return this;
878
+ }
879
+ onTaskComplete(callback) {
880
+ this.#onTaskComplete = callback;
881
+ return this;
882
+ }
883
+ onTaskError(callback) {
884
+ this.#onTaskError = callback;
885
+ return this;
886
+ }
887
+ onProgress(callback) {
888
+ this.#onProgress = callback;
889
+ return this;
890
+ }
891
+ async execute() {
892
+ const results = await this.#run();
893
+ const firstError = results.find((r) => r.status === "rejected");
894
+ if (firstError && firstError.status === "rejected") throw firstError.reason;
895
+ return results.map((r) => {
896
+ if (r.status === "fulfilled") return r.value;
897
+ throw r.reason;
898
+ });
899
+ }
900
+ async settle() {
901
+ const originalStopOnError = this.#stopOnFirstError;
902
+ this.#stopOnFirstError = false;
903
+ const results = await this.#run();
904
+ this.#stopOnFirstError = originalStopOnError;
905
+ return results;
906
+ }
907
+ async #run() {
908
+ const total = this.#tasks.length;
909
+ if (total === 0) return [];
910
+ const results = Array.from({ length: total });
911
+ let completed = 0;
912
+ let failed = 0;
913
+ let aborted = false;
914
+ const semaphore$1 = new Semaphore(this.#concurrency === Infinity ? total : this.#concurrency);
915
+ if (this.#signal?.aborted) {
916
+ const error = /* @__PURE__ */ new Error("Operation aborted");
917
+ error.name = "AbortError";
918
+ throw error;
919
+ }
920
+ const abortHandler = () => {
921
+ aborted = true;
922
+ };
923
+ this.#signal?.addEventListener("abort", abortHandler);
924
+ const executeTask = async (task, index) => {
925
+ if (aborted) {
926
+ const error = /* @__PURE__ */ new Error("Operation aborted");
927
+ error.name = "AbortError";
928
+ results[index] = {
929
+ status: "rejected",
930
+ reason: error,
931
+ index
932
+ };
933
+ return;
934
+ }
935
+ try {
936
+ const value = await semaphore$1.run(() => this.#executeWithPolicies(task, index));
937
+ completed++;
938
+ results[index] = {
939
+ status: "fulfilled",
940
+ value,
941
+ index
942
+ };
943
+ const progress = {
944
+ completed,
945
+ failed,
946
+ total,
947
+ index
948
+ };
949
+ this.#onTaskComplete?.(value, progress);
950
+ this.#onProgress?.(progress);
951
+ } catch (error) {
952
+ failed++;
953
+ const err = error instanceof Error ? error : new Error(String(error));
954
+ results[index] = {
955
+ status: "rejected",
956
+ reason: err,
957
+ index
958
+ };
959
+ const progress = {
960
+ completed,
961
+ failed,
962
+ total,
963
+ index
964
+ };
965
+ this.#onTaskError?.(err, progress);
966
+ this.#onProgress?.(progress);
967
+ if (this.#stopOnFirstError) aborted = true;
968
+ }
969
+ };
970
+ await Promise.all(this.#tasks.map((task, index) => executeTask(task, index)));
971
+ this.#signal?.removeEventListener("abort", abortHandler);
972
+ return results;
973
+ }
974
+ async #executeWithPolicies(task, _index) {
975
+ let fn = task;
976
+ if (this.#timeoutMs) {
977
+ const timeoutMs = this.#timeoutMs;
978
+ const strategy = this.#timeoutStrategy;
979
+ fn = () => this.#withTimeout({
980
+ task,
981
+ ms: timeoutMs,
982
+ strategy
983
+ });
984
+ }
985
+ if (this.#retryAttempts > 0) return this.#withRetry(fn);
986
+ return fn();
987
+ }
988
+ async #withTimeout(options) {
989
+ if (options.strategy === "cooperative") return Promise.race([options.task(), new Promise((_, reject) => {
990
+ setTimeout(() => {
991
+ const error = /* @__PURE__ */ new Error(`Operation timed out after ${options.ms}ms`);
992
+ error.name = "TimeoutError";
993
+ reject(error);
994
+ }, options.ms);
995
+ })]);
996
+ const controller = new AbortController();
997
+ const timeoutId = setTimeout(() => controller.abort(), options.ms);
998
+ try {
999
+ const result = await Promise.race([options.task(), new Promise((_, reject) => {
1000
+ controller.signal.addEventListener("abort", () => {
1001
+ const error = /* @__PURE__ */ new Error(`Operation timed out after ${options.ms}ms`);
1002
+ error.name = "TimeoutError";
1003
+ reject(error);
1004
+ });
1005
+ })]);
1006
+ clearTimeout(timeoutId);
1007
+ return result;
1008
+ } catch (error) {
1009
+ clearTimeout(timeoutId);
1010
+ throw error;
1011
+ }
1012
+ }
1013
+ async #withRetry(task) {
1014
+ let lastError;
1015
+ const maxAttempts = this.#retryAttempts + 1;
1016
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) try {
1017
+ return await task();
1018
+ } catch (error) {
1019
+ lastError = error instanceof Error ? error : new Error(String(error));
1020
+ if (this.#retryOptions?.abortIf?.(lastError)) throw lastError;
1021
+ if (attempt < maxAttempts) {
1022
+ if (this.#retryOptions?.retryIf && !this.#retryOptions.retryIf(lastError)) throw lastError;
1023
+ if (this.#retryOptions?.delay !== void 0) {
1024
+ const delay = this.#calculateDelay(attempt, lastError);
1025
+ await this.#sleep(delay);
1026
+ }
1027
+ }
1028
+ }
1029
+ throw lastError;
1030
+ }
1031
+ #calculateDelay(attempt, error) {
1032
+ const delay = this.#retryOptions?.delay;
1033
+ if (delay === void 0) return 0;
1034
+ if (typeof delay === "function") return delay(attempt, error);
1035
+ return parseDuration(delay);
1036
+ }
1037
+ #sleep(ms$1) {
1038
+ return new Promise((resolve) => setTimeout(resolve, ms$1));
1039
+ }
1040
+ };
1041
+ const backoff = {
1042
+ constant(delay) {
1043
+ const ms$1 = parseDuration(delay);
1044
+ return () => ms$1;
1045
+ },
1046
+ exponential(options = {}) {
1047
+ const initial = parseDuration(options.initial ?? 100);
1048
+ const max = parseDuration(options.max ?? 3e4);
1049
+ const exponent = options.exponent ?? 2;
1050
+ return (attempt) => {
1051
+ const delay = initial * Math.pow(exponent, attempt - 1);
1052
+ return Math.min(delay, max);
1053
+ };
1054
+ },
1055
+ exponentialWithJitter(options = {}) {
1056
+ const initial = parseDuration(options.initial ?? 100);
1057
+ const max = parseDuration(options.max ?? 3e4);
1058
+ const exponent = options.exponent ?? 2;
1059
+ const jitter = options.jitter ?? "full";
1060
+ let previousDelay = initial;
1061
+ return (attempt) => {
1062
+ if (jitter === "decorrelated") {
1063
+ const delay = Math.min(max, randomBetween(initial, previousDelay * 3));
1064
+ previousDelay = delay;
1065
+ return delay;
1066
+ }
1067
+ const exponentialDelay = initial * Math.pow(exponent, attempt - 1);
1068
+ const cappedDelay = Math.min(exponentialDelay, max);
1069
+ return Math.floor(Math.random() * cappedDelay);
1070
+ };
1071
+ },
1072
+ linear(options = {}) {
1073
+ const initial = parseDuration(options.initial ?? 100);
1074
+ const step = parseDuration(options.step ?? 100);
1075
+ const max = parseDuration(options.max ?? 3e4);
1076
+ return (attempt) => {
1077
+ const delay = initial + step * (attempt - 1);
1078
+ return Math.min(delay, max);
1079
+ };
1080
+ }
1081
+ };
1082
+ function randomBetween(min, max) {
1083
+ return Math.floor(Math.random() * (max - min + 1)) + min;
1084
+ }
1085
+ const Tenace = {
1086
+ call(fn) {
1087
+ return new TenaceBuilder({ fn });
1088
+ },
1089
+ policy() {
1090
+ return new TenacePolicy();
1091
+ },
1092
+ wrap(fn, policy) {
1093
+ return policy.wrap(fn);
1094
+ },
1095
+ all(tasks) {
1096
+ return new CollectionBuilder(tasks);
1097
+ },
1098
+ map(items, mapper) {
1099
+ return new CollectionBuilder(Array.from(items).map((item, index) => () => mapper(item, index)));
1100
+ },
1101
+ waitFor(condition, options) {
1102
+ return waitFor(condition, options);
1103
+ },
1104
+ chaos: {
1105
+ enable: (config) => chaosManager.enable(config),
1106
+ disable: () => chaosManager.disable(),
1107
+ isEnabled: () => chaosManager.isEnabled()
1108
+ },
1109
+ hooks: {
1110
+ onRetry: (fn) => registerHook("onRetry", fn),
1111
+ onRetryExhausted: (fn) => registerHook("onRetryExhausted", fn),
1112
+ onTimeout: (fn) => registerHook("onTimeout", fn),
1113
+ onCircuitOpened: (fn) => registerHook("onCircuitOpened", fn),
1114
+ onCircuitClosed: (fn) => registerHook("onCircuitClosed", fn),
1115
+ onCircuitHalfOpened: (fn) => registerHook("onCircuitHalfOpened", fn),
1116
+ onBulkheadRejected: (fn) => registerHook("onBulkheadRejected", fn),
1117
+ onCacheHit: (fn) => registerHook("onCacheHit", fn),
1118
+ onCacheMiss: (fn) => registerHook("onCacheMiss", fn),
1119
+ onRateLimitRejected: (fn) => registerHook("onRateLimitRejected", fn),
1120
+ onFallback: (fn) => registerHook("onFallback", fn),
1121
+ onLockAcquired: (fn) => registerHook("onLockAcquired", fn),
1122
+ onLockRejected: (fn) => registerHook("onLockRejected", fn)
1123
+ }
1124
+ };
1125
+ export { CollectionBuilder, Semaphore, Tenace, backoff, clearPlugins, configStore, getPlugins, registerHook, semaphore, use };