How to await for connection before resolving other Promises? - javascript

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.

Related

How to await a value being set asynchronously?

Anticipated FAQ:
Yes, I know what a Promise is.
No, I can't simply move the init logic to the constructor. It needs to be called in the initMethod because the initMethod is a hook that needs to be called at a certain time.
Sorry, it's just that I saw some similar questions marked as "duplicate", so I wanted to put these FAQ at the top.
Question
My issue is the following race condition:
class Service {
private x: string | null = null;
initMethod() {
this.x = 'hello';
}
async methodA() {
return this.x.length;
}
}
const service = new Service();
setTimeout(() => service.initMethod(), 1000);
service.methodA().catch(console.log);
TypeError: Cannot read properties of null (reading 'length')
at Service.methodA (<anonymous>:15:19)
at <anonymous>:20:9
at dn (<anonymous>:16:5449)
I need something like a Promise whose settled value can be set from another part of the code. Something like:
class Service {
private x: SettablePromise<string> = new SettablePromise();
initMethod() {
this.x.set('hello');
}
async methodA() {
return (await this.x).length;
}
}
const service = new Service();
setTimeout(() => service.initMethod(), 1000);
service.methodA().catch(console.log);
The best I can come up with is to make a class that polls a value until it turns non-null. I'm hoping there's something smarter. I don't want to have to fine-tune a poll interval.
Edits
Sorry for the initial confusing example. The race condition is that methodA can be called before initMethod.
There was also an unnecessary async in the initMethod. I just made it async because the real method it was based on is async.
In the following example, you can run the init before or after the async method call. Either will work -
const s = new Service()
// init
s.init()
// then query
s.query("SELECT * FROM evil").then(console.log)
const s = new Service()
// query first
s.query("SELECT * FROM evil").then(console.log)
// then init
s.init()
deferred
The solution begins with a generic deferred value that allows us to externally resolve or reject a promise -
function deferred() {
let resolve, reject
const promise = new Promise((res,rej) => {
resolve = res
reject = rej
})
return { promise, resolve, reject }
}
service
Now we will write Service which has a resource deferred value. The init method will resolve the resource at some point in time. The asynchronous method query will await the resource before it proceeds -
class Service {
resource = deferred() // deferred resource
async init() {
this.resource.resolve(await connect()) // resolve resource
}
async query(input) {
const db = await this.resource.promise // await resource
return db.query(input)
}
}
connect
This is just some example operation that we run in the init method. It returns an object with a query method that mocks a database call -
async function connect() {
await sleep(2000) // fake delay
return {
query: (input) => {
console.log("running query:", input)
return ["hello", "world"] // mock data result
}
}
}
function sleep(ms) {
return new Promise(r => setTimeout(r, ms))
}
demo
function deferred() {
let resolve, reject
const promise = new Promise((res,rej) => {
resolve = res
reject = rej
})
return { promise, resolve, reject }
}
class Service {
resource = deferred()
async init() {
this.resource.resolve(await connect())
}
async query(input) {
const db = await this.resource.promise
return db.query(input)
}
}
async function connect() {
await sleep(2000)
return {
query: (input) => {
console.log("running query:", input)
return ["hello", "world"]
}
}
}
function sleep(ms) {
return new Promise(r => setTimeout(r, ms))
}
const s = new Service()
s.query("SELECT * FROM evil").then(console.log)
s.init()
always handle rejections
If init fails to connect to the database, we need to reflect that with the resource, otherwise our program will hang indefinitely -
class Service {
resource = deferred()
async init() {
try {
const db = await timeout(connect(), 5000) // timeout
this.resource.resolve(db)
}
catch (err) {
this.resource.reject(err) // reject error
}
}
async query(input) {
const db = await timeout(this.resource.promise, 5000) // timeout
return db.query(input)
}
}
function timeout(p, ms) {
return Promise.race([
p,
sleep(ms).then(() => { throw Error("timeout") }),
])
}
As I understand it, you need to have a promise settled within constructor but it have to be resolved only when initMethod is called.
You can expose a promise resolver alongside the promise :
class Service {
private x: string | null = null;
private promise;
private resolve;
constructor() {
this.promise = new Promise(resolve => this.resolve = resolve);
}
async initMethod() {
// Do your async stuff
await new Promise(resolve => setTimeout(resolve, 1000));
this.x = 'hello';
// And resolve Service promise
this.resolve();
}
async methodA() {
await this.promise;
return this.x.length;
}
}
See Resolve Javascript Promise outside the Promise constructor scope for more examples.
You need to initialise a property with a Promise value that only resolves after initMethod() has completed.
This involves also maintaining the promise's resolve callback as a class property.
class Service {
#initResolve;
#initPromise = new Promise((resolve) => {
this.#initResolve = resolve;
});
#x = null;
async initMethod() {
await new Promise(resolve => setTimeout(resolve, 1000));
this.#x = 'hello';
this.#initResolve(); // resolve #initPromise
}
async methodA() {
await this.#initPromise;
return this.#x.length;
}
}
const service = new Service();
console.log("methodA: start");
service.methodA().then(console.log.bind(console, "methodA: end:"));
setTimeout(async () => {
console.log("initMethod: start");
await service.initMethod()
console.log("initMethod: end");
}, 1000);
Note: Using JavaScript private class features for the Snippet but I'm sure you can translate it to Typescript.

Skip waiting if async function (Promise) is taking too much time [duplicate]

This question already has answers here:
Timeout in async/await
(3 answers)
Closed 1 year ago.
In my express application, I am making call to 2 APIs. The 2nd API is managed by 3rd party and sometimes can take more than 5 seconds to respond. Hence, I want to just wait for 1 second for the API to respond. If it does not, just proceed with data from 1st API.
Below is the mock-up of the functions being called.
I am thinking to use setTimeout to throw error if the API takes more than 1 second. If the API responds within 1 second then I just cancel the setTimeout and no error is ever thrown.
But there is problem with this approach:
setTimeout errors cannot be catched using try...catch block.
I cannot use axios's timeout option, as I still need to wait for the 2nd API to finish the processing and save the data in the DB. This will ofcourse, can happen later, when the 2nd API call finishes.
// Function to simulate it's taking time.
async function cWait(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// Track whether it took time.
let isTimeOut = false
async function test() {
console.log('starting')
try {
const one = await apiCall1()
const myt = setTimeout(() => {
console.log('Its taking time, skip the 2nd API Call')
isTimeOut = true
throw new Error('Its taking time')
})
const two = await apiCall2(myt)
} catch (error) {
console.log(error)
}
saveInDB({ ...one, ...two })
}
async function apiCall2(timeOutInstance) {
console.log('start-apiCall')
await cWait(1800)
clearTimeout(timeOutInstance)
if (isTimeOut) saveInDB()
console.log('done-apiCall')
}
async function apiCall1() {
await cWait(5)
}
async function saveInDB(data) {
console.log('saveInDB')
}
test()
please note, this is not the answer as it was when it was accepted
as I misread the question and failed to call saveInDB in a timed out
situation
Promise.race seems perfect for the job
Also, you'd actually use your cWait function, not for mock-up, but to actually do something useful ... win the race :p
const api2delay = 800;
async function cWait(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
const TIMEDOUT = Symbol('TIMEDOUT');
async function cReject(ms) {
return new Promise((_, reject) => setTimeout(reject, ms, TIMEDOUT));
}
function apiCall2timeout(timeoutCallback) {
const resultApi2 = apiCall2();
const timeout = cReject(1000);
return Promise.race([resultApi2, timeout])
.catch(e => {
if (e === TIMEDOUT) {
resultApi2.then(timeoutCallback);
} else {
throw e;
}
});
}
async function test() {
console.log('starting')
let one, two;
try {
one = await apiCall1();
two = await apiCall2timeout(saveInDB);
} catch (error) {
console.log('error', error)
}
saveInDB({
...one,
...two
})
}
async function apiCall2() {
console.log('start-apiCall2')
await cWait(api2delay)
console.log('done-apiCall2')
return {
api2: 'done'
}
}
async function apiCall1() {
await cWait(5)
return {
api1: 'done'
}
}
async function saveInDB(data) {
console.log('saveInDB', data)
}
test()
Note: I changed where one and two were declared since const is block scoped
I you run with await cWait(800) in apiCall2, the saveInDB will run with both data.
But if you run await cWait(1800), the saveInDB will run 2 times.
// Function to simulate it's taking time.
async function cWait(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// https://italonascimento.github.io/applying-a-timeout-to-your-promises/
const promiseTimeout = function (ms, promise) {
// Create a promise that rejects in <ms> milliseconds
let timeout = new Promise((resolve, reject) => {
let id = setTimeout(() => {
clearTimeout(id);
reject('Timed out in ' + ms + 'ms.')
}, ms)
})
// Returns a race between our timeout and the passed in promise
return Promise.race([
promise,
timeout
])
}
// Track whether it took time.
let isTimeOut = false
async function test() {
console.log('starting')
const one = await apiCall1() // get data from 1st API
let two = {};
try {
two = await promiseTimeout(1000, apiCall2())
} catch (error) {
isTimeOut = true;
console.log(error)
}
saveInDB({ ...one, ...two })
}
async function apiCall2() {
console.log('start-apiCall')
await cWait(800)
console.log('done-apiCall', isTimeOut)
if (isTimeOut) {
saveInDB({ 2: 'two' })
}
return { 2: 'two' }
}
async function apiCall1() {
await cWait(5)
return { 1: 'one' }
}
async function saveInDB(data) {
console.log('saveInDB', data)
}
test()

NodeJS asynchronous process.on with multiple functions

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.

How do I setup this JS code to do better testing?

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();
}

How to cover setInterval in the unit test case in javascript

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)
});

Categories