Skip to content

Commit

Permalink
feat(context): add tryCatchFinally helper function for value or pro…
Browse files Browse the repository at this point in the history
…mise
  • Loading branch information
raymondfeng committed Jun 2, 2020
1 parent e5e3d19 commit c764ac6
Show file tree
Hide file tree
Showing 3 changed files with 233 additions and 56 deletions.
188 changes: 188 additions & 0 deletions packages/context/src/__tests__/unit/try-catch-finally.unit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
// Copyright IBM Corp. 2020. All Rights Reserved.
// Node module: @loopback/context
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {expect} from '@loopback/testlab';
import {tryCatchFinally, tryWithFinally} from '../..';

describe('tryWithFinally', () => {
it('performs final action for a fulfilled promise', async () => {
let finalActionInvoked = false;
const action = () => Promise.resolve(1);
const finalAction = () => (finalActionInvoked = true);
await tryWithFinally(action, finalAction);
expect(finalActionInvoked).to.be.true();
});

it('performs final action for a resolved value', () => {
let finalActionInvoked = false;
const action = () => 1;
const finalAction = () => (finalActionInvoked = true);
// eslint-disable-next-line @typescript-eslint/no-floating-promises
tryWithFinally(action, finalAction);
expect(finalActionInvoked).to.be.true();
});

it('performs final action for a rejected promise', async () => {
let finalActionInvoked = false;
const action = () => Promise.reject(new Error('error'));
const finalAction = () => (finalActionInvoked = true);
await expect(tryWithFinally(action, finalAction)).be.rejectedWith('error');
expect(finalActionInvoked).to.be.true();
});

it('performs final action for an action that throws an error', () => {
let finalActionInvoked = false;
const action = () => {
throw new Error('error');
};
const finalAction = () => (finalActionInvoked = true);
expect(() => tryWithFinally(action, finalAction)).to.throw('error');
expect(finalActionInvoked).to.be.true();
});
});

describe('tryCatchFinally', () => {
it('performs final action for a fulfilled promise', async () => {
let finalActionInvoked = false;
const action = () => Promise.resolve(1);
const finalAction = () => (finalActionInvoked = true);
await tryCatchFinally(action, undefined, finalAction);
expect(finalActionInvoked).to.be.true();
});

it('performs final action for a resolved value', () => {
let finalActionInvoked = false;
const action = () => 1;
const finalAction = () => (finalActionInvoked = true);
// eslint-disable-next-line @typescript-eslint/no-floating-promises
tryCatchFinally(action, undefined, finalAction);
expect(finalActionInvoked).to.be.true();
});

it('skips error action for a fulfilled promise', async () => {
let errorActionInvoked = false;
const action = () => Promise.resolve(1);
const errorAction = (err: unknown) => {
errorActionInvoked = true;
throw err;
};
await tryCatchFinally(action, errorAction);
expect(errorActionInvoked).to.be.false();
});

it('skips error action for a resolved value', () => {
let errorActionInvoked = false;
const action = () => 1;
const errorAction = (err: unknown) => {
errorActionInvoked = true;
throw err;
};
// eslint-disable-next-line @typescript-eslint/no-floating-promises
tryCatchFinally(action, errorAction);
expect(errorActionInvoked).to.be.false();
});

it('performs error action for a rejected promise', async () => {
let errorActionInvoked = false;
const errorAction = (err: unknown) => {
errorActionInvoked = true;
throw err;
};
const action = () => Promise.reject(new Error('error'));
const finalAction = () => true;
await expect(
tryCatchFinally(action, errorAction, finalAction),
).be.rejectedWith('error');
expect(errorActionInvoked).to.be.true();
});

it('performs error action for an action that throws an error', () => {
let errorActionInvoked = false;
const errorAction = (err: unknown) => {
errorActionInvoked = true;
throw err;
};
const action = () => {
throw new Error('error');
};
const finalAction = () => true;
expect(() => tryCatchFinally(action, errorAction, finalAction)).to.throw(
'error',
);
expect(errorActionInvoked).to.be.true();
});

it('allows error action to return a value for a rejected promise', async () => {
let errorActionInvoked = false;
const errorAction = (err: unknown) => {
errorActionInvoked = true;
return 1;
};
const action = () => Promise.reject(new Error('error'));
const result = await tryCatchFinally(action, errorAction);
expect(errorActionInvoked).to.be.true();
expect(result).to.equal(1);
});

it('allows error action to return a value for an action that throws an error', () => {
let errorActionInvoked = false;
const errorAction = (err: unknown) => {
errorActionInvoked = true;
return 1;
};
const action = () => {
throw new Error('error');
};
const result = tryCatchFinally(action, errorAction);
expect(result).to.equal(1);
expect(errorActionInvoked).to.be.true();
});

it('skips error action for rejection from the final action', async () => {
let errorActionInvoked = false;
const errorAction = (err: unknown) => {
errorActionInvoked = true;
throw err;
};
const action = () => Promise.resolve(1);
const finalAction = () => {
throw new Error('error');
};
await expect(
tryCatchFinally(action, errorAction, finalAction),
).be.rejectedWith('error');
expect(errorActionInvoked).to.be.false();
});

it('skips error action for error from the final action', () => {
let errorActionInvoked = false;
const errorAction = (err: unknown) => {
errorActionInvoked = true;
throw err;
};
const action = () => 1;
const finalAction = () => {
throw new Error('error');
};
expect(() => tryCatchFinally(action, errorAction, finalAction)).to.throw(
'error',
);
expect(errorActionInvoked).to.be.false();
});

it('allows default error action', () => {
const action = () => {
throw new Error('error');
};
expect(() => tryCatchFinally(action)).to.throw('error');
});

it('allows default error action for rejected promise', () => {
const action = () => {
return Promise.reject(new Error('error'));
};
return expect(tryCatchFinally(action)).to.be.rejectedWith('error');
});
});
38 changes: 0 additions & 38 deletions packages/context/src/__tests__/unit/value-promise.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,46 +10,8 @@ import {
resolveMap,
resolveUntil,
transformValueOrPromise,
tryWithFinally,
} from '../..';

describe('tryWithFinally', () => {
it('performs final action for a fulfilled promise', async () => {
let finalActionInvoked = false;
const action = () => Promise.resolve(1);
const finalAction = () => (finalActionInvoked = true);
await tryWithFinally(action, finalAction);
expect(finalActionInvoked).to.be.true();
});

it('performs final action for a resolved value', () => {
let finalActionInvoked = false;
const action = () => 1;
const finalAction = () => (finalActionInvoked = true);
// eslint-disable-next-line @typescript-eslint/no-floating-promises
tryWithFinally(action, finalAction);
expect(finalActionInvoked).to.be.true();
});

it('performs final action for a rejected promise', async () => {
let finalActionInvoked = false;
const action = () => Promise.reject(new Error('error'));
const finalAction = () => (finalActionInvoked = true);
await expect(tryWithFinally(action, finalAction)).be.rejectedWith('error');
expect(finalActionInvoked).to.be.true();
});

it('performs final action for an action that throws an error', () => {
let finalActionInvoked = false;
const action = () => {
throw new Error('error');
};
const finalAction = () => (finalActionInvoked = true);
expect(() => tryWithFinally(action, finalAction)).to.throw('error');
expect(finalActionInvoked).to.be.true();
});
});

describe('getDeepProperty', () => {
it('gets the root value if path is empty', () => {
const obj = {x: {y: 1}};
Expand Down
63 changes: 45 additions & 18 deletions packages/context/src/value-promise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,36 +187,63 @@ export function resolveList<T, V>(
* @param action - A function that returns a promise or a value
* @param finalAction - A function to be called once the action
* is fulfilled or rejected (synchronously or asynchronously)
*
* @typeParam T - Type for the return value
*/
export function tryWithFinally<T>(
action: () => ValueOrPromise<T>,
finalAction: () => void,
): ValueOrPromise<T> {
return tryCatchFinally(action, undefined, finalAction);
}

/**
* Try to run an action that returns a promise or a value with error and final
* actions to mimic `try {} catch(err) {} finally {}` for a value or promise.
*
* @param action - A function that returns a promise or a value
* @param errorAction - A function to be called once the action
* is rejected (synchronously or asynchronously). It must either return a new
* value or throw an error.
* @param finalAction - A function to be called once the action
* is fulfilled or rejected (synchronously or asynchronously)
*
* @typeParam T - Type for the return value
*/
export function tryCatchFinally<T>(
action: () => ValueOrPromise<T>,
errorAction: (err: unknown) => T | never = err => {
throw err;
},
finalAction: () => void = () => {},
): ValueOrPromise<T> {
let result: ValueOrPromise<T>;
try {
result = action();
} catch (err) {
finalAction();
throw err;
result = reject(err);
}
if (isPromiseLike(result)) {
// Once (promise.finally)[https://github.com/tc39/proposal-promise-finally
// is supported, the following can be simplifed as
// `result = result.finally(finalAction);`
result = result.then(
val => {
finalAction();
return val;
},
err => {
finalAction();
throw err;
},
);
} else {
finalAction();
return result.then(resolve, reject);
}

return resolve(result);

function resolve(value: T) {
try {
return value;
} finally {
finalAction();
}
}

function reject(err: unknown): T | never {
try {
return errorAction(err);
} finally {
finalAction();
}
}
return result;
}

/**
Expand Down

0 comments on commit c764ac6

Please sign in to comment.