liamcottle / rustplus.js

Unofficial NodeJS library for controlling Smart Switches in the PC game Rust
224 stars 46 forks source link

Error-first callbacks #18

Closed raine closed 3 years ago

raine commented 3 years ago

Hey. For instance the sendRequest method can't be promisified with the common tools because it's not returning the error first in the callback. Even if error wasn't possible due to the websocket architecture of listening to messages in the socket and picking the right callback based on seq, I think the first argument to the callback should be null.

Thoughts?

liamcottle commented 3 years ago

I'm not too sure what you mean here, could you elaborate with some examples?

raine commented 3 years ago

In node.js, it's idiomatic to pass an error, if any, as the first argument in callback functions to indicate if a call resulted in an error.

Promisifying functions such as util.promisify rely on this behavior.

In the case here, callbacks are called with the message as first argument: https://github.com/liamcottle/rustplus.js/blob/d3628d7a230648297e5c0425763ef127279f9469/rustplus.js#L80

Hence, if you try promisifying sendRequest or other methods, you'll see that successful response is registered as an error instead.

  const util = require('util')
  const rustplus = new RustPlus(...)

  const sendRequestAsync = util.promisify(rustplus.sendRequest.bind(rustplus))

  rustplus.on('connected', () => {
    sendRequestAsync({
      getInfo: {}
    })
      .then((res) => {
        console.log('result', res)
      })
      .catch((err) => {
        console.log('error', err) // logs the appinfo object
      })
  })
liamcottle commented 3 years ago

Ahh, I see what you mean now.

If I were to change the callback signature for sendRequest from (message) to (error, message), it would probably make sense for the sendRequest method to map all AppError responses to error and all others to message.

So it would end up being something like this:

rustplus.on('connected', () => {
    sendRequestAsync({
        getInfo: {}
    }).then((result) => {
        console.log('result', result); // this would be an AppResponse instance
    }).catch((error) => {
        console.log('error', error); // this would be an AppError instance
    });
});

The issue I have with doing this, is that in the case of catching it as an error, you would only be getting the AppError object, which means you lose access to the seq that's available in the parent AppResponse. Of course that's not too important, but in the future there could be other values that are needed or available.

To get around that, we could have it return the AppResponse, but then in that case, we now have then and catch both providing an AppResponse and we have to double handle.

Maybe there's a better way to get promises working with this. But then again, I'm unsure if it should be in the core of this library or not, because you may not ever get a result from the server for your request, which means the promise would never be fulfilled, and if you were to await the promise for some reason, it would just hang.

Is there a specific reason you're trying to turn sendRequest into sendRequestAsync ? As far as I'm aware, all requests would be synchronous at the websocket level anyway, since packets have to be fully sent before another can be sent.

Alternatively, you could wrap the response from the callback into your own promise if you're trying to pass it through processing pipes etc.

What are you thoughts on it?

raine commented 3 years ago

Maybe there's a better way to get promises working with this. But then again, I'm unsure if it should be in the core of this library or not, because you may not ever get a result from the server for your request, which means the promise would never be fulfilled, and if you were to await the promise for some reason, it would just hang.

I think it would ideal for the library to handle the timeout scenario by throwing a timeout error.

Is there a specific reason you're trying to turn sendRequest into sendRequestAsync ? As far as I'm aware, all requests would be synchronous at the websocket level anyway, since packets have to be fully sent before another can be sent.

I'm not sure what you mean, a promise API is nicer to work with.

Here's what I've ended up with for now, subject to change as I make progress: https://github.com/raine/ruoste_bot/blob/master/src/rustplus/rustplus-socket.ts#L93-L102

liamcottle commented 3 years ago

Cool, yeah handling timeouts in the library makes sense.

I've implemented sendRequestAsync in v2.1.1 and have added example 6_async_requests.js

Let me know what you think :)

raine commented 3 years ago

Thank you, it's great. Started using it right away.

I added a small wrapper around it still to throw a more readable error in case it's called before .connect(). This might happen if my bot is missing some rustplus configuration and hence not connecting.

ERROR: Cannot read property 'fromObject' of undefined
    TypeError: Cannot read property 'fromObject' of undefined
        at RustPlus.sendRequest (node_modules/@liamcottle/rustplus.js/rustplus.js:138:39)

^ You get that if you call sendRequest() before .connect()