I have a main function/file which calls a doLogic function/file. This doLogic opens up a database connection, and I am trying to use process.on('SIGINT') to cleanup the database, however, when I hit CTRL-C the process doesn't wait for the asynchronous calls.
main.js
import {doLogic} from "./doLogic"
import {cleanup} from "./cleanup"
// Attach SIGINT listener
process.on("SIGINT", async () => {
await cleanup()
process.exit(0)
})
async function run() {
doLogic()
}
run()
doLogic.js
import { connectionIds } from "./cleanup"
export async function doLogic {
for (const desiredConnection of desiredConnections) {
let id = await connectToDB(desiredConnection)
connectionIds.add(id)
}
}
cleanup.js
export const connectionIds = new Map()
// This function is just to simulate disconnecting
unlockConnection(connectionId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("resolve");
}, 5* 1000);
setTimeout(() => {
reject("reject");
}, 5 * 1000);
});
}
export async function cleanup() {
let asyncCalls = []
for(const lockedConnection of connectionIds.keys()) {
// I have also tried below with await unlockConn
// But not much difference
asyncCalls.push(unlockConnection(lockedConnection))
}
return Promise.all(asyncCalls)
}
The weird thing is that it is strictly doLogic() which seems to prevent the await cleanup() call from actually awaiting. If I comment this out and just pause the program with stdin() then if I send a SIGINT await cleanup() seems to work fine. Also I can confirm that there are actually connections in the map.
Related
I found interesting problem (not a real task):
class Client {
async connect() {
return new Promise(resolve => {
setTimeout(() => {
console.log('connect');
resolve()
}, 2000);
})
}
async request() {
console.log('done something')
}
}
class Api {
#client;
async getClient() {
if (!this.#client) {
const client = new Client();
await client.connect();
this.#client = client;
}
return this.#client;
}
async request() {
const client = await this.getClient();
return client.request();
}
}
const run = async () => {
const api = new Api();
await Promise.all([api.request(), api.request(), api.request(), api.request()]);
}
run();
It will output this:
connect
request completed
connect
request completed
connect
request completed
connect
request completed
Obviously, we need to wait for initial connection and then resolve other requests, so output would be like this:
connect
request completed
request completed
request completed
request completed
My working solution:
class Client {
async connect() {
return new Promise(resolve => {
setTimeout(() => {
console.log('connect');
resolve()
}, 2000);
})
}
async request() {
console.log('request completed')
}
}
class Api {
#connecting = false;
#client;
#queue = [];
async getClient() {
if (!this.#client && !this.#connecting) {
this.#connecting = true;
const client = new Client();
await client.connect();
this.#connecting = false;
this.#client = client;
this.resolveQueue(client)
}
return this.#client;
}
async resolveQueue(client) {
await Promise.all(this.#queue.map(task => task(client)))
}
async request() {
const client = await this.getClient();
if (client) {
return client.request();
} else {
this.#queue.push(async (client) => client.request());
}
}
}
const run = async () => {
const api = new Api();
await Promise.all([api.request(), api.request(), api.request(), api.request()]);
}
run();
But it seems there should be more clear/easy way to do this, because I added a lot of layers and complexity. Also, I think the goal here is to add as little changes as possible, keeping the code structure as is, which I also tried to achieve. In real world situation, I would probably rewrite this completely and made it more verbose.
There are various different approaches to this. With minimal changes to your code, there are two things you'll want to do:
Remember a promise in Client for the connect result, so you can wait for it to be fulfilled in request
In Api, set this.#client immediately, before awaiting the connection result, so that multiple calls to request don't create multiple separate Client instances
class Client {
// Have a promise for connection, which also gives you a handy way
// to know that `connect` has been called
connectionPromise = null;
async connect() {
// Remember the connection promise
this.connectionPromise = new Promise((resolve) => {
setTimeout(() => {
console.log("connect");
resolve();
}, 200);
});
return this.connectionPromise;
}
async request() {
// Check that `connect` has been called
if (!this.connectionPromise) {
throw new Error(`Can't call 'request' before calling 'connect'`);
}
// Wait until the promise is fulfilled
await this.connectionPromise;
// Now do the request
console.log("done something");
}
}
class Api {
#client;
async getClient() {
if (!this.#client) {
// Set `this.#client` immediately, before awaiting
// the connection, so multiple overlapping calls to
// `request` use the **same** client
this.#client = new Client();
await this.#client.connect();
}
return this.#client;
}
async request() {
const client = await this.getClient();
return client.request();
}
}
const run = async () => {
const api = new Api();
await Promise.all([api.request(), api.request(), api.request(), api.request()]);
};
run();
.as-console-wrapper {
max-height: 100% !important;
}
But, I'd suggest you separate Client into two parts:
The part that does connection (which can just be a function; if you wanted, it could be a static method on Client)
The client object that can do requests
That way, you never have a stateful Client instance that isn't ready for use. By the time you get one (from the function that does connection), it's connected and good to go.
In node i keep checking if connection is available if it is it will get token and store in variable but if not then catch error will be thrown and it will then execute setTimeout to try again in 2 seconds.
But i think i need a clean way to clear the setTimeout because when i log it i keep getting higher number in Symbol(asyncId) this to me feels like it keeps adding setTimeout more and more to memory?
export const getToken = async () => {
try {
if (!GLOBAL_VARS.authToken) {
await auth(`Getting token`)
}
// Do more stuff
} catch (error: any) {
tokenTimeout()
}
}
getToken()
export const tokenTimeout = () => {
setTimeout(() => {
getToken()
}, 2000)
}
Yest it will add more setTimeouts to the memory.
you can clear it like this:
export const tokenTimeout = setTimeout(() => {
getToken()
}, 2000)
export const getToken = async () => {
try {
if (!GLOBAL_VARS.authToken) {
await auth(`Getting token`)
}
// Do more stuff
} catch (error: any) {
const timeout = tokenTimeout()
}
}
getToken()
/// use it wherever you want
clearTimeout(timeout);
Memory is going to increase because of whatever you are doing with auth() and not because you are creating timeouts.
The issue I see with your code is not really using async await properly. It should look more like
const sleep = (ms) =>
new Promise((resolve) => {
setTimeout(resolve, ms);
});
export const getToken = async (count = 0) => {
try {
if (!GLOBAL_VARS.authToken) {
await auth(`Getting token`);
}
} catch (error: any) {
await sleep(2000);
if (count < 10) await getToken(++count);
else throw new Error('Taking too long to get auth token');
}
}
(async function () {
await getToken();
})();
I am using React to build a website. I have imported an asynchronous function to execute when I press a button. However, the function is not working asynchronously and I really don't understand why.
interact.js:
export const getNFT = async () => {
setTimeout(() => {
console.log('getNFT code execute');
return nft;
}, 2000);
};
const nft = {
tokenURI: 'https://gateway.pinata.cloud/ipfs/QmdxQFWzBJmtSvrJXp75UNUaoVMDH49g43WsL1YEyb',
imageURL: 'https://gateway.pinata.cloud/ipfs/QmeMTHnqdfpUcRVJBRJ4GQ2XHU2ruVrdJqZhLz',
ID: '212'
};
Main.js
import {
getNFT
} from 'interact.js';
// This function is executed when a user clicks on a button
let getAllocatedNFT = async () => {
try {
let response = await getNFT();
console.log('response from server:: '+response);
}catch(e){
console.log(e);
}
};
console:
response from server:: undefined
getNFT code execute // This is executed correctly after 2 seconds
You have to return promise which will resolve your webAPI(setTimeout)
Please use like below:
const getNFT = async () => {
return new Promise(resolve => setTimeout(() => {
console.log("getNFT code execute")
resolve(true)
}, 2000)
);
};
Hi guys I'm having trouble testing the below JS using Jest. It starts with waitForWorker. if the response is 'working' then it calls waitForWorker() again. I tried Jest testing but I don't know how to test an inner function call and I've been researching and failing.
const $ = require('jquery')
const axios = require('axios')
let workerComplete = () => {
window.location.reload()
}
async function checkWorkerStatus() {
const worker_id = $(".worker-waiter").data('worker-id')
const response = await axios.get(`/v1/workers/${worker_id}`)
return response.data
}
function waitForWorker() {
if (!$('.worker-waiter').length) {
return
}
checkWorkerStatus().then(data => {
// delay next action by 1 second e.g. calling api again
return new Promise(resolve => setTimeout(() => resolve(data), 1000));
}).then(worker_response => {
const working_statuses = ['queued', 'working']
if (worker_response && working_statuses.includes(worker_response.status)) {
waitForWorker()
} else {
workerComplete()
}
})
}
export {
waitForWorker,
checkWorkerStatus,
workerComplete
}
if (process.env.NODE_ENV !== 'test') $(waitForWorker)
Some of my test is below since i can't double check with anyone. I don't know if calling await Worker.checkWorkerStatus() twice in the tests is the best way since waitForWorker should call it again if the response data.status is 'working'
import axios from 'axios'
import * as Worker from 'worker_waiter'
jest.mock('axios')
beforeAll(() => {
Object.defineProperty(window, 'location', {
value: { reload: jest.fn() }
})
});
beforeEach(() => jest.resetAllMocks() )
afterEach(() => {
jest.restoreAllMocks();
});
describe('worker is complete after 2 API calls a', () => {
const worker_id = Math.random().toString(36).slice(-5) // random string
beforeEach(() => {
axios.get
.mockResolvedValueOnce({ data: { status: 'working' } })
.mockResolvedValueOnce({ data: { status: 'complete' } })
jest.spyOn(Worker, 'waitForWorker')
jest.spyOn(Worker, 'checkWorkerStatus')
document.body.innerHTML = `<div class="worker-waiter" data-worker-id="${worker_id}"></div>`
})
it('polls the correct endpoint twice a', async() => {
const endpoint = `/v1/workers/${worker_id}`
await Worker.checkWorkerStatus().then((data) => {
expect(axios.get.mock.calls).toMatchObject([[endpoint]])
expect(data).toMatchObject({"status": "working"})
})
await Worker.checkWorkerStatus().then((data) => {
expect(axios.get.mock.calls).toMatchObject([[endpoint],[endpoint]])
expect(data).toMatchObject({"status": "complete"})
})
})
it('polls the correct endpoint twice b', async() => {
jest.mock('waitForWorker', () => {
expect(Worker.checkWorkerStatus).toBeCalled()
})
expect(Worker.waitForWorker).toHaveBeenCalledTimes(2)
await Worker.waitForWorker()
})
I think there are a couple things you can do here.
Inject status handlers
You could make the waitForWorker dependencies and side effects more explicit by injecting them into the function this lets you fully black box the system under test and assert the proper injected effects are triggered. This is known as dependency injection.
function waitForWorker(onComplete, onBusy) {
// instead of calling waitForWorker call onBusy.
// instead of calling workerComplete call onComplete.
}
Now to test, you really just need to create mock functions.
const onComplete = jest.fn();
const onBusy = jest.fn();
And assert that those are being called in the way you expect. This function is also async so you need to make sure your jest test is aware of the completion. I notice you are using async in your test, but your current function doesnt return a pending promise so the test will complete synchronously.
Return a promise
You could just return a promise and test for its competition. Right now the promise you have is not exposed outside of waitForWorker.
async function waitForWorker() {
let result = { status: 'empty' };
if (!$('.worker-waiter').length) {
return result;
}
try {
const working_statuses = ['queued', 'working'];
const data = await checkWorkerStatus();
if (data && working_statuses.includes(data.status)) {
await waitForWorker();
} else {
result = { status: 'complete' };
}
} catch (e) {
result = { status: 'error' };
}
return result;
}
The above example converts your function to async for readability and removes side effects. I returned an async result with a status, this is usefull since there are many branches that waitForWorker can complete. This will tell you that given your axios setup that the promise will complete eventually with some status. You can then use coverage reports to make sure the branches you care about were executed without worrying about testing inner implementation details.
If you do want to test inner implementation details, you may want to incorporate some of the injection principals I mentioned above.
async function waitForWorker(request) {
// ...
try {
const working_statuses = ['queued', 'working'];
const data = await request();
} catch (e) {
// ...
}
// ...
}
You can then inject any function into this, even a mock and make sure its called the way you want without having to mock up axios. In your application you simply just inject checkWorkerStatus.
const result = await waitForWorker(checkWorkerStatus);
if (result.status === 'complete') {
workerComplete();
}
Hi I am write a unit test case of this function. When I run this function from the unit test case then it covers all statements but setInterval complete lines are not covered.
Does anyone know how to cover it in javascript? I am using mocha.
const open = async function (config) {
...... set of lines..
let retryIn = setInterval(async () => {
try {
client = await connect(config);
clearInterval(retryIn);
return client;
} catch (err) {
//error
}
}, 20000);
};
I am simply calling it like this
it("###test", async () => {
open(config);
});
});
First of all, you should never use setInterval in the case where you want to retry a task that has failed. You should use setTimeout instead.
Besides that, you cannot return a value from a classical callback base function like setInterval or setTimeout. So in its current form, the promise returned when calling open will be resolved before any connection is made.
With await/async you can create a clean and simple setup for such a situation:
function wait(seconds) {
return new Promise((resolve, _) => setTimeout(resolve, seconds))
}
async function connect() {
throw new Error('failed')
}
async function open(config) {
let client;
while (client === undefined /* || retries > maxRetries*/ ) {
try {
client = await connect(config);
} catch (err) {
// if connection failed due to an error, wait n seconds and retry
console.log('failed wait 2 seconds for reconnect');
await wait(2000)
// ++retries
}
}
if (!client) {
throw new Error('connection failed due to max number of retries reached.')
}
return client
}
async function main() {
let connection = await open()
}
main().catch(err => console.log(err))
You can further extend this snippet by adding a retry limit. See the comments for a rough idea on how that can be achieved.
To test the above code, you would write:
it("###test", function() {
return open(config);
});
Someone posted an answer about fake timers and then deleted it , The answer was correct so I re-posted again.
You can use sinonjs to create fake timers
Fake timers are synchronous implementations of setTimeout and friends
that Sinon.JS can overwrite the global functions with to allow you to
more easily test code using them
But from your code, it seems you are trying to test async code, in mocha, this can be achieved like this
describe('somthing', () => {
it('the result is 2', async () => {
const x = await add(1, 1)
expect(x).to.equal(4);
});
});
With something closer to your code
async function open() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('done')
}, 1000);
});
};
describe('somthing', () => {
it('###test', async () => {
const x = await open()
chai.expect(x).to.equal("done");
});
});
Just wrap to Promise
const open = async function (config) {
...... set of lines..
return new Promise((resolve, reject) => {
let retryIn = setInterval(async () => {
client = await connect(asConfigParam);
clearInterval(retryIn);
return client;
}, 20000);
return resolve(retryIn);
});
};
it("###test", async () => {
const result = await open(config);
console.log('result', result)
});