Mark subscribe() and unsubscribe() as async #678
Conversation
Codecov Report
@@ Coverage Diff @@
## main #678 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 19 19
Lines 531 531
Branches 39 39
=========================================
Hits 531 531
Flags with carried forward coverage won't be shown. Click here to find out more.
|
@bennycode This is ready for review. There is one additional question I have and would like your input. Here's the code in question: coinbase-pro-node/src/client/WebSocketClient.ts Lines 483 to 502 in d48e45e
In the sendMessage() function, we check for a non-existent socket object at the beginning of the call. This is most likely necessary for signRequest to work. However, if someone happens to call subscribe() without await, call flow would return back to the original caller while signRequest is being awaited. If the original caller happened to immediately call disconnect(), the socket object would be set to undefined. Then when signRequest eventually returns, this code runs:
This obviously results in the error that we are calling a function on an undefined object. What I'm wondering is if we should check for an undefined socket object both before AND after the call to signRequest() function? We would at least at that point be able to throw a meaningful exception. I did in fact try this using this code: async sendMessage(message: WebSocketRequest): Promise<void> {
const noSocketError = `Failed to send message of type "${message.type}": You need to connect to the WebSocket first.`;
if (!this.socket) {
throw new Error(noSocketError);
}
/**
* Authentication will result in a couple of benefits:
* 1. Messages where you're one of the parties are expanded and have more useful fields
* 2. You will receive private messages, such as lifecycle information about stop orders you placed
* @see https://docs.pro.coinbase.com/#subscribe
*/
const signature = await this.signRequest({
httpMethod: 'GET',
payload: '',
requestPath: `${UserAPI.URL.USERS}/self/verify`,
});
Object.assign(message, signature);
/*
* We retest here because someone may have called this function without awaiting it and then then
* when signRequest() is awaited process flow returns to calling code which could call disconnect().
* Process flow would eventually return from signRequest() and there would no longer be a socket
* object even though there was a socket object at the beginning of this function call.
*/
if (!this.socket) {
throw new Error(noSocketError);
}
this.socket.send(JSON.stringify(message));
} However, code coverage complained about not hitting the second throw line (right before the call to send()). I can't add a test with just |
We found that if sendMessage() was called but not awaited on, the code execution would get to the awaited signRequest() call and then immediately return execution to caller of sendMessage() before signRequest() returns. If the caller then calls disconnect(), when signRequest() returns, the socket would be undefined, even though our test for undefined above would find it existing.
@bennycode I thought about this some more and realized that I was thinking the signRequest() call required the socket object. However, that is not the case. (It uses the REST API for time call, not WebSocket.) So, I just moved the test for undefined socket after the signRequest() call. This fixes the issue if someone calls sendMessage() without await and then immediately calls disconnect(). |
This PR is intended to solve the issue described in #661. Basically, because subscribe() called sendMessage() which was an async function without awaiting it, subscribe would immediately return process flow back to the calling code. If the calling code happened to call disconnect()--which sets this.socket to undefined--when the call to sendMessage() returns, subscribe() would reference a non-existent socket object when sending the subscribe command.