home-assistant / home-assistant-js-websocket

:aerial_tramway: JavaScript websocket client for Home Assistant

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Await promise in event handler

RonnyWinkler opened this issue · comments

First a question:
In my case, the app tries to reconnect to HomeAssistant after a "heartbeat" check registeres that the connection is not possible. After the old connection is closed/cleared and a new connection is established, all "old" subscriptions are unsubscribed and and subscribed again (connection.removeEventListener() and call the unsibscribe promise collected from connection.subscribeEvents() ).
In this case I get several event log like: Received event for unknown subscription 15. Unsubscribing.
Is this the correct way it should be done?
Or should the connection never be cleared and a kind of soft-rerconnect shoule be done?
Or should I count on the auto-reconnect of the module in this case and I shoud not reconnect creating a new connection instance?

Now an issue in event handler haws.cjs:

    constructor(socket, options) {
        this._handleMessage = (event) => {
            let messageGroup = JSON.parse(event.data);
            if (!Array.isArray(messageGroup)) {
                messageGroup = [messageGroup];
            }
            messageGroup.forEach((message) => {
                const info = this.commands.get(message.id);
                switch (message.type) {
                    case "event":
                        if (info) {
                            info.callback(message.event);
                        }
                        else {
                            console.warn(`Received event for unknown subscription ${message.id}. Unsubscribing.`);
                            this.sendMessagePromise(unsubscribeEvents(message.id));
                        }
                        break;

The event handler only uses sync handler functions, but calls a promise inside.
This promise can be rejected. Then You get such unhandled errors:
Unhandled Rejection at: Promise { <rejected> 3 } reason: 3

Shouldn't the handler be async and try/catch the promise call like this (or similar)?:

                const info = this.commands.get(message.id);
                switch (message.type) {
                    case "event":
                        if (info) {
                            info.callback(message.event);
                        }
                        else {
                            console.warn(`Received event for unknown subscription ${message.id}. Unsubscribing.`);
                            try{
                                 this.sendMessagePromise(unsubscribeEvents(message.id));
                            }
                            catch(error){
                                console.warn(`Error unsubscribing unknown subscription ${message.id}.`);
                            }
                        }
                        break;

There is another place where an "await" could prevent unhandled exceptions:

haws.cjs

    async subscribeMessage(callback, subscribeMessage, options) {
        if (this._queuedMessages) {
            await new Promise((resolve, reject) => {
                this._queuedMessages.push({ resolve, reject });
            });
        }
        let info;
        await new Promise((resolve, reject) => {
            // Command ID that will be used
            const commandId = this._genCmdId();
            // We store unsubscribe on info object. That way we can overwrite it in case
            // we get disconnected and we have to subscribe again.
            info = {
                resolve,
                reject,
                callback,
                subscribe: (options === null || options === void 0 ? void 0 : options.resubscribe) !== false
                    ? () => this.subscribeMessage(callback, subscribeMessage)
                    : undefined,
                unsubscribe: async () => {
                    // No need to unsubscribe if we're disconnected
                    if (this.connected) {
                        // old version without await will cause unhandled exception it it thows an exception (HA restart...)
                        // await this.sendMessagePromise(unsubscribeEvents(commandId));
                        // suggestion >>>
                        try{
                            await this.sendMessagePromise(unsubscribeEvents(commandId));
                        }
                        catch(error){
                            console.error(`Error unsubscribing event, command ID: ${commandId}.`);
                        }
                        // <<<
                    }
                    this.commands.delete(commandId);

As per the type, event listeners are not expected to be async, and so we shouldn't accommodate that.

But you are calling an async function/promise this.sendMessagePromise from a sync function.
And that's causing unhandled exceptions if tje promise is rejected later.