From 0df1192326e3c2755532f5065a8d315af553ed14 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 23 Aug 2022 10:43:40 -1000 Subject: [PATCH 01/12] Support message coalescing --- lib/connection.ts | 88 +++++++++++++++++++++++++---------------------- lib/socket.ts | 72 ++++++++++++++++++++++++-------------- 2 files changed, 93 insertions(+), 67 deletions(-) diff --git a/lib/connection.ts b/lib/connection.ts index 5e4483cc..58983ff9 100644 --- a/lib/connection.ts +++ b/lib/connection.ts @@ -349,57 +349,63 @@ export class Connection { } private _handleMessage = (event: MessageEvent) => { - const message: WebSocketResponse = JSON.parse(event.data); + let messages: WebSocketResponse | [WebSocketResponse] = JSON.parse(event.data); - if (DEBUG) { - console.log("Received", message); + if (!Array.isArray(messages)) { + messages = [messages] } - const info = this.commands.get(message.id); - - switch (message.type) { - case "event": - if (info) { - (info as SubscribeEventCommmandInFlight).callback(message.event); - } else { - console.warn( - `Received event for unknown subscription ${message.id}. Unsubscribing.` - ); - this.sendMessagePromise(messages.unsubscribeEvents(message.id)); - } - break; + messages.forEach((message) => { + if (DEBUG) { + console.log("Received", message); + } - case "result": - // No info is fine. If just sendMessage is used, we did not store promise for result - if (info) { - if (message.success) { - info.resolve(message.result); + const info = this.commands.get(message.id); - // Don't remove subscriptions. - if (!("subscribe" in info)) { + switch (message.type) { + case "event": + if (info) { + (info as SubscribeEventCommmandInFlight).callback(message.event); + } else { + console.warn( + `Received event for unknown subscription ${message.id}. Unsubscribing.` + ); + this.sendMessagePromise(messages.unsubscribeEvents(message.id)); + } + break; + + case "result": + // No info is fine. If just sendMessage is used, we did not store promise for result + if (info) { + if (message.success) { + info.resolve(message.result); + + // Don't remove subscriptions. + if (!("subscribe" in info)) { + this.commands.delete(message.id); + } + } else { + info.reject(message.error); this.commands.delete(message.id); } - } else { - info.reject(message.error); + } + break; + + case "pong": + if (info) { + info.resolve(); this.commands.delete(message.id); + } else { + console.warn(`Received unknown pong response ${message.id}`); } - } - break; - - case "pong": - if (info) { - info.resolve(); - this.commands.delete(message.id); - } else { - console.warn(`Received unknown pong response ${message.id}`); - } - break; + break; - default: - if (DEBUG) { - console.warn("Unhandled message", message); - } - } + default: + if (DEBUG) { + console.warn("Unhandled message", message); + } + } + }) }; private _handleClose = async () => { diff --git a/lib/socket.ts b/lib/socket.ts index 044180cf..edbc6efc 100644 --- a/lib/socket.ts +++ b/lib/socket.ts @@ -16,6 +16,22 @@ export const MSG_TYPE_AUTH_REQUIRED = "auth_required"; export const MSG_TYPE_AUTH_INVALID = "auth_invalid"; export const MSG_TYPE_AUTH_OK = "auth_ok"; +type WebSocketAuthOKMessage = { + type: "auth_ok"; + ha_version: string; +}; + + +type WebSocketAuthRequiredMessage = { + type: "auth_required"; +}; + +type WebSocketAuthInvalidMessage = { + type: "auth_invalid"; +}; + +type WebSocketAuthMessage = WebSocketAuthOKMessage | WebSocketAuthRequiredMessage | WebSocketAuthInvalidMessage; + export interface HaWebSocket extends WebSocket { haVersion: string; } @@ -95,34 +111,38 @@ export function createSocket(options: ConnectionOptions): Promise { }; const handleMessage = async (event: MessageEvent) => { - const message = JSON.parse(event.data); - - if (DEBUG) { - console.log("[Auth phase] Received", message); + let messages: WebSocketAuthMessage | [WebSocketAuthMessage] = JSON.parse(event.data); + if (!Array.isArray(messages)) { + messages = [messages]; } - switch (message.type) { - case MSG_TYPE_AUTH_INVALID: - invalidAuth = true; - socket.close(); - break; - - case MSG_TYPE_AUTH_OK: - socket.removeEventListener("open", handleOpen); - socket.removeEventListener("message", handleMessage); - socket.removeEventListener("close", closeMessage); - socket.removeEventListener("error", closeMessage); - socket.haVersion = message.ha_version; - promResolve(socket); - break; - - default: - if (DEBUG) { - // We already send response to this message when socket opens - if (message.type !== MSG_TYPE_AUTH_REQUIRED) { - console.warn("[Auth phase] Unhandled message", message); + messages.forEach((message) => { + if (DEBUG) { + console.log("[Auth phase] Received", message); + } + switch (message.type) { + case MSG_TYPE_AUTH_INVALID: + invalidAuth = true; + socket.close(); + break; + + case MSG_TYPE_AUTH_OK: + socket.removeEventListener("open", handleOpen); + socket.removeEventListener("message", handleMessage); + socket.removeEventListener("close", closeMessage); + socket.removeEventListener("error", closeMessage); + socket.haVersion = message.ha_version; + promResolve(socket); + break; + + default: + if (DEBUG) { + // We already send response to this message when socket opens + if (message.type !== MSG_TYPE_AUTH_REQUIRED) { + console.warn("[Auth phase] Unhandled message", message); + } } - } - } + } + ) }; socket.addEventListener("open", handleOpen); From 6503773c3b35cbdba0365ae9d42ba9f635e5151e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 23 Aug 2022 18:13:33 -0700 Subject: [PATCH 02/12] naming --- lib/connection.ts | 10 +++++----- lib/socket.ts | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/connection.ts b/lib/connection.ts index 58983ff9..4722381f 100644 --- a/lib/connection.ts +++ b/lib/connection.ts @@ -349,13 +349,13 @@ export class Connection { } private _handleMessage = (event: MessageEvent) => { - let messages: WebSocketResponse | [WebSocketResponse] = JSON.parse(event.data); + let message_group: WebSocketResponse | [WebSocketResponse] = JSON.parse(event.data); - if (!Array.isArray(messages)) { - messages = [messages] + if (!Array.isArray(message_group)) { + message_group = [message_group]; } - messages.forEach((message) => { + message_group.forEach((message) => { if (DEBUG) { console.log("Received", message); } @@ -405,7 +405,7 @@ export class Connection { console.warn("Unhandled message", message); } } - }) + }); }; private _handleClose = async () => { diff --git a/lib/socket.ts b/lib/socket.ts index edbc6efc..81fd815d 100644 --- a/lib/socket.ts +++ b/lib/socket.ts @@ -111,11 +111,11 @@ export function createSocket(options: ConnectionOptions): Promise { }; const handleMessage = async (event: MessageEvent) => { - let messages: WebSocketAuthMessage | [WebSocketAuthMessage] = JSON.parse(event.data); - if (!Array.isArray(messages)) { - messages = [messages]; + let message_group: WebSocketAuthMessage | [WebSocketAuthMessage] = JSON.parse(event.data); + if (!Array.isArray(message_group)) { + message_group = [message_group]; } - messages.forEach((message) => { + message_group.forEach((message) => { if (DEBUG) { console.log("[Auth phase] Received", message); } @@ -142,7 +142,7 @@ export function createSocket(options: ConnectionOptions): Promise { } } } - ) + }); }; socket.addEventListener("open", handleOpen); From a04479bbeee8354b90af1930870dac5d6ae02542 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 24 Aug 2022 13:07:01 -0500 Subject: [PATCH 03/12] Update lib/connection.ts Co-authored-by: Paulus Schoutsen --- lib/connection.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/connection.ts b/lib/connection.ts index 4722381f..1e1e9fe0 100644 --- a/lib/connection.ts +++ b/lib/connection.ts @@ -349,7 +349,7 @@ export class Connection { } private _handleMessage = (event: MessageEvent) => { - let message_group: WebSocketResponse | [WebSocketResponse] = JSON.parse(event.data); + let message_group: WebSocketResponse | WebSocketResponse[] = JSON.parse(event.data); if (!Array.isArray(message_group)) { message_group = [message_group]; From fb5f97476f46cddcacbd55cec96f3a8667eeae15 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 24 Aug 2022 13:08:04 -0500 Subject: [PATCH 04/12] reduce --- lib/socket.ts | 55 +++++++++++++++++++++++---------------------------- 1 file changed, 25 insertions(+), 30 deletions(-) diff --git a/lib/socket.ts b/lib/socket.ts index 81fd815d..294abbbd 100644 --- a/lib/socket.ts +++ b/lib/socket.ts @@ -111,38 +111,33 @@ export function createSocket(options: ConnectionOptions): Promise { }; const handleMessage = async (event: MessageEvent) => { - let message_group: WebSocketAuthMessage | [WebSocketAuthMessage] = JSON.parse(event.data); - if (!Array.isArray(message_group)) { - message_group = [message_group]; + const message: WebSocketAuthMessage = JSON.parse(event.data); + if (DEBUG) { + console.log("[Auth phase] Received", message); } - message_group.forEach((message) => { - if (DEBUG) { - console.log("[Auth phase] Received", message); - } - switch (message.type) { - case MSG_TYPE_AUTH_INVALID: - invalidAuth = true; - socket.close(); - break; - - case MSG_TYPE_AUTH_OK: - socket.removeEventListener("open", handleOpen); - socket.removeEventListener("message", handleMessage); - socket.removeEventListener("close", closeMessage); - socket.removeEventListener("error", closeMessage); - socket.haVersion = message.ha_version; - promResolve(socket); - break; - - default: - if (DEBUG) { - // We already send response to this message when socket opens - if (message.type !== MSG_TYPE_AUTH_REQUIRED) { - console.warn("[Auth phase] Unhandled message", message); - } + switch (message.type) { + case MSG_TYPE_AUTH_INVALID: + invalidAuth = true; + socket.close(); + break; + + case MSG_TYPE_AUTH_OK: + socket.removeEventListener("open", handleOpen); + socket.removeEventListener("message", handleMessage); + socket.removeEventListener("close", closeMessage); + socket.removeEventListener("error", closeMessage); + socket.haVersion = message.ha_version; + promResolve(socket); + break; + + default: + if (DEBUG) { + // We already send response to this message when socket opens + if (message.type !== MSG_TYPE_AUTH_REQUIRED) { + console.warn("[Auth phase] Unhandled message", message); } - } - }); + } + } }; socket.addEventListener("open", handleOpen); From a21cd668a4b2f8b0e5f529b3d9dccc5012383ea6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 24 Aug 2022 13:08:35 -0500 Subject: [PATCH 05/12] revert socket changes --- lib/socket.ts | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/lib/socket.ts b/lib/socket.ts index 294abbbd..044180cf 100644 --- a/lib/socket.ts +++ b/lib/socket.ts @@ -16,22 +16,6 @@ export const MSG_TYPE_AUTH_REQUIRED = "auth_required"; export const MSG_TYPE_AUTH_INVALID = "auth_invalid"; export const MSG_TYPE_AUTH_OK = "auth_ok"; -type WebSocketAuthOKMessage = { - type: "auth_ok"; - ha_version: string; -}; - - -type WebSocketAuthRequiredMessage = { - type: "auth_required"; -}; - -type WebSocketAuthInvalidMessage = { - type: "auth_invalid"; -}; - -type WebSocketAuthMessage = WebSocketAuthOKMessage | WebSocketAuthRequiredMessage | WebSocketAuthInvalidMessage; - export interface HaWebSocket extends WebSocket { haVersion: string; } @@ -111,7 +95,8 @@ export function createSocket(options: ConnectionOptions): Promise { }; const handleMessage = async (event: MessageEvent) => { - const message: WebSocketAuthMessage = JSON.parse(event.data); + const message = JSON.parse(event.data); + if (DEBUG) { console.log("[Auth phase] Received", message); } From 05767747fcc552b1a3d630f8b4c7934ccc69d5e1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 24 Aug 2022 14:14:58 -0500 Subject: [PATCH 06/12] prettier --- lib/connection.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/connection.ts b/lib/connection.ts index 1e1e9fe0..a50dd864 100644 --- a/lib/connection.ts +++ b/lib/connection.ts @@ -349,13 +349,15 @@ export class Connection { } private _handleMessage = (event: MessageEvent) => { - let message_group: WebSocketResponse | WebSocketResponse[] = JSON.parse(event.data); + let message_group: WebSocketResponse | WebSocketResponse[] = JSON.parse( + event.data + ); if (!Array.isArray(message_group)) { message_group = [message_group]; } - message_group.forEach((message) => { + message_group.forEach((message) => { if (DEBUG) { console.log("Received", message); } @@ -365,7 +367,9 @@ export class Connection { switch (message.type) { case "event": if (info) { - (info as SubscribeEventCommmandInFlight).callback(message.event); + (info as SubscribeEventCommmandInFlight).callback( + message.event + ); } else { console.warn( `Received event for unknown subscription ${message.id}. Unsubscribing.` From 06a93945ccacedd39540594ddf54d958e9130b27 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 24 Aug 2022 15:03:15 -0500 Subject: [PATCH 07/12] send supported features right after auth --- lib/connection.ts | 2 +- lib/messages.ts | 8 ++++++++ lib/socket.ts | 5 +++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/lib/connection.ts b/lib/connection.ts index a50dd864..da3e548e 100644 --- a/lib/connection.ts +++ b/lib/connection.ts @@ -105,7 +105,7 @@ export class Connection { // - createSocket: create a new Socket connection this.options = options; // id if next command to send - this.commandId = 1; + this.commandId = 2; // socket may send 1 at the start to enable features // info about active subscriptions and commands in flight this.commands = new Map(); // map of event listeners diff --git a/lib/messages.ts b/lib/messages.ts index 57258c13..5b86060a 100644 --- a/lib/messages.ts +++ b/lib/messages.ts @@ -7,6 +7,14 @@ export function auth(accessToken: string) { }; } +export function supported_features() { + return { + type: "supported_features", + id: 1, // Always the first message after auth + features: { coalesce_messages: 1.0 }, + }; +} + export function states() { return { type: "get_states", diff --git a/lib/socket.ts b/lib/socket.ts index 044180cf..1996d94a 100644 --- a/lib/socket.ts +++ b/lib/socket.ts @@ -9,6 +9,7 @@ import { import { Error } from "./types.js"; import type { ConnectionOptions } from "./connection.js"; import * as messages from "./messages.js"; +import { atLeastHaVersion } from "./util.js"; const DEBUG = false; @@ -112,6 +113,10 @@ export function createSocket(options: ConnectionOptions): Promise { socket.removeEventListener("close", closeMessage); socket.removeEventListener("error", closeMessage); socket.haVersion = message.ha_version; + if (atLeastHaVersion(socket.haVersion, 2022, 9, 0)) { + socket.send(JSON.stringify(messages.supported_features())); + } + promResolve(socket); break; From 1afecb83808b07a95d59e9598953cb94d3ef36d5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 24 Aug 2022 16:17:17 -0500 Subject: [PATCH 08/12] Update lib/messages.ts --- lib/messages.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/messages.ts b/lib/messages.ts index 5b86060a..10c8876f 100644 --- a/lib/messages.ts +++ b/lib/messages.ts @@ -11,7 +11,7 @@ export function supported_features() { return { type: "supported_features", id: 1, // Always the first message after auth - features: { coalesce_messages: 1.0 }, + features: { coalesce_messages: 1 }, }; } From e61aa1ffb605472d77334f095e202a2cbffe5567 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 24 Aug 2022 16:17:30 -0500 Subject: [PATCH 09/12] Update lib/socket.ts Co-authored-by: Paulus Schoutsen --- lib/socket.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/socket.ts b/lib/socket.ts index 1996d94a..d3f83da2 100644 --- a/lib/socket.ts +++ b/lib/socket.ts @@ -113,7 +113,7 @@ export function createSocket(options: ConnectionOptions): Promise { socket.removeEventListener("close", closeMessage); socket.removeEventListener("error", closeMessage); socket.haVersion = message.ha_version; - if (atLeastHaVersion(socket.haVersion, 2022, 9, 0)) { + if (atLeastHaVersion(socket.haVersion, 2022, 9)) { socket.send(JSON.stringify(messages.supported_features())); } From 5b5d3bf7746b61e165e8f28fc75ab9f15e579968 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 24 Aug 2022 20:46:40 -0500 Subject: [PATCH 10/12] camel --- lib/connection.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/connection.ts b/lib/connection.ts index da3e548e..bf2a71f8 100644 --- a/lib/connection.ts +++ b/lib/connection.ts @@ -349,15 +349,15 @@ export class Connection { } private _handleMessage = (event: MessageEvent) => { - let message_group: WebSocketResponse | WebSocketResponse[] = JSON.parse( + let messageGroup: WebSocketResponse | WebSocketResponse[] = JSON.parse( event.data ); - if (!Array.isArray(message_group)) { - message_group = [message_group]; + if (!Array.isArray(messageGroup)) { + messageGroup = [messageGroup]; } - message_group.forEach((message) => { + messageGroup.forEach((message) => { if (DEBUG) { console.log("Received", message); } From 2888392eb4c60ddfc45e42bdfd0c5a702bcab9ba Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 24 Aug 2022 20:47:16 -0500 Subject: [PATCH 11/12] camel --- lib/socket.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/socket.ts b/lib/socket.ts index d3f83da2..2839dbb6 100644 --- a/lib/socket.ts +++ b/lib/socket.ts @@ -114,7 +114,7 @@ export function createSocket(options: ConnectionOptions): Promise { socket.removeEventListener("error", closeMessage); socket.haVersion = message.ha_version; if (atLeastHaVersion(socket.haVersion, 2022, 9)) { - socket.send(JSON.stringify(messages.supported_features())); + socket.send(JSON.stringify(messages.supportedFeatures())); } promResolve(socket); From bc42fc77807b1d7fe1a291593163f53be10d616e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 24 Aug 2022 20:48:25 -0500 Subject: [PATCH 12/12] camel --- lib/messages.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/messages.ts b/lib/messages.ts index 10c8876f..2e6c3d06 100644 --- a/lib/messages.ts +++ b/lib/messages.ts @@ -7,7 +7,7 @@ export function auth(accessToken: string) { }; } -export function supported_features() { +export function supportedFeatures() { return { type: "supported_features", id: 1, // Always the first message after auth