Skip to content

Commit

Permalink
feat: expose wrapped policies in merged policy
Browse files Browse the repository at this point in the history
Fixes #61
  • Loading branch information
connor4312 committed Aug 13, 2022
1 parent 7525ad8 commit e409dcc
Show file tree
Hide file tree
Showing 10 changed files with 51 additions and 40 deletions.
5 changes: 5 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## 3.0.0

- **breaking:** please see the breaking changes for the two 3.0.0-beta releases
- **feat:** expose `wrap()`ed policies in the merged policy ([#61](https://github.com/connor4312/cockatiel/issues/61))

## 3.0.0-beta.1

- **breaking:** **refactor:** create policies as free-floating functions rather than Policy methods
Expand Down
2 changes: 2 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,8 @@ wrap(retry, breaker, timeout).execute(context => {
});
```

The individual wrapped policies are accessible on the `policies` property of the policy returned from `wrap()`.

### `@usePolicy(policy)`

A decorator that can be used to wrap class methods and apply the given policy to them. It also adds the last argument normally given in `Policy.execute` as the last argument in the function call. For example:
Expand Down
2 changes: 2 additions & 0 deletions src/BulkheadPolicy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ interface IQueueItem<T> {
* Bulkhead limits concurrent requests made.
*/
export class BulkheadPolicy implements IPolicy {
declare readonly _altReturn: never;

private active = 0;
private readonly queue: Array<IQueueItem<unknown>> = [];
private readonly onRejectEmitter = new EventEmitter<void>();
Expand Down
2 changes: 2 additions & 0 deletions src/CircuitBreakerPolicy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ type InnerState =
| { value: CircuitState.HalfOpen; test: Promise<any> };

export class CircuitBreakerPolicy implements IPolicy {
declare readonly _altReturn: never;

private readonly breakEmitter = new EventEmitter<FailureReason<unknown> | { isolated: true }>();
private readonly resetEmitter = new EventEmitter<void>();
private readonly halfOpenEmitter = new EventEmitter<void>();
Expand Down
2 changes: 2 additions & 0 deletions src/FallbackPolicy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { ExecuteWrapper } from './common/Executor';
import { IDefaultPolicyContext, IPolicy } from './Policy';

export class FallbackPolicy<AltReturn> implements IPolicy<IDefaultPolicyContext, AltReturn> {
declare readonly _altReturn: AltReturn;

/**
* @inheritdoc
*/
Expand Down
2 changes: 2 additions & 0 deletions src/NoopPolicy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { IDefaultPolicyContext, IPolicy } from './Policy';
* A no-op policy, useful for unit tests and stubs.
*/
export class NoopPolicy implements IPolicy {
declare readonly _altReturn: never;

private readonly executor = new ExecuteWrapper();

// tslint:disable-next-line: member-ordering
Expand Down
7 changes: 5 additions & 2 deletions src/Policy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,16 @@ describe('Policy', () => {
});

it('wraps and keeps correct types', async () => {
const policy = wrap(
const policies = [
retry(handleAll, { maxAttempts: 2 }),
circuitBreaker(handleAll, { halfOpenAfter: 100, breaker: new ConsecutiveBreaker(2) }),
fallback(handleAll, 'foo'),
timeout(1000, TimeoutStrategy.Aggressive),
noop,
);
] as const;
const policy = wrap(...policies);

expect(policy.wrapped).to.deep.equal(policies);

const result = await policy.execute(context => {
expect(context.signal).to.be.an.instanceOf(AbortSignal);
Expand Down
65 changes: 27 additions & 38 deletions src/Policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import { Event } from './common/Event';
import { ExecuteWrapper } from './common/Executor';
import { FallbackPolicy } from './FallbackPolicy';
import { NoopPolicy } from './NoopPolicy';
import { IRetryBackoffContext, IRetryContext, RetryPolicy } from './RetryPolicy';
import { ICancellationContext, TimeoutPolicy, TimeoutStrategy } from './TimeoutPolicy';
import { IRetryBackoffContext, RetryPolicy } from './RetryPolicy';
import { TimeoutPolicy, TimeoutStrategy } from './TimeoutPolicy';

type Constructor<T> = new (...args: any) => T;

Expand Down Expand Up @@ -74,6 +74,12 @@ export interface IPolicy<
ContextType extends IDefaultPolicyContext = IDefaultPolicyContext,
AltReturn = never,
> {
/**
* Virtual property only used for TypeScript--will not actually be defined.
* @deprecated This property does not exist
*/
readonly _altReturn: AltReturn;

/**
* Fires on the policy when a request successfully completes and some
* successful value will be returned. In a retry policy, this is fired once
Expand All @@ -96,23 +102,18 @@ export interface IPolicy<
): Promise<T | AltReturn>;
}

type PolicyType<T> = T extends RetryPolicy
? IPolicy<IRetryContext, never>
: T extends TimeoutPolicy
? IPolicy<ICancellationContext, never>
: T extends FallbackPolicy<infer F>
? IPolicy<IRetryContext, F>
: T extends CircuitBreakerPolicy
? IPolicy<IRetryContext, never>
: T extends NoopPolicy
? IPolicy<IDefaultPolicyContext, never>
: T extends IPolicy<infer ContextType, infer ReturnType>
? IPolicy<ContextType, ReturnType>
: never;
export interface IMergedPolicy<A extends IDefaultPolicyContext, B, W extends IPolicy<any, any>[]>
extends IPolicy<A, B> {
readonly wrapped: W;
}

type MergePolicies<A, B> = A extends IPolicy<infer A1, infer A2>
? B extends IPolicy<infer B1, infer B2>
? IPolicy<A1 & B1, A2 | B2>
type MergePolicies<A, B> = A extends IPolicy<infer A1, any>
? B extends IPolicy<infer B1, any>
? IMergedPolicy<
A1 & B1,
A['_altReturn'] | B['_altReturn'],
B extends IMergedPolicy<any, any, infer W> ? [A, ...W] : [A, B]
>
: never
: never;

Expand Down Expand Up @@ -341,30 +342,22 @@ export function timeout(duration: number, strategy: TimeoutStrategy) {
// types well in that scenario (unless p is explicitly typed as an IPolicy
// and not some implementation) and returns `IPolicy<void, unknown>` and
// the like. This is the best solution I've found for it.
export function wrap<A extends IPolicy<IDefaultPolicyContext, unknown>>(p1: A): PolicyType<A>;
export function wrap<A extends IPolicy<IDefaultPolicyContext, unknown>>(p1: A): A;
export function wrap<
A extends IPolicy<IDefaultPolicyContext, unknown>,
B extends IPolicy<IDefaultPolicyContext, unknown>,
>(p1: A, p2: B): MergePolicies<PolicyType<A>, PolicyType<B>>;
>(p1: A, p2: B): MergePolicies<A, B>;
export function wrap<
A extends IPolicy<IDefaultPolicyContext, unknown>,
B extends IPolicy<IDefaultPolicyContext, unknown>,
C extends IPolicy<IDefaultPolicyContext, unknown>,
>(p1: A, p2: B, p3: C): MergePolicies<PolicyType<C>, MergePolicies<PolicyType<A>, PolicyType<B>>>;
>(p1: A, p2: B, p3: C): MergePolicies<C, MergePolicies<A, B>>;
export function wrap<
A extends IPolicy<IDefaultPolicyContext, unknown>,
B extends IPolicy<IDefaultPolicyContext, unknown>,
C extends IPolicy<IDefaultPolicyContext, unknown>,
D extends IPolicy<IDefaultPolicyContext, unknown>,
>(
p1: A,
p2: B,
p3: C,
p4: D,
): MergePolicies<
PolicyType<D>,
MergePolicies<PolicyType<C>, MergePolicies<PolicyType<A>, PolicyType<B>>>
>;
>(p1: A, p2: B, p3: C, p4: D): MergePolicies<D, MergePolicies<C, MergePolicies<A, B>>>;
export function wrap<
A extends IPolicy<IDefaultPolicyContext, unknown>,
B extends IPolicy<IDefaultPolicyContext, unknown>,
Expand All @@ -377,20 +370,16 @@ export function wrap<
p3: C,
p4: D,
p5: E,
): MergePolicies<
PolicyType<E>,
MergePolicies<
PolicyType<D>,
MergePolicies<PolicyType<C>, MergePolicies<PolicyType<A>, PolicyType<B>>>
>
>;
): MergePolicies<E, MergePolicies<D, MergePolicies<C, MergePolicies<A, B>>>>;
export function wrap<C extends IDefaultPolicyContext, A>(...p: Array<IPolicy<C, A>>): IPolicy<C, A>;
export function wrap<C extends IDefaultPolicyContext, A>(
...p: Array<IPolicy<C, A>>
): IPolicy<C, A> {
): IMergedPolicy<C, A, IPolicy<C, A>[]> {
return {
_altReturn: undefined as any,
onFailure: p[0].onFailure,
onSuccess: p[0].onSuccess,
wrapped: p,
execute<T>(fn: (context: C) => PromiseLike<T> | T, signal: AbortSignal): Promise<T | A> {
const run = (context: C, i: number): PromiseLike<T | A> | T | A =>
i === p.length
Expand Down
2 changes: 2 additions & 0 deletions src/RetryPolicy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ export interface IRetryPolicyConfig {
}

export class RetryPolicy implements IPolicy<IRetryContext> {
declare readonly _altReturn: never;

private readonly onGiveUpEmitter = new EventEmitter<FailureReason<unknown>>();
private readonly onRetryEmitter = new EventEmitter<FailureReason<unknown> & { delay: number }>();

Expand Down
2 changes: 2 additions & 0 deletions src/TimeoutPolicy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export interface ICancellationContext {
}

export class TimeoutPolicy implements IPolicy<ICancellationContext> {
declare readonly _altReturn: never;

private readonly timeoutEmitter = new EventEmitter<void>();

/**
Expand Down

0 comments on commit e409dcc

Please sign in to comment.