@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.
- package/README.md +1034 -0
- package/build/src/adapters/cache/memory.d.ts +23 -0
- package/build/src/adapters/cache/memory.js +2 -0
- package/build/src/adapters/cache/types.d.ts +56 -0
- package/build/src/adapters/cache/types.js +1 -0
- package/build/src/adapters/lock/types.d.ts +104 -0
- package/build/src/adapters/lock/types.js +1 -0
- package/build/src/adapters/rate_limiter/memory.d.ts +14 -0
- package/build/src/adapters/rate_limiter/memory.js +2 -0
- package/build/src/adapters/rate_limiter/types.d.ts +101 -0
- package/build/src/adapters/rate_limiter/types.js +1 -0
- package/build/src/backoff.d.ts +79 -0
- package/build/src/chaos/manager.d.ts +29 -0
- package/build/src/chaos/policies.d.ts +10 -0
- package/build/src/chaos/types.d.ts +75 -0
- package/build/src/collection.d.ts +81 -0
- package/build/src/config.d.ts +38 -0
- package/build/src/errors/errors.d.ts +79 -0
- package/build/src/errors/main.d.ts +1 -0
- package/build/src/errors/main.js +2 -0
- package/build/src/errors-BODHnryv.js +67 -0
- package/build/src/internal/adapter_policies.d.ts +31 -0
- package/build/src/internal/cockatiel_factories.d.ts +18 -0
- package/build/src/internal/telemetry.d.ts +50 -0
- package/build/src/main.d.ts +176 -0
- package/build/src/main.js +1125 -0
- package/build/src/memory-DWyezb1O.js +37 -0
- package/build/src/memory-DXkg8s6y.js +60 -0
- package/build/src/plugin.d.ts +30 -0
- package/build/src/policy_configurator.d.ts +108 -0
- package/build/src/semaphore.d.ts +71 -0
- package/build/src/tenace_builder.d.ts +22 -0
- package/build/src/tenace_policy.d.ts +41 -0
- package/build/src/types/backoff.d.ts +57 -0
- package/build/src/types/collection.d.ts +46 -0
- package/build/src/types/main.d.ts +5 -0
- package/build/src/types/main.js +1 -0
- package/build/src/types/plugin.d.ts +61 -0
- package/build/src/types/types.d.ts +241 -0
- package/build/src/wait_for.d.ts +23 -0
- 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 };
|