Is there a python-like `with` or `raii` for JavaScript? - javascript

Currently I am doing sth like this:
declare type Resource = { dispose: () => void }
declare function getResource(): Promise<Resource>
async function withResource<T>(executor: (r: Resource) => T) {
const r = await getResource()
try {
return await executor(r)
} finally {
r.dispose()
}
}
...
withResource(async r => {
// code goes here
})
Is there a better way to achieve this behavior ?

Related

Jest Unit Testing function that calls a second one that returns a promise

Edited Question with vazsonyidl suggestions applied
I have to write unit tests for a function similar to this one:
import {External} from 'ExternalModule';
async functionA(){
this.functionB().then((data) => {
External.functionC(options);
console.log("Reached1");
}).catch((data) => {
const { OnError = "" } = data || {}
if(OnError) {
External.functionC(anotherOptions);
console.log("Reached2");
}
})
}
functionB() {
return new Promise(() => {
});
}
As functionC belongs to another module, I placed a mock of it in the _mocks_folder:
//_mocks_/ExternalModule.ts
export var External: ExternalClass = {
functionC(){}
}
class ExternalClass{
constructor(){};
functionC(){};
}
I have mocked functionB in two diferent ways for testing the then and the catch :
it("should test then block", () => {
functionB = jest.fn(() => {return Promise.resolve()});
const functionSpy = jest.spyOn(ExternalModule.External, 'functionC');
void functionA().then(() => {
expect(functionSpy).not.toHaveBeenCalled();
});
})
it("should test catch block", () => {
const err = { OnError: "Error" };
functionB = jest.fn(() => {return Promise.reject(err)});
const functionSpy = jest.spyOn(ExternalModule.External, 'functionC');
void functionA().then(() => {
expect(functionSpy).not.toHaveBeenCalled();
});
})
What I am trying to do is expect that functionC was called and called with the correct params, but the test is always passing even if I test if functionC was not called.
What am I doing wrong?
Jest does not wait for the async code to complete before doing assertions.
You can use the following function:
const waitForPromises = () => new Promise(setImmediate);
to force Jest to wait for promises to complete before continuing like so:
it("does something", async () => {
promiseCall();
await waitForPromises();
expect(something).toBe(something)
});
I think when this function catch error, this error should have an 'OnError' property so the functionC can run.
const { OnError = "" } = data || {}
if(OnError) {
ExternalClass.functionC(anotherOptions);
}
change you response error data to return Promise.reject({OnError: '404'}) may solve this problem.
Because you are not providing it to your class.
The following code is working for me:
class A {
async functionA() {
this.functionB().then((data) => {
this.functionC(); // It woll log aaa here, you need this one.
}).catch((data) => {
const {OnError = ''} = data || {};
if (OnError) {
console.log('onerror');
}
});
}
functionB() {
return new Promise(() => {
});
}
functionC() {
return 2;
}
}
describe('a', () => {
it('test', () => {
const a = new A();
a.functionB = jest.fn(() => Promise.resolve());
const functionBSpy = jest.spyOn(a, 'functionC');
void a.functionA().then(() => {
expect(functionBSpy).toHaveBeenCalledTimes(1);
});
});
});
Hope this helps, any comment appreciated.
As you provided no information about your functionB I mocked something that may suitable for you.
Your original problem is that Jest does not wait for your callbacks to settle. It does the assertion although, even if your function calls happen later, Jest will not recognise them and says that no call ever occurred.
There are several docs available, for example Jest's one here

TypeScript: How can I properly type a function that takes a promise and returns that promise as it is

I have a function called prepareOnTick and it returns a function which takes a promise and adds some logic in the promise's then catch finally callbacks.
const prepareOnTick = (onPerItemSettle: OnPerItemSettle) => {
return (promise: Promise<any>) => {
promise
.then(
() => {
onPerItemSettle.onSuccess?.();
},
() => {
onPerItemSettle.onError?.();
}
)
.finally(() => {
onPerItemSettle.onSettled?.();
});
return promise;
};
};
const onTick = prepareOnTick({...})
I want the type of onTick to reflect the fact that whatever promise it takes, it is going to return that as it is. But now the type for it is (promise: Promise<any>) => Promise<any> which is not quite accurate. I suppose it should be (promise: Promise<T>) => Promise<T>.
So I tried to type it like this
const prepareOnTick = (onPerItemSettle: OnPerItemSettle) => {
return <T>(promise: Promise<T>): Promise<T>=> {
promise
.then(
() => {
onPerItemSettle.onSuccess?.();
},
() => {
onPerItemSettle.onError?.();
}
)
.finally(() => {
onPerItemSettle.onSettled?.();
});
return promise;
};
};
But TS compiler doesn't like my type annotations apparently so I must be doing something wrong.
This is the demo can someone take a look at it?
You forgot to add extra comma after generic parameter, here: return <T,>
const prepareOnTick = (onPerItemSettle: OnPerItemSettle) => {
// TS compiler does not like <T>, he likes more <T,> or <T extends unknown> because of
// JSX :D
return <T,>(promise: Promise<T>): Promise<T>=> {
promise
.then(
() => {
onPerItemSettle.onSuccess?.();
},
() => {
onPerItemSettle.onError?.();
}
)
.finally(() => {
onPerItemSettle.onSettled?.();
});
return promise;
};
};
You should take a look on this question

Replacing then statements with try/catch

I'm trying to remove the then statements from the following piece of code and then replace all the catches with try/catch statements. I'm having some issues knowing what to do with the then statements.
export class WelcomePageContribution implements IWorkbenchContribution {
constructor(
#IInstantiationService instantiationService: IInstantiationService,
#IConfigurationService configurationService: IConfigurationService,
#IEditorService editorService: IEditorService,
#IBackupFileService backupFileService: IBackupFileService,
#IFileService fileService: IFileService,
#IWorkspaceContextService contextService: IWorkspaceContextService,
#ILifecycleService lifecycleService: ILifecycleService,
#ICommandService private readonly commandService: ICommandService,
) {
const enabled = isWelcomePageEnabled(configurationService, contextService);
if (enabled && lifecycleService.startupKind !== StartupKind.ReloadedWindow) {
backupFileService.hasBackups().then(hasBackups => {
const activeEditor = editorService.activeEditor;
if (!activeEditor && !hasBackups) {
const openWithReadme = configurationService.getValue(configurationKey) === 'readme';
if (openWithReadme) {
return Promise.all(contextService.getWorkspace().folders.map(folder => {
const folderUri = folder.uri;
return fileService.resolve(folderUri)
.then(folder => {
const files = folder.children ? folder.children.map(child => child.name) : [];
const file = arrays.find(files.sort(), file => strings.startsWith(file.toLowerCase(), 'readme'));
if (file) {
return joinPath(folderUri, file);
}
return undefined;
}, onUnexpectedError);
})).then(arrays.coalesce)
.then<any>(readmes => {
if (!editorService.activeEditor) {
if (readmes.length) {
const isMarkDown = (readme: URI) => strings.endsWith(readme.path.toLowerCase(), '.md');
return Promise.all([
this.commandService.executeCommand('markdown.showPreview', null, readmes.filter(isMarkDown), { locked: true }),
editorService.openEditors(readmes.filter(readme => !isMarkDown(readme))
.map(readme => ({ resource: readme }))),
]);
} else {
return instantiationService.createInstance(WelcomePage).openEditor();
}
}
return undefined;
});
} else {
return instantiationService.createInstance(WelcomePage).openEditor();
}
}
return undefined;
}).then(undefined, onUnexpectedError);
}
}
}
so that the entire thing reads more like this..
const enabled = await isWelcomePageEnabled(configurationService, contextService);
if (enabled && lifecycleService.startupKind !== StartupKind.ReloadedWindow) {
const hasBackups = await backupFileService.hasBackups();
const activeEditor = editorService.activeEditor;
if (!activeEditor && !hasBackups) {
const openWithReadme = configurationService.getValue(configurationKey) === 'readme';
if (openWithReadme) {
...
It looks like you're on the right track with your second code block. then is called on a promise, so instead of using then you would await the function then was called on, save it to a variable, and then move the code that was in the callback to then below the await at the same indentation level. Whenever you await, you can wrap it in a try/catch and put what would have been in the catch callback inside of the catch block.
So for example
fetchData().then(data => {
console.log(data)
}).catch(err => {
console.error(err)
})
becomes
try {
const data = await fetchData()
console.log(data)
}
catch (err) {
console.error(err)
}
The complication in your example is that the code is in a class constructor, and those can't be async.

Multiple if's refactoring

I have this function, with two ifs where I want to find the user depending on which alphanumeric code I receive. How can I refactor this one with sanctuary-js?
//const code = '0011223344';
const code = 'aabbc';
const isNumberCode = code => !!/^[0-9]{10}$/.exec(code);
const isLiteralCode = code => !!/^[A-Za-z]{5}$/.exec(code);
const findUser = (criteria) => {
return new Promise(function(resolve, reject) {
setTimeout(function() {
resolve('user object');
}, 300);
});
}
async function handler(code) {
if (isNumberCode(code)) {
const user = await findUser({id: code});
return user;
}
if (isLiteralCode(code)) {
const user = await findUser({identifier: code});
return user;
}
return 'not found';
}
async function run() {
const user = await handler(code);
console.log(user)
}
run();
I can't understand how I should handle three different types: number code, literal code and not found code.
-- UPDATE
Here my functional solution (I may think so):
const Code = x => ({
chain: f => f(x),
fold: (e, a, f) => e(x)
});
const ErrorCode = x => ({
fold: (e, a, f) => e(x),
findLabel: f => ErrorCode(x)
});
const PromiseToCode = promise => ({
fold: (e, a, f) => promise.then(x => x.fold(e, a, f))
});
const NumberCode = x => ({
fold: (e, a, f) => a(x),
findLabel: f => PromiseToCode(f(x, {activationCode: x}, NumberCode))
});
const LiteralCode = x => ({
fold: (e, a, f) => f(x),
findLabel: f => PromiseToCode(f(x, {finderCode: x}, LiteralCode))
});
const checkTypeOfCode = code => {
if (isNumberCode(code)) {
return NumberCode(code);
}
if (isLiteralCode(code)) {
return LiteralCode(code);
}
return ErrorCode(code);
};
const find = async (code, criteria, type) => {
const user = findUser();
if (!user) {
return ErrorCode(code);
}
return type(user);
};
const handler2 = (code) =>
Code(code)
.chain(checkTypeOfCode)
.findLabel(find)
.fold(
e => 'not found',
a => 'user object find by id',
l => 'user object find by identifier'
)
handler2(code).then(console.log);
But I don't know if it's good code. Also I'm asking about sanctuary-js because I think that all this object not good way to programming.
Since you are looking for a more functional restructuring, you can try this:
Divide your code into smaller, more independent sections:
findUser: This function is responsible to give either UserObject or Not found.
Create a function getCriteria, that will have all the logic as to isNumberCode or isLiteralCode etc. This will return a criteria object or undefined.
handler should be responsible to get criteria, and based on that return findUser's response. Any cleanup code can be kept here but this is a hub function which calls various functions and return an output. It should have bare minimum business logic.
//const code = '0011223344';
const code = 'aabbc';
const isNumberCode = code => !!/^[0-9]{10}$/.exec(code);
const isLiteralCode = code => !!/^[A-Za-z]{5}$/.exec(code);
const findUser = (criteria) => {
return new Promise(function(resolve, reject) {
if (!criteria) resolve('not found')
setTimeout(function() {
resolve('user object');
}, 300);
});
}
function getCriteria(code) {
if (isNumberCode(code)) {
return { id: code };
}
if (isLiteralCode(code)) {
return { identifier: code }
}
}
async function handler(code) {
const user = await findUser(getCriteria(code))
return user;
}
async function run() {
const user = await handler(code);
console.log(user)
}
run();
You can create enum for multiple type of input and use switch statement as below.
// Enum for search-parameters
var ParameterTypes =
{
NUMBER :1 ,
LITERAL:2 ,
OTHER : 3
}
function getParameterType()
{
//responsible to get search-parameter
return isNumberCode(code) ? ParameterTypes.NUMBER :
( isLiteralCode(code) ? ParameterTypes.LITERAL : ParameterTypes.OTHER);
}
async function handler(code)
{
//responsible to search user
var user;
switch(getParameterType())
{
case ParameterTypes.NUMBER :
user = await findUser({id: code});
//console.log('number');
break;
case ParameterTypes.LITERAL :
user = await findUser({identifier: code});
//console.log('literal');
break;
case ParameterTypes.OTHER :
user = 'not found';
//console.log('other');
break;
}
return user;
}

Serialising Observables

I would like to serialise execution of functions returning an observable. I've somewhat managed to do this but it is not a very pragmatic solution. What would be a more reactive way to achieve the following?
import Rx from 'rx'
export default class Executor {
constructor() {
this.queue = []
this.draining = false
}
run(fn) {
return Rx.Observable.create(o => {
this.queue.push(Rx.Observable.defer(() =>
fn()
.doOnNext((value) => o.onNext(value))
.doOnError((err) => o.onError(err))
.doOnCompleted(() => o.onCompleted())))
async () => {
if (this.draining) {
return
}
this.draining = true
while (this.queue.length > 0) {
try {
await this.queue.shift().toPromise()
} catch(err) {
// Do nothing...
}
}
this.draining = false
}()
})
}
}
Not entirely sure what you are trying to achieve but it sounds like you are after the flatMapWithMaxConcurrent operator.
import Rx from 'rx'
export default class Executor {
constructor() {
// Queue of: () => Observable
this.queue = new Rx.Subject();
this.queue
.flatMapWithMaxConcurrent(1,
fn => fn().catch(Rx.Observable.empty()) // Replace with proper error handling
)
.subscribe(result => console.log(result));
}
push(fn) {
this.queue.onNext(fn);
}
}

Categories