- breaking: updated minimum Node version to 22, and cockatiel is now built as an ES module
- breaking: errors surfaced from policies are no longer cast via
as Error; callback and result types now reflectunknownfor thrown values (#97) - feat: add
halfOpenSamplingoption to the circuit breaker (#73) - fix: bulkhead queue starvation when queued executions were aborted (#112)
- fix: propagate the abort signal into the half-open state of the circuit breaker (#112)
- fix: deep copy state in
CountBreakerandSamplingBreakergetters/setters viastructuredClone(#113) - fix: correct type predicates for errors (#103)
- fix: propagate abort reason in derived signals (#101)
- docs: clarify that
TaskCancelledErroris not thrown on cooperative timeouts (#98) - docs: fix comment in wrapped policies example (#118)
- chore: update node types to support
AbortSignal(#116)
- fix: restore breaker state when circuit state is restored
- feat: allow hydration of circuit breaker
initialState(#89) - feat: allow passing a backoff algorithm to circuit breakers'
halfOpenAfter(#96) - feat: add new
CountBreaker(#93) - feat: provide the attempt number to retry's context (#95)
- fix: event listener leak in
timeout
- fix: decorrelatedJitter backoff returning NaN after many iterations (#86)
- chore: remove test files from dist package (#84)
- fix: memory leak when using
timeout()inwrap()(#69)
- feat: add new option
abortOnReturnto timeouts (#72)
- breaking: please see the breaking changes for the two 3.0.0-beta releases
- feat: expose
wrap()ed policies in the merged policy (#61)
-
breaking: refactor: create policies as free-floating functions rather than Policy methods
Previously, all policies were created via something like
Policy.handleAll().retry(...). However, as a result, it was hard for bundlers to tree-shake Cockatiel, since thePolicyclass was used and referenced every mechanism provided in this library.Instead, policies are now created via functions that consume the base
Policyconfiguration--and that configuration is started as free functions rather than static methods. For example, where you previously wrote:import { Policy } from 'cockatiel'; Policy.handleAll().retry().attempts(3);
You instead write
import { handleAll, retry } from 'cockatiel'; retry(handleAll, { attempts: 3 );
The full changes are:
Policy.retry()->retry(policy, options)Policy.circuitBreaker(halfOpenAfter, breaker)->retry(policy, { halfOpenAfter: number, breaker: IBreaker })Policy.fallback(valueOrFactory)->fallback(policy, valueOrFactory)Policy.wrap(...)->wrap(...)Policy.timeout(duration, strategy)->timeout(duration, strategy)Policy.bulkhead(limit[, quue])->bulkhead(limit[, quue])Policy.use()->usePolicy(policy)
This resolves #50
-
breaking: refactor: remove confusing Retry builder.
Previously, this package had a builder interface on
Policy.retry().... However, it was confusing how the different options of the builder interacted in more complex cases. For example, both the retry policy itself and the backoff could have different max attempts.We simplified it to be a simple options object given in
policy, where the max attempts is also given. For the backoff itself, you pass the underlying backoff generator (or a custom one)Instead of:
Policy.retry().attempts(2).delay(5), you can writeretry(policy, { maxAttempts: 2, backoff: new ConstantBackoff(5) })Policy.retry().delay([100, 200, 300]), you can writeretry(policy, { maxAttempts: 3, backoff: new IterableBackoff(100, 200, 300) })Policy.retry().exponential(opts), you can writeretry(policy, { backoff: new ExponentialBackoff(opts) })Policy.retry().delegate(fn), you can writeretry(policy, { backoff: new DelegateBackoff(fn) })
This is a little more verbose, but should be more clear to readers, and it also tree-shakes better.
As part of this, the
CompositeBackoffhas been removed. This was mostly an implementation detail of the retry builder internally, and can be better implemented as a custom function in aDelegateBackoffby most consumers.This resolves #58
-
fix: TypeScript warnings when using other providers of
AbortSignal.
-
breaking: refactor: move to using native
AbortSignaloverCancellationToken.Previously, this package provided its own implementation of cancellation via the
CancellationTokenSourceandCancellationToken. Now, we use the nativeAbortSignalwhich is available in browsers and Node.js since Node 16. To migrate, instead of...- accessing
context.cancellationToken, accesscontext.signalwhich is anAbortSignal, - pass in an
AbortSignalas the second argument toPolicy.execute, instead of aCancellationToken, - use
signal.abortedinstead ofsignal.isCancellationRequestedto check for cancellation, - use
signal.addEventListener("abort", fn)instead ofsignal.onCancellationRequested(fn)to listen for cancellation, - use
new AbortController()instead ofnew CancellationTokenSource(), andctrl.abort()andctrl.signalinstead ofctrl.cancel()andctrl.token(), - use the helper function
deriveAbortController(signal)exported from this package instead ofnew CancellationTokenSource(parent).
- accessing
- feat: improve event performance
- fix: export
IDisposable
- fix: remove incorrect deprecated marker on
RetryPolicy.onGiveUp - fix: incorrect typings in
retry().backoff()(#34)
-
breaking: reactor: introduce a separate BackoffFactory interface for the first backoff
This only requires changes if you use retry policies in your own code, outside of the
Policy.retry().See #30. For some backoff policies, such as delegate and exponential policies, the first backoff was always 0, before
next()was called. This is undesirable, and fixing it involved separating the backoff factory from the backoff itself.The backoff classes, such as
DelegateBackoffandExponentialBackoff, now only have anext()method. Theduration, which is now a property instead of a method, is only available after the firstnext()call.For example, previously if you did this:
let backoff = new ExponentialBackoff(); while (!succeeded) { if (!tryAgain()) { await delay(backoff.duration()); backoff = backoff.next(); } }
You now need to call
next()before you accessduration:let backoff = new ExponentialBackoff(); while (!succeeded) { if (!tryAgain()) { backoff = backoff.next(); await delay(backoff.duration); } }
Note: if you use typescript, you will need another variable for it to understand you. Here's an example of how we use it inside the RetryPolicy.
- fix: events on the timeout policy being emitted incorrectly, or not emitted (see #27)
- feat: add an optional
CancellationTokentoIPolicy.execute. Add cancellation awareness to all policies; see their specific documentation for more information. (see #25) - docs: fix outdated docs on
Policy.circuitBreakerand unnecessary dashes in jsdoc comments (see #22, #23, #24)
- fix: cockatiel not working in certain browser builds
- breaking: Node versions <10 are no longer supported.
- breaking:
FallbackPolicy.onFallbackis replaced withFallbackPolicy.onFailure. When a failure happens, a fallback will occur. - feat: add
isBrokenCircuitError,isBulkheadRejectedError,isIsolatedCircuitError,isTaskCancelledErrormethods to the errors and matching predicate functions. - feat: all policies now include
onFailureandonSuccesscallbacks for monitoring purposes (see #20) - fix: add
onHalfOpenevent to the circuit breaker (see #18) - fix:
retry.exponential()requiring an argument when it should have been optional (see #18)
- feat: add
.dangerouslyUnrefmethods for timeouts and retries (#11, thanks to @novemberborn)
- fix:
Timeout.Aggressivetriggering timeouts immediately (#16, thanks to @ekillops) - fix: correctly compile to ES2018 (#10, thanks to @novemberborn)
- feat: add new
Policy.use()decorator
- fix: wrong typing information for options to
retry.exponential()
- fix: jitter backoff not applying max delay correctly
- fix: jitter backoff adding more than intended amount of jitter
Initial Release