From 16e177e00e49c9cdf067ac2e6f75ec45fc2cc70e Mon Sep 17 00:00:00 2001 From: Chong Wen Hao <58220142+8kdesign@users.noreply.github.com> Date: Fri, 1 Mar 2024 12:37:07 +0800 Subject: [PATCH 01/14] Added communication library --- modules.json | 3 + package.json | 10 +- src/bundles/communication/Communications.ts | 123 ++++++++++ .../communication/GlobalStateController.ts | 117 +++++++++ src/bundles/communication/MqttController.ts | 103 ++++++++ .../communication/MultiUserController.ts | 62 +++++ src/bundles/communication/RpcController.ts | 123 ++++++++++ src/bundles/communication/__tests__/index.ts | 224 ++++++++++++++++++ src/bundles/communication/index.ts | 28 +++ yarn.lock | 217 +++++++++++++++-- 10 files changed, 987 insertions(+), 23 deletions(-) create mode 100644 src/bundles/communication/Communications.ts create mode 100644 src/bundles/communication/GlobalStateController.ts create mode 100644 src/bundles/communication/MqttController.ts create mode 100644 src/bundles/communication/MultiUserController.ts create mode 100644 src/bundles/communication/RpcController.ts create mode 100644 src/bundles/communication/__tests__/index.ts create mode 100644 src/bundles/communication/index.ts diff --git a/modules.json b/modules.json index 7e3cc6942..b7d28dfdf 100644 --- a/modules.json +++ b/modules.json @@ -98,5 +98,8 @@ "tabs": [ "physics_2d" ] + }, + "communication": { + "tabs": [] } } \ No newline at end of file diff --git a/package.json b/package.json index 289ca538e..e4e9dbc1f 100644 --- a/package.json +++ b/package.json @@ -107,9 +107,12 @@ "ace-builds": "^1.25.1", "classnames": "^2.3.1", "dayjs": "^1.10.4", + "events": "^3.3.0", "gl-matrix": "^3.3.0", "js-slang": "^1.0.20", "lodash": "^4.17.21", + "mqtt": "^4.3.7", + "os": "^0.1.2", "patch-package": "^6.5.1", "phaser": "^3.54.0", "plotly.js-dist": "^2.17.1", @@ -121,7 +124,9 @@ "save-file": "^2.3.1", "source-academy-utils": "^1.0.0", "source-academy-wabt": "^1.0.4", - "tslib": "^2.3.1" + "tslib": "^2.3.1", + "uniqid": "^5.4.0", + "url": "^0.11.3" }, "jest": { "projects": [ @@ -131,6 +136,7 @@ }, "resolutions": { "@types/react": "^18.2.0", - "esbuild": "^0.18.20" + "esbuild": "^0.18.20", + "**/gl": "^6.0.2" } } diff --git a/src/bundles/communication/Communications.ts b/src/bundles/communication/Communications.ts new file mode 100644 index 000000000..8b1781fb3 --- /dev/null +++ b/src/bundles/communication/Communications.ts @@ -0,0 +1,123 @@ +import context from 'js-slang/context'; +import { MultiUserController } from './MultiUserController'; +import { GlobalStateController } from './GlobalStateController'; +import { RpcController } from './RpcController'; + +class CommunicationModuleState { + multiUser: MultiUserController; + globalState: GlobalStateController | null = null; + rpc: RpcController | null = null; + + constructor(address: string, port: number) { + let multiUser = new MultiUserController(); + multiUser.setupController(address, port); + this.multiUser = multiUser; + } +} + +let moduleState = new CommunicationModuleState('broker.hivemq.com', 8884); +context.moduleContexts.communication.state = moduleState; + +// Loop + +let interval: number | undefined; + +/** + * Keeps the program running so that messages can come in. + */ +export function keepRunning() { + interval = window.setInterval(() => {}, 20000); +} + +/** + * Removes interval that keeps the program running. + */ +export function stopRunning() { + if (interval) { + window.clearInterval(interval); + interval = undefined; + } +} + +// Global State + +/** + * Initializes global state. + * + * @param topicHeader MQTT topic to use for global state. + * @param callback Callback to receive updates of global state. + */ +export function initGlobalState( + topicHeader: string, + callback: (state: any) => void, +) { + moduleState.globalState = new GlobalStateController( + topicHeader, + moduleState.multiUser, + callback, + ); +} + +/** + * Obtains the current global state. + * + * @returns Current global state. + */ +export function getGlobalState() { + return moduleState.globalState?.globalState; +} + +/** + * Broadcasts the new states to all devices. + * Has ability to modify only part of the JSON state. + * + * @param path Path within the json state. + * @param updatedState Replacement value at specified path. + */ +export function updateGlobalState(path: string, updatedState: any) { + moduleState.globalState?.updateGlobalState(path, updatedState); +} + +// Rpc + +/** + * Initializes RPC. + * + * @param topicHeader MQTT topic to use for rpc. + * @param userId Identifier for this user. + */ +export function initRpc(topicHeader: string, userId?: string) { + moduleState.rpc = new RpcController( + topicHeader, + moduleState.multiUser, + userId, + ); +} + +/** + * Exposes the specified function to other users. + * Other users can use "callFunction" to call this function. + * + * @param name Identifier for the function. + * @param func Function to call when request received. + */ +export function expose(name: string, func: (...args: any[]) => any) { + moduleState.rpc?.expose(name, func); +} + +/** + * Calls a function exposed by another user. + * + * @param receiver Identifier for the user whose function we want to call. + * @param name Identifier for function to call. + * @param args Array of arguments to pass into the function. + * @param callback Callback with return value. + */ +export function callFunction( + receiver: string, + name: string, + args: any[], + callback: (args: any[]) => void, +) { + moduleState.rpc?.callFunction(receiver, name, args, callback); +} diff --git a/src/bundles/communication/GlobalStateController.ts b/src/bundles/communication/GlobalStateController.ts new file mode 100644 index 000000000..98df10cb6 --- /dev/null +++ b/src/bundles/communication/GlobalStateController.ts @@ -0,0 +1,117 @@ +import { type MultiUserController } from './MultiUserController'; + +/** + * Controller for maintaining a global state across all devices. + * Depends on MQTT implementation in MultiUserController. + * + * @param topicHeader Identifier for all global state messages, must not include '/'. + * @param multiUser Instance of multi user controller. + * @param callback Callback called when the global state changes. + */ +export class GlobalStateController { + private topicHeader: string; + private multiUser: MultiUserController; + private callback: (state: any) => void; + globalState: any; + + constructor( + topicHeader: string, + multiUser: MultiUserController, + callback: (state: any) => void, + ) { + this.topicHeader = topicHeader; + this.multiUser = multiUser; + this.callback = callback; + this.setupGlobalState(); + } + + /** + * Sets up callback for global state messages. + * Parses received message and stores it as global state. + */ + private setupGlobalState() { + if (this.topicHeader.length <= 0) return; + this.multiUser.addMessageCallback(this.topicHeader, (topic, message) => { + let shortenedTopic = topic.substring( + this.topicHeader.length, + topic.length, + ); + this.parseGlobalStateMessage(shortenedTopic, message); + }); + } + + public parseGlobalStateMessage(shortenedTopic: string, message: string) { + let preSplitTopic = shortenedTopic.trim(); + if (preSplitTopic.length === 0) { + try { + this.setGlobalState(JSON.parse(message)); + } catch { + this.setGlobalState(undefined); + } + return; + } + if (!preSplitTopic.startsWith('/')) { + preSplitTopic = `/${preSplitTopic}`; + } + let splitTopic = preSplitTopic.split('/'); + try { + let newGlobalState = { ...this.globalState }; + if ( + this.globalState instanceof Array || + typeof this.globalState === 'string' + ) { + newGlobalState = {}; + } + let currentJson = newGlobalState; + for (let i = 1; i < splitTopic.length - 1; i++) { + let subTopic = splitTopic[i]; + if ( + !(currentJson[subTopic] instanceof Object) || + currentJson[subTopic] instanceof Array || + typeof currentJson[subTopic] === 'string' + ) { + currentJson[subTopic] = {}; + } + currentJson = currentJson[subTopic]; + } + if (message === undefined || message.length === 0) { + delete currentJson[splitTopic[splitTopic.length - 1]]; + } else { + let jsonMessage = JSON.parse(message); + currentJson[splitTopic[splitTopic.length - 1]] = jsonMessage; + } + this.setGlobalState(newGlobalState); + } catch {} + } + + /** + * Sets the new global state and calls the callback to notify changes. + * + * @param newState New state received. + */ + private setGlobalState(newState: any) { + this.globalState = newState; + this.callback(newState); + } + + /** + * Broadcasts the new states to all devices. + * Has ability to modify only part of the JSON state. + * + * @param path Path within the json state. + * @param updatedState Replacement value at specified path. + */ + public updateGlobalState(path: string, updatedState: any) { + if (this.topicHeader.length === 0) return; + let topic = this.topicHeader; + if (path.length !== 0 && !path.startsWith('/')) { + topic += '/'; + } + topic += path; + this.multiUser.controller?.publish( + topic, + JSON.stringify(updatedState), + false, + ); + } +} diff --git a/src/bundles/communication/MqttController.ts b/src/bundles/communication/MqttController.ts new file mode 100644 index 000000000..db5b7a5d0 --- /dev/null +++ b/src/bundles/communication/MqttController.ts @@ -0,0 +1,103 @@ +import { connect, type MqttClient } from 'mqtt/dist/mqtt'; + +export const STATE_CONNECTED = 'Connected'; +export const STATE_DISCONNECTED = 'Disconnected'; +export const STATE_RECONNECTED = 'Reconnected'; +export const STATE_OFFLINE = 'Offline'; + +/** + * Abstraction of MQTT. + * + * @param connectionCallback Callback when the connection state changed. + * @param messageCallback Callback when a message has been received. + */ +export class MqttController { + private client: MqttClient | null = null; + private connected: boolean = false; + private connectionCallback: (status: string) => void; + private messageCallback: (topic: string, message: string) => void; + + address: string = ''; + port: number = 8080; + + constructor( + connectionCallback: (status: string) => void, + messageCallback: (topic: string, message: string) => void, + ) { + this.connectionCallback = connectionCallback; + this.messageCallback = messageCallback; + } + + /** + * Sets up MQTT client link and connects to it. + * Also handles connection status callbacks. + */ + public connectClient() { + if (this.connected || this.address.length === 0) return; + let link = `wss://${this.address}:${this.port}/mqtt`; + this.client = connect(link); + this.connected = true; + this.client.on('connect', () => { + this.connectionCallback(STATE_CONNECTED); + }); + this.client.on('disconnect', () => { + this.connectionCallback(STATE_DISCONNECTED); + }); + this.client.on('reconnect', () => { + this.connectionCallback(STATE_RECONNECTED); + }); + this.client.on('offline', () => { + this.connectionCallback(STATE_OFFLINE); + }); + this.client.on('message', (topic, message) => { + let decoder = new TextDecoder('utf-8'); + this.messageCallback(topic, decoder.decode(message)); + }); + } + + /** + * Disconnects the MQTT client. + */ + public disconnect() { + if (this.client) { + this.client.end(true); + } + this.connectionCallback = () => {}; + this.messageCallback = () => {}; + } + + /** + * Broadcasts message to topic. + * QoS of 1, all listening devices receive the message at least once. + * + * @param topic Identifier for group of devices to broadcast to. + * @param message Message to broadcast. + * @param isRetain Whether the message should be retained. + */ + public publish(topic: string, message: string, isRetain: boolean) { + this.client?.publish(topic, message, { + qos: 1, + retain: isRetain, + }); + } + + /** + * Subscribes to a topic. + * + * @param topic Identifier for group of devices receiving the broadcast. + */ + public subscribe(topic: string) { + if (this.client) { + this.client.subscribe(topic); + } + } + + /** + * Unsubscribes from a topic. + * + * @param topic Identifier for group of devices receiving the broadcast. + */ + public unsubscribe(topic: string) { + this.client?.unsubscribe(topic); + } +} diff --git a/src/bundles/communication/MultiUserController.ts b/src/bundles/communication/MultiUserController.ts new file mode 100644 index 000000000..6d1a13b2b --- /dev/null +++ b/src/bundles/communication/MultiUserController.ts @@ -0,0 +1,62 @@ +import { MqttController, STATE_DISCONNECTED } from './MqttController'; + +/** + * Controller with implementation of MQTT. + * Required by both GlobalStateController and RpcController. + */ +export class MultiUserController { + controller: MqttController | null = null; + connectionState: string = STATE_DISCONNECTED; + messageCallbacks: Map void> = + new Map(); + + /** + * Sets up and connect to the MQTT link. + * Uses websocket implementation. + * + * @param address Address to connect to. + * @param port MQTT port number. + */ + public setupController(address: string, port: number) { + let currentController = this.controller; + if (currentController !== null) { + currentController.disconnect(); + this.connectionState = STATE_DISCONNECTED; + } else { + currentController = new MqttController( + (status: string) => { + this.connectionState = status; + }, + (topic: string, message: string) => { + let splitTopic = topic.split('/'); + this.messageCallbacks.forEach((callback, identifier) => { + let splitIdentifier = identifier.split('/'); + if (splitTopic.length < splitIdentifier.length) return; + for (let i = 0; i < splitIdentifier.length; i++) { + if (splitIdentifier[i] !== splitTopic[i]) return; + } + callback(topic, message); + }); + }, + ); + this.controller = currentController; + } + currentController.address = address; + currentController.port = port; + currentController.connectClient(); + } + + /** + * Adds a callback for a particular topic. + * + * @param identifier Topic header for group. + * @param callback Callback called when message for topic is received. + */ + public addMessageCallback( + identifier: string, + callback: (topic: string, message: string) => void, + ) { + this.controller?.subscribe(`${identifier}/#`); + this.messageCallbacks.set(identifier, callback); + } +} diff --git a/src/bundles/communication/RpcController.ts b/src/bundles/communication/RpcController.ts new file mode 100644 index 000000000..f649c0868 --- /dev/null +++ b/src/bundles/communication/RpcController.ts @@ -0,0 +1,123 @@ +import { type MultiUserController } from './MultiUserController'; +import uniqid from 'uniqid'; + +type DeclaredFunction = { + name: string; + func: (...args: any[]) => any; +}; + +/** + * Controller for RPC communication between 2 devices. + * + * @param topicHeader Topic header for all RPC communication, must not include '/'. + * @param multiUser Instance of multi user controller. + * @param userId ID of the user, used for identifying caller/callee. + */ +export class RpcController { + private topicHeader: string; + private multiUser: MultiUserController; + private userId: string; + private functions = new Map(); + private pendingReturns = new Map void>(); + private returnTopic: string; + + constructor( + topicHeader: string, + multiUser: MultiUserController, + userId?: string, + ) { + this.topicHeader = topicHeader; + this.multiUser = multiUser; + this.userId = userId ?? uniqid(); + this.returnTopic = `${this.topicHeader}_return/${this.userId}`; + this.multiUser.addMessageCallback(this.returnTopic, (topic, message) => { + let messageJson = JSON.parse(message); + let callId = messageJson.callId; + let callback = this.pendingReturns.get(callId); + if (callback) { + this.pendingReturns.delete(callId); + callback(messageJson.result); + } + }); + } + + /** + * Sends return value back to caller. + * + * @param sender ID of caller. + * @param callId ID of function call. + * @param result Return value for function call. + */ + private returnResponse(sender: string, callId: string, result: any) { + let message = { + callId, + result, + }; + let topic = `${this.topicHeader}_return/${sender}`; + this.multiUser.controller?.publish(topic, JSON.stringify(message), false); + } + + /** + * Exposes a function to other callers. + * + * @param name Name for the function, cannot include '/'. + * @param func Function to run. + */ + public expose(name: string, func: (...args: any[]) => any) { + let item = { + name, + func, + }; + this.functions.set(name, item); + let functionTopic = `${this.topicHeader}/${this.userId}/${name}`; + this.multiUser.addMessageCallback(functionTopic, (topic, message) => { + let splitTopic = topic.split('/'); + if (splitTopic.length !== 3) { + return; + } + let parsedMessage = JSON.parse(message); + let callId = parsedMessage.callId; + let sender = parsedMessage.sender; + if (!callId || !sender) return; + let calledName = splitTopic[2]; + let calledFunc = this.functions.get(calledName); + if (!calledFunc) { + this.returnResponse(sender, callId, null); + return; + } + try { + let args = parsedMessage.args; + let result = calledFunc?.func(...args); + this.returnResponse(sender, callId, result); + } catch { + this.returnResponse(sender, callId, null); + } + }); + } + + /** + * Calls a function on another device. + * + * @param receiver ID of the callee. + * @param name Name of the function to call. + * @param args Argument values of the function. + * @param callback Callback for return value received. + */ + public callFunction( + receiver: string, + name: string, + args: any[], + callback: (args: any[]) => void, + ) { + let topic = `${this.topicHeader}/${receiver}/${name}`; + let callId = uniqid(); + this.pendingReturns.set(callId, callback); + let messageJson = { + sender: this.userId, + callId, + args, + }; + let messageString = JSON.stringify(messageJson); + this.multiUser.controller?.publish(topic, messageString, false); + } +} diff --git a/src/bundles/communication/__tests__/index.ts b/src/bundles/communication/__tests__/index.ts new file mode 100644 index 000000000..73c402bec --- /dev/null +++ b/src/bundles/communication/__tests__/index.ts @@ -0,0 +1,224 @@ +import { MultiUserController } from '../MultiUserController'; +import { GlobalStateController } from '../GlobalStateController'; + +let multiUser = new MultiUserController(); +multiUser.setupController('broker.hivemq.com', 8884); +let globalStateController = new GlobalStateController( + 'test', + multiUser, + (_) => {}, +); + +// Empty Root - Replace root. + +test('Empty Root Set Null', () => { + globalStateController.globalState = undefined; + globalStateController.parseGlobalStateMessage('', JSON.stringify(null)); + expect(JSON.stringify(globalStateController.globalState)).toBe( + JSON.stringify(null), + ); +}); + +test('Empty Root Set Object', () => { + globalStateController.globalState = undefined; + let object = { + a: 'b', + }; + globalStateController.parseGlobalStateMessage('', JSON.stringify(object)); + expect(JSON.stringify(globalStateController.globalState)).toBe( + JSON.stringify(object), + ); +}); + +// Non-Empty Root - Replace root. + +test('Non-Empty Root Set Empty', () => { + let object = { + a: 'b', + }; + globalStateController.globalState = object; + globalStateController.parseGlobalStateMessage('', JSON.stringify(undefined)); + expect(JSON.stringify(globalStateController.globalState)).toBe( + JSON.stringify(undefined), + ); +}); + +test('Non-Empty Root Set Null', () => { + let object = { + a: 'b', + }; + globalStateController.globalState = object; + globalStateController.parseGlobalStateMessage('', JSON.stringify(null)); + expect(JSON.stringify(globalStateController.globalState)).toBe( + JSON.stringify(null), + ); +}); + +test('Non-Empty Root Set Object', () => { + globalStateController.globalState = { + a: 'b', + }; + let object = { + c: 'd', + }; + globalStateController.parseGlobalStateMessage('', JSON.stringify(object)); + expect(JSON.stringify(globalStateController.globalState)).toBe( + JSON.stringify(object), + ); +}); + +// Branch Value - Replace value if non-empty, remove path if empty. + +test('Branch Value Set Empty', () => { + globalStateController.globalState = { + a: 'b', + c: 'd', + }; + globalStateController.parseGlobalStateMessage('a', JSON.stringify(undefined)); + expect(JSON.stringify(globalStateController.globalState)).toBe( + JSON.stringify({ c: 'd' }), + ); +}); + +test('Nested Branch Value Set Empty', () => { + globalStateController.globalState = { + a: { + b: 'c', + }, + }; + globalStateController.parseGlobalStateMessage( + 'a/b', + JSON.stringify(undefined), + ); + expect(JSON.stringify(globalStateController.globalState)).toBe( + JSON.stringify({ a: {} }), + ); +}); + +test('Branch Value Set Null', () => { + globalStateController.globalState = { + a: 'b', + c: 'd', + }; + globalStateController.parseGlobalStateMessage('a', JSON.stringify(null)); + expect(JSON.stringify(globalStateController.globalState)).toBe( + JSON.stringify({ a: null, c: 'd' }), + ); +}); + +test('Nested Branch Value Set Null', () => { + globalStateController.globalState = { + a: { + b: 'c', + }, + }; + globalStateController.parseGlobalStateMessage('a/b', JSON.stringify(null)); + expect(JSON.stringify(globalStateController.globalState)).toBe( + JSON.stringify({ a: { b: null } }), + ); +}); + +test('Branch Value Set Object', () => { + globalStateController.globalState = { + a: 'b', + c: 'd', + }; + let object = { + b: 'e', + }; + globalStateController.parseGlobalStateMessage('a', JSON.stringify(object)); + expect(JSON.stringify(globalStateController.globalState)).toBe( + JSON.stringify({ a: object, c: 'd' }), + ); +}); + +test('Nested Branch Value Set Object', () => { + globalStateController.globalState = { + a: { + b: 'c', + }, + }; + let object = { + c: 'd', + }; + globalStateController.parseGlobalStateMessage('a/b', JSON.stringify(object)); + expect(JSON.stringify(globalStateController.globalState)).toBe( + JSON.stringify({ a: { b: object } }), + ); +}); + +// Branch Object - Replace object if non-empty, remove path if empty. + +test('Branch Object Set Empty', () => { + globalStateController.globalState = { + a: { b: 'c' }, + d: 'e', + }; + globalStateController.parseGlobalStateMessage('a', JSON.stringify(undefined)); + expect(JSON.stringify(globalStateController.globalState)).toBe( + JSON.stringify({ d: 'e' }), + ); +}); + +test('Nested Branch Object Set Empty', () => { + globalStateController.globalState = { + a: { b: { c: 'd' }, e: 'f' }, + }; + globalStateController.parseGlobalStateMessage( + 'a/b', + JSON.stringify(undefined), + ); + expect(JSON.stringify(globalStateController.globalState)).toBe( + JSON.stringify({ a: { e: 'f' } }), + ); +}); + +test('Branch Object Set Null', () => { + globalStateController.globalState = { + a: { b: 'c' }, + d: 'e', + }; + globalStateController.parseGlobalStateMessage('a', JSON.stringify(null)); + expect(JSON.stringify(globalStateController.globalState)).toBe( + JSON.stringify({ a: null, d: 'e' }), + ); +}); + +test('Nested Branch Object Set Null', () => { + globalStateController.globalState = { + a: { b: { c: 'd' }, e: 'f' }, + }; + globalStateController.parseGlobalStateMessage('a/b', JSON.stringify(null)); + expect(JSON.stringify(globalStateController.globalState)).toBe( + JSON.stringify({ a: { b: null, e: 'f' } }), + ); +}); + +test('Branch Object Set Object', () => { + globalStateController.globalState = { + a: { b: 'c', d: 'e' }, + f: 'g', + }; + let object = { + d: 'f', + g: 'h', + }; + globalStateController.parseGlobalStateMessage('a', JSON.stringify(object)); + expect(JSON.stringify(globalStateController.globalState)).toBe( + JSON.stringify({ a: object, f: 'g' }), + ); +}); + +test('Nested Branch Object Set Null', () => { + globalStateController.globalState = { + a: { b: { c: 'd' }, e: 'f' }, + }; + let object = { + c: 'g', + h: 'i', + }; + globalStateController.parseGlobalStateMessage('a/b', JSON.stringify(object)); + expect(JSON.stringify(globalStateController.globalState)).toBe( + JSON.stringify({ a: { b: object, e: 'f' } }), + ); +}); diff --git a/src/bundles/communication/index.ts b/src/bundles/communication/index.ts new file mode 100644 index 000000000..65cdab6e4 --- /dev/null +++ b/src/bundles/communication/index.ts @@ -0,0 +1,28 @@ +/** + * Module for communication between multiple devices. + * + * Offers 2 modes: + * 1. RPC - Call functions on another device. + * 2. Global State - Maintain a global state on all devices. + * + * @module communication + * @author Chong Wen Hao + */ + +export { + STATE_CONNECTED, + STATE_DISCONNECTED, + STATE_OFFLINE, + STATE_RECONNECTED, +} from './MqttController'; + +export { + initGlobalState, + getGlobalState, + updateGlobalState, + initRpc, + expose, + callFunction, + keepRunning, + stopRunning, +} from './Communications'; diff --git a/yarn.lock b/yarn.lock index d704b9d64..d2a4b64dc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1862,7 +1862,7 @@ bit-twiddle@^1.0.2: resolved "https://registry.yarnpkg.com/bit-twiddle/-/bit-twiddle-1.0.2.tgz#0c6c1fabe2b23d17173d9a61b7b7093eb9e1769e" integrity sha512-B9UhK0DKFZhoTFcfvAzhqsjStvGJp9vYWf3+6SNTtdSQnvIgfkHbgHrg/e4+TH71N2GDu8tpmCVoyfrL1d7ntA== -bl@^4.0.3: +bl@^4.0.2, bl@^4.0.3: version "4.1.0" resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== @@ -2184,11 +2184,29 @@ commander@^9.4.0: resolved "https://registry.yarnpkg.com/commander/-/commander-9.5.0.tgz#bc08d1eb5cedf7ccb797a96199d41c7bc3e60d30" integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ== +commist@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/commist/-/commist-1.1.0.tgz#17811ec6978f6c15ee4de80c45c9beb77cee35d5" + integrity sha512-rraC8NXWOEjhADbZe9QBNzLAN5Q3fsTPQtBV+fEVj6xKIgDgNiEVE6ZNfHpZOqfQ21YUzfVNUXLOEZquYvQPPg== + dependencies: + leven "^2.1.0" + minimist "^1.1.0" + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== +concat-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-2.0.0.tgz#414cf5af790a48c60ab9be4527d56d5e41133cb1" + integrity sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.0.2" + typedarray "^0.0.6" + confusing-browser-globals@^1.0.10: version "1.0.11" resolved "https://registry.yarnpkg.com/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz#ae40e9b57cdd3915408a2805ebd3a5585608dc81" @@ -2308,7 +2326,7 @@ dayjs@^1.10.4: resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.7.tgz#4b296922642f70999544d1144a2c25730fce63e2" integrity sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ== -debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4: +debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -2491,6 +2509,16 @@ dtype@^2.0.0: resolved "https://registry.yarnpkg.com/dtype/-/dtype-2.0.0.tgz#cd052323ce061444ecd2e8f5748f69a29be28434" integrity sha512-s2YVcLKdFGS0hpFqJaTwscsyt0E8nNFdmo73Ocd81xNPj4URI4rj6D60A+vFMIw7BXWlb4yRkEwfBqcZzPGiZg== +duplexify@^4.1.1: + version "4.1.2" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-4.1.2.tgz#18b4f8d28289132fa0b9573c898d9f903f81c7b0" + integrity sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw== + dependencies: + end-of-stream "^1.4.1" + inherits "^2.0.3" + readable-stream "^3.1.1" + stream-shift "^1.0.0" + ecstatic@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/ecstatic/-/ecstatic-3.3.2.tgz#6d1dd49814d00594682c652adb66076a69d46c48" @@ -2966,6 +2994,11 @@ eventemitter3@^4.0.0, eventemitter3@^4.0.7: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== +events@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + execa@^4.0.3: version "4.1.0" resolved "https://registry.yarnpkg.com/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a" @@ -3017,6 +3050,11 @@ expect@^29.4.3: jest-message-util "^29.4.3" jest-util "^29.4.3" +exponential-backoff@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.1.tgz#64ac7526fe341ab18a39016cd22c787d01e00bf6" + integrity sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw== + fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -3290,17 +3328,17 @@ gl-wiretap@^0.6.2: resolved "https://registry.yarnpkg.com/gl-wiretap/-/gl-wiretap-0.6.2.tgz#e4aa19622831088fbaa7e5a18d01768f7a3fb07c" integrity sha512-fxy1XGiPkfzK+T3XKDbY7yaqMBmozCGvAFyTwaZA3imeZH83w7Hr3r3bYlMRWIyzMI/lDUvUMM/92LE2OwqFyQ== -gl@^5.0.3: - version "5.0.3" - resolved "https://registry.yarnpkg.com/gl/-/gl-5.0.3.tgz#a10f37c50e48954348cc3e790b83313049bdbe1c" - integrity sha512-toWmb3Rgli5Wl9ygjZeglFBVLDYMOomy+rXlVZVDCoIRV+6mQE5nY4NgQgokYIc5oQzc1pvWY9lQJ0hGn61ZUg== +gl@^5.0.3, gl@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/gl/-/gl-6.0.2.tgz#685579732a19075e3acf4684edb1270278e551c7" + integrity sha512-yBbfpChOtFvg5D+KtMaBFvj6yt3vUnheNAH+UrQH2TfDB8kr0tERdL0Tjhe0W7xJ6jR6ftQBluTZR9jXUnKe8g== dependencies: bindings "^1.5.0" bit-twiddle "^1.0.2" glsl-tokenizer "^2.1.5" - nan "^2.16.0" - node-abi "^3.22.0" - node-gyp "^9.0.0" + nan "^2.17.0" + node-abi "^3.26.0" + node-gyp "^9.2.0" prebuild-install "^7.1.1" glob-parent@^5.1.2, glob-parent@~5.1.2: @@ -3317,7 +3355,7 @@ glob-parent@^6.0.2: dependencies: is-glob "^4.0.3" -glob@^7.1.3, glob@^7.1.4, glob@^7.2.0: +glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.2.0: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -3490,6 +3528,14 @@ header-case@^2.0.4: capital-case "^1.0.4" tslib "^2.0.3" +help-me@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/help-me/-/help-me-3.0.0.tgz#9803c81b5f346ad2bce2c6a0ba01b82257d319e8" + integrity sha512-hx73jClhyk910sidBB7ERlnhMlFsJJIBqSVMFDwPN8o2v9nmp5KgLq1Xz1Bf1fCMMZ6mPrX159iG0VLy/fPMtQ== + dependencies: + glob "^7.1.6" + readable-stream "^3.6.0" + html-encoding-sniffer@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz#2cb1a8cf0db52414776e5b2a7a04d5dd98158de9" @@ -4354,7 +4400,7 @@ jest@^29.4.1: import-local "^3.0.2" jest-cli "^29.4.3" -js-sdsl@^4.1.4: +js-sdsl@4.3.0, js-sdsl@^4.1.4: version "4.3.0" resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.3.0.tgz#aeefe32a451f7af88425b11fdb5f58c90ae1d711" integrity sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ== @@ -4509,6 +4555,11 @@ language-tags@=1.0.5: dependencies: language-subtag-registry "~0.3.2" +leven@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/leven/-/leven-2.1.0.tgz#c2e7a9f772094dee9d34202ae8acce4687875580" + integrity sha512-nvVPLpIHUxCUoRLrFqTgSxXJ614d8AgQoWl7zPe/2VadE8+1dpU3LBhowRuBAcuwruWtOdD8oYC9jDNJjXDPyA== + leven@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" @@ -4812,6 +4863,38 @@ mkdirp@^1.0.3, mkdirp@^1.0.4: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== +mqtt-packet@^6.8.0: + version "6.10.0" + resolved "https://registry.yarnpkg.com/mqtt-packet/-/mqtt-packet-6.10.0.tgz#c8b507832c4152e3e511c0efa104ae4a64cd418f" + integrity sha512-ja8+mFKIHdB1Tpl6vac+sktqy3gA8t9Mduom1BA75cI+R9AHnZOiaBQwpGiWnaVJLDGRdNhQmFaAqd7tkKSMGA== + dependencies: + bl "^4.0.2" + debug "^4.1.1" + process-nextick-args "^2.0.1" + +mqtt@^4.3.7: + version "4.3.8" + resolved "https://registry.yarnpkg.com/mqtt/-/mqtt-4.3.8.tgz#b8cc9a6eb5e4e0cb6eea699f24cd70dd7b228f1d" + integrity sha512-2xT75uYa0kiPEF/PE0VPdavmEkoBzMT/UL9moid0rAvlCtV48qBwxD62m7Ld/4j8tSkIO1E/iqRl/S72SEOhOw== + dependencies: + commist "^1.0.0" + concat-stream "^2.0.0" + debug "^4.1.1" + duplexify "^4.1.1" + help-me "^3.0.0" + inherits "^2.0.3" + lru-cache "^6.0.0" + minimist "^1.2.5" + mqtt-packet "^6.8.0" + number-allocator "^1.0.9" + pump "^3.0.0" + readable-stream "^3.6.0" + reinterval "^1.1.0" + rfdc "^1.3.0" + split2 "^3.1.0" + ws "^7.5.5" + xtend "^4.0.2" + ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" @@ -4822,10 +4905,10 @@ ms@^2.0.0, ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== -nan@^2.16.0: - version "2.17.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb" - integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ== +nan@^2.17.0: + version "2.18.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.18.0.tgz#26a6faae7ffbeb293a39660e88a76b82e30b7554" + integrity sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w== nanoid@^3.3.6: version "3.3.6" @@ -4860,7 +4943,14 @@ no-case@^3.0.4: lower-case "^2.0.2" tslib "^2.0.3" -node-abi@^3.22.0, node-abi@^3.3.0: +node-abi@^3.26.0: + version "3.56.0" + resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.56.0.tgz#ca807d5ff735ac6bbbd684ae3ff2debc1c2a40a7" + integrity sha512-fZjdhDOeRcaS+rcpve7XuwHBmktS1nS1gzgghwKUQQ8nTy2FdSDr6ZT8k6YhvlJeHmmQMYiT/IH9hfco5zeW2Q== + dependencies: + semver "^7.3.5" + +node-abi@^3.3.0: version "3.33.0" resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.33.0.tgz#8b23a0cec84e1c5f5411836de6a9b84bccf26e7f" integrity sha512-7GGVawqyHF4pfd0YFybhv/eM9JwTtPqx0mAanQ146O3FlSh3pA24zf9IRQTOsfTSqXTNzPSP5iagAJ94jjuVog== @@ -4872,12 +4962,13 @@ node-getopt@^0.3.2: resolved "https://registry.yarnpkg.com/node-getopt/-/node-getopt-0.3.2.tgz#57507cd22f6f69650aa99252304a842f1224e44c" integrity sha512-yqkmYrMbK1wPrfz7mgeYvA4tBperLg9FQ4S3Sau3nSAkpOA0x0zC8nQ1siBwozy1f4SE8vq2n1WKv99r+PCa1Q== -node-gyp@^9.0.0: - version "9.3.1" - resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-9.3.1.tgz#1e19f5f290afcc9c46973d68700cbd21a96192e4" - integrity sha512-4Q16ZCqq3g8awk6UplT7AuxQ35XN4R/yf/+wSAwcBUAjg7l58RTactWaP8fIDTi0FzI7YcVLujwExakZlfWkXg== +node-gyp@^9.2.0: + version "9.4.1" + resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-9.4.1.tgz#8a1023e0d6766ecb52764cc3a734b36ff275e185" + integrity sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ== dependencies: env-paths "^2.2.0" + exponential-backoff "^3.1.1" glob "^7.1.4" graceful-fs "^4.2.6" make-fetch-happen "^10.0.3" @@ -4937,6 +5028,14 @@ npmlog@^6.0.0: gauge "^4.0.3" set-blocking "^2.0.0" +number-allocator@^1.0.9: + version "1.0.14" + resolved "https://registry.yarnpkg.com/number-allocator/-/number-allocator-1.0.14.tgz#1f2e32855498a7740dcc8c78bed54592d930ee4d" + integrity sha512-OrL44UTVAvkKdOdRQZIJpLkAdjXGTRda052sN4sO77bKEzYYqWKMBjQvrJFzqygI99gL6Z4u2xctPW1tB8ErvA== + dependencies: + debug "^4.3.1" + js-sdsl "4.3.0" + nwsapi@^2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.2.tgz#e5418863e7905df67d51ec95938d67bf801f0bb0" @@ -5066,6 +5165,11 @@ os-tmpdir@~1.0.2: resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g== +os@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/os/-/os-0.1.2.tgz#f29a50c62908516ba42652de42f7038600cadbc2" + integrity sha512-ZoXJkvAnljwvc56MbvhtKVWmSkzV712k42Is2mA0+0KTSRakq5XXuXpjZjgAt9ctzl51ojhQWakQQpmOvXWfjQ== + p-limit@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" @@ -5326,6 +5430,11 @@ pretty-format@^29.4.3: ansi-styles "^5.0.0" react-is "^18.0.0" +process-nextick-args@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + process@^0.11.1: version "0.11.10" resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" @@ -5374,11 +5483,23 @@ pump@^3.0.0: end-of-stream "^1.1.0" once "^1.3.1" +punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + integrity sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ== + punycode@^2.1.0, punycode@^2.1.1: version "2.3.0" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== +qs@^6.11.2: + version "6.11.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9" + integrity sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA== + dependencies: + side-channel "^1.0.4" + qs@^6.4.0: version "6.11.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" @@ -5520,6 +5641,15 @@ react@^18.2.0: isarray "0.0.1" string_decoder "~0.10.x" +readable-stream@^3.0.0, readable-stream@^3.0.2: + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" @@ -5565,6 +5695,11 @@ regl@2.1.0, regl@^2.1.0: resolved "https://registry.yarnpkg.com/regl/-/regl-2.1.0.tgz#7dae71e9ff20f29c4f42f510c70cd92ebb6b657c" integrity sha512-oWUce/aVoEvW5l2V0LK7O5KJMzUSKeiOwFuJehzpSFd43dO5spP9r+sSUfhKtsky4u6MCqWJaRL+abzExynfTg== +reinterval@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/reinterval/-/reinterval-1.1.0.tgz#3361ecfa3ca6c18283380dd0bb9546f390f5ece7" + integrity sha512-QIRet3SYrGp0HUHO88jVskiG6seqUGC5iAG7AwI/BV4ypGcuqk9Du6YQBUOUqm9c8pw1eyLoIaONifRua1lsEQ== + require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" @@ -5625,6 +5760,11 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== +rfdc@^1.3.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.1.tgz#2b6d4df52dffe8bb346992a10ea9451f24373a8f" + integrity sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg== + rimraf@^2.6.3: version "2.7.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" @@ -5910,6 +6050,13 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== +split2@^3.1.0: + version "3.2.2" + resolved "https://registry.yarnpkg.com/split2/-/split2-3.2.2.tgz#bf2cf2a37d838312c249c89206fd7a17dd12365f" + integrity sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg== + dependencies: + readable-stream "^3.0.0" + sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" @@ -5936,6 +6083,11 @@ stop-iteration-iterator@^1.0.0: dependencies: internal-slot "^1.0.4" +stream-shift@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.3.tgz#85b8fab4d71010fc3ba8772e8046cc49b8a3864b" + integrity sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ== + string-length@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" @@ -6279,6 +6431,11 @@ typed-styles@^0.0.7: resolved "https://registry.yarnpkg.com/typed-styles/-/typed-styles-0.0.7.tgz#93392a008794c4595119ff62dde6809dbc40a3d9" integrity sha512-pzP0PWoZUhsECYjABgCGQlRGL1n7tOHsgwYv3oIiEpJwGhFTuty/YNeduxQYzXXa3Ge5BdT6sHYIQYpl4uJ+5Q== +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== + typedoc@^0.25.1: version "0.25.1" resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.25.1.tgz#50de2d8fb93623fbfb59e2fa6407ff40e3d3f438" @@ -6321,6 +6478,11 @@ union@~0.5.0: dependencies: qs "^6.4.0" +uniqid@^5.4.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/uniqid/-/uniqid-5.4.0.tgz#4e17bfcab66dfe33563411ae0c801f46ef964e66" + integrity sha512-38JRbJ4Fj94VmnC7G/J/5n5SC7Ab46OM5iNtSstB/ko3l1b5g7ALt4qzHFgGciFkyiRNtDXtLNb+VsxtMSE77A== + unique-filename@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-2.0.1.tgz#e785f8675a9a7589e0ac77e0b5c34d2eaeac6da2" @@ -6395,6 +6557,14 @@ url-parse@^1.5.3: querystringify "^2.1.1" requires-port "^1.0.0" +url@^0.11.3: + version "0.11.3" + resolved "https://registry.yarnpkg.com/url/-/url-0.11.3.tgz#6f495f4b935de40ce4a0a52faee8954244f3d3ad" + integrity sha512-6hxOLGfZASQK/cijlZnZJTq8OXAkt/3YGfQX45vvMYXpZoo8NdWZcY73K108Jf759lS1Bv/8wXnHDTSz17dSRw== + dependencies: + punycode "^1.4.1" + qs "^6.11.2" + util-deprecate@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" @@ -6576,6 +6746,11 @@ write@^1.0.0: dependencies: mkdirp "^0.5.1" +ws@^7.5.5: + version "7.5.9" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" + integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== + ws@^8.11.0: version "8.12.1" resolved "https://registry.yarnpkg.com/ws/-/ws-8.12.1.tgz#c51e583d79140b5e42e39be48c934131942d4a8f" @@ -6598,7 +6773,7 @@ xmlhttprequest-ts@^1.0.1: dependencies: tslib "^1.9.2" -"xtend@>=4.0.0 <4.1.0-0": +"xtend@>=4.0.0 <4.1.0-0", xtend@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== From 3d2b143be97d4095eb25787f8cb54870e3e618e3 Mon Sep 17 00:00:00 2001 From: Chong Wen Hao <58220142+8kdesign@users.noreply.github.com> Date: Mon, 4 Mar 2024 10:49:04 +0800 Subject: [PATCH 02/14] Clean code and added init comms --- src/bundles/communication/Communications.ts | 82 +++++++++++++++---- .../communication/GlobalStateController.ts | 8 +- src/bundles/communication/MqttController.ts | 4 +- .../communication/MultiUserController.ts | 6 +- src/bundles/communication/RpcController.ts | 37 ++++----- src/bundles/communication/index.ts | 1 + 6 files changed, 93 insertions(+), 45 deletions(-) diff --git a/src/bundles/communication/Communications.ts b/src/bundles/communication/Communications.ts index 8b1781fb3..bca01a76d 100644 --- a/src/bundles/communication/Communications.ts +++ b/src/bundles/communication/Communications.ts @@ -9,14 +9,30 @@ class CommunicationModuleState { rpc: RpcController | null = null; constructor(address: string, port: number) { - let multiUser = new MultiUserController(); + const multiUser = new MultiUserController(); multiUser.setupController(address, port); this.multiUser = multiUser; } } -let moduleState = new CommunicationModuleState('broker.hivemq.com', 8884); -context.moduleContexts.communication.state = moduleState; +/** + * Initializes connection with MQTT broker. + * Currently only supports WebSocket. + * + * @param address Address of broker. + * @param port WebSocket port number for broker. + */ +export function initCommunications(address: string, port: number) { + if (getGlobalState() instanceof CommunicationModuleState) { + return; + } + const newModuleState = new CommunicationModuleState(address, port); + context.moduleContexts.communication.state = newModuleState; +} + +function getModuleState() { + return context.moduleContexts.communication.state; +} // Loop @@ -51,11 +67,19 @@ export function initGlobalState( topicHeader: string, callback: (state: any) => void, ) { - moduleState.globalState = new GlobalStateController( - topicHeader, - moduleState.multiUser, - callback, - ); + const moduleState = getModuleState(); + if (moduleState instanceof CommunicationModuleState) { + if (moduleState.globalState instanceof GlobalStateController) { + return; + } + moduleState.globalState = new GlobalStateController( + topicHeader, + moduleState.multiUser, + callback, + ); + return; + } + throw new Error('Error: Communication module not initialized.'); } /** @@ -64,7 +88,11 @@ export function initGlobalState( * @returns Current global state. */ export function getGlobalState() { - return moduleState.globalState?.globalState; + const moduleState = getModuleState(); + if (moduleState instanceof CommunicationModuleState) { + return moduleState.globalState?.globalState; + } + throw new Error('Error: Communication module not initialized.'); } /** @@ -75,7 +103,12 @@ export function getGlobalState() { * @param updatedState Replacement value at specified path. */ export function updateGlobalState(path: string, updatedState: any) { - moduleState.globalState?.updateGlobalState(path, updatedState); + const moduleState = getModuleState(); + if (moduleState instanceof CommunicationModuleState) { + moduleState.globalState?.updateGlobalState(path, updatedState); + return; + } + throw new Error('Error: Communication module not initialized.'); } // Rpc @@ -87,11 +120,16 @@ export function updateGlobalState(path: string, updatedState: any) { * @param userId Identifier for this user. */ export function initRpc(topicHeader: string, userId?: string) { - moduleState.rpc = new RpcController( - topicHeader, - moduleState.multiUser, - userId, - ); + const moduleState = getModuleState(); + if (moduleState instanceof CommunicationModuleState) { + moduleState.rpc = new RpcController( + topicHeader, + moduleState.multiUser, + userId, + ); + return; + } + throw new Error('Error: Communication module not initialized.'); } /** @@ -102,7 +140,12 @@ export function initRpc(topicHeader: string, userId?: string) { * @param func Function to call when request received. */ export function expose(name: string, func: (...args: any[]) => any) { - moduleState.rpc?.expose(name, func); + const moduleState = getModuleState(); + if (moduleState instanceof CommunicationModuleState) { + moduleState.rpc?.expose(name, func); + return; + } + throw new Error('Error: Communication module not initialized.'); } /** @@ -119,5 +162,10 @@ export function callFunction( args: any[], callback: (args: any[]) => void, ) { - moduleState.rpc?.callFunction(receiver, name, args, callback); + const moduleState = getModuleState(); + if (moduleState instanceof CommunicationModuleState) { + moduleState.rpc?.callFunction(receiver, name, args, callback); + return; + } + throw new Error('Error: Communication module not initialized.'); } diff --git a/src/bundles/communication/GlobalStateController.ts b/src/bundles/communication/GlobalStateController.ts index 98df10cb6..8eebf20b0 100644 --- a/src/bundles/communication/GlobalStateController.ts +++ b/src/bundles/communication/GlobalStateController.ts @@ -32,7 +32,7 @@ export class GlobalStateController { private setupGlobalState() { if (this.topicHeader.length <= 0) return; this.multiUser.addMessageCallback(this.topicHeader, (topic, message) => { - let shortenedTopic = topic.substring( + const shortenedTopic = topic.substring( this.topicHeader.length, topic.length, ); @@ -53,7 +53,7 @@ export class GlobalStateController { if (!preSplitTopic.startsWith('/')) { preSplitTopic = `/${preSplitTopic}`; } - let splitTopic = preSplitTopic.split('/'); + const splitTopic = preSplitTopic.split('/'); try { let newGlobalState = { ...this.globalState }; if ( @@ -64,7 +64,7 @@ export class GlobalStateController { } let currentJson = newGlobalState; for (let i = 1; i < splitTopic.length - 1; i++) { - let subTopic = splitTopic[i]; + const subTopic = splitTopic[i]; if ( !(currentJson[subTopic] instanceof Object) || currentJson[subTopic] instanceof Array || @@ -77,7 +77,7 @@ export class GlobalStateController { if (message === undefined || message.length === 0) { delete currentJson[splitTopic[splitTopic.length - 1]]; } else { - let jsonMessage = JSON.parse(message); + const jsonMessage = JSON.parse(message); currentJson[splitTopic[splitTopic.length - 1]] = jsonMessage; } this.setGlobalState(newGlobalState); diff --git a/src/bundles/communication/MqttController.ts b/src/bundles/communication/MqttController.ts index db5b7a5d0..d300adb36 100644 --- a/src/bundles/communication/MqttController.ts +++ b/src/bundles/communication/MqttController.ts @@ -34,7 +34,7 @@ export class MqttController { */ public connectClient() { if (this.connected || this.address.length === 0) return; - let link = `wss://${this.address}:${this.port}/mqtt`; + const link = `wss://${this.address}:${this.port}/mqtt`; this.client = connect(link); this.connected = true; this.client.on('connect', () => { @@ -50,7 +50,7 @@ export class MqttController { this.connectionCallback(STATE_OFFLINE); }); this.client.on('message', (topic, message) => { - let decoder = new TextDecoder('utf-8'); + const decoder = new TextDecoder('utf-8'); this.messageCallback(topic, decoder.decode(message)); }); } diff --git a/src/bundles/communication/MultiUserController.ts b/src/bundles/communication/MultiUserController.ts index 6d1a13b2b..f1548dfa0 100644 --- a/src/bundles/communication/MultiUserController.ts +++ b/src/bundles/communication/MultiUserController.ts @@ -19,7 +19,7 @@ export class MultiUserController { */ public setupController(address: string, port: number) { let currentController = this.controller; - if (currentController !== null) { + if (currentController) { currentController.disconnect(); this.connectionState = STATE_DISCONNECTED; } else { @@ -28,9 +28,9 @@ export class MultiUserController { this.connectionState = status; }, (topic: string, message: string) => { - let splitTopic = topic.split('/'); + const splitTopic = topic.split('/'); this.messageCallbacks.forEach((callback, identifier) => { - let splitIdentifier = identifier.split('/'); + const splitIdentifier = identifier.split('/'); if (splitTopic.length < splitIdentifier.length) return; for (let i = 0; i < splitIdentifier.length; i++) { if (splitIdentifier[i] !== splitTopic[i]) return; diff --git a/src/bundles/communication/RpcController.ts b/src/bundles/communication/RpcController.ts index f649c0868..36c2243d8 100644 --- a/src/bundles/communication/RpcController.ts +++ b/src/bundles/communication/RpcController.ts @@ -31,9 +31,9 @@ export class RpcController { this.userId = userId ?? uniqid(); this.returnTopic = `${this.topicHeader}_return/${this.userId}`; this.multiUser.addMessageCallback(this.returnTopic, (topic, message) => { - let messageJson = JSON.parse(message); - let callId = messageJson.callId; - let callback = this.pendingReturns.get(callId); + const messageJson = JSON.parse(message); + const callId = messageJson.callId; + const callback = this.pendingReturns.get(callId); if (callback) { this.pendingReturns.delete(callId); callback(messageJson.result); @@ -49,11 +49,11 @@ export class RpcController { * @param result Return value for function call. */ private returnResponse(sender: string, callId: string, result: any) { - let message = { + const message = { callId, result, }; - let topic = `${this.topicHeader}_return/${sender}`; + const topic = `${this.topicHeader}_return/${sender}`; this.multiUser.controller?.publish(topic, JSON.stringify(message), false); } @@ -64,30 +64,29 @@ export class RpcController { * @param func Function to run. */ public expose(name: string, func: (...args: any[]) => any) { - let item = { + const item = { name, func, }; this.functions.set(name, item); - let functionTopic = `${this.topicHeader}/${this.userId}/${name}`; + const functionTopic = `${this.topicHeader}/${this.userId}/${name}`; this.multiUser.addMessageCallback(functionTopic, (topic, message) => { - let splitTopic = topic.split('/'); + const splitTopic = topic.split('/'); if (splitTopic.length !== 3) { return; } - let parsedMessage = JSON.parse(message); - let callId = parsedMessage.callId; - let sender = parsedMessage.sender; + const parsedMessage = JSON.parse(message); + const callId = parsedMessage.callId; + const sender = parsedMessage.sender; if (!callId || !sender) return; - let calledName = splitTopic[2]; - let calledFunc = this.functions.get(calledName); + const calledName = splitTopic[2]; + const calledFunc = this.functions.get(calledName); if (!calledFunc) { this.returnResponse(sender, callId, null); return; } try { - let args = parsedMessage.args; - let result = calledFunc?.func(...args); + const result = calledFunc?.func(...parsedMessage.args); this.returnResponse(sender, callId, result); } catch { this.returnResponse(sender, callId, null); @@ -109,15 +108,15 @@ export class RpcController { args: any[], callback: (args: any[]) => void, ) { - let topic = `${this.topicHeader}/${receiver}/${name}`; - let callId = uniqid(); + const topic = `${this.topicHeader}/${receiver}/${name}`; + const callId = uniqid(); this.pendingReturns.set(callId, callback); - let messageJson = { + const messageJson = { sender: this.userId, callId, args, }; - let messageString = JSON.stringify(messageJson); + const messageString = JSON.stringify(messageJson); this.multiUser.controller?.publish(topic, messageString, false); } } diff --git a/src/bundles/communication/index.ts b/src/bundles/communication/index.ts index 65cdab6e4..e1de7824b 100644 --- a/src/bundles/communication/index.ts +++ b/src/bundles/communication/index.ts @@ -17,6 +17,7 @@ export { } from './MqttController'; export { + initCommunications, initGlobalState, getGlobalState, updateGlobalState, From 78e874a3188aad0ffe4c68a5ecc7864ce3ed0fd2 Mon Sep 17 00:00:00 2001 From: Chong Wen Hao <58220142+8kdesign@users.noreply.github.com> Date: Mon, 4 Mar 2024 11:01:23 +0800 Subject: [PATCH 03/14] Additional cleanup --- package.json | 3 +-- src/bundles/communication/MqttController.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index e4e9dbc1f..0160ed6be 100644 --- a/package.json +++ b/package.json @@ -136,7 +136,6 @@ }, "resolutions": { "@types/react": "^18.2.0", - "esbuild": "^0.18.20", - "**/gl": "^6.0.2" + "esbuild": "^0.18.20" } } diff --git a/src/bundles/communication/MqttController.ts b/src/bundles/communication/MqttController.ts index d300adb36..2ca3cfe05 100644 --- a/src/bundles/communication/MqttController.ts +++ b/src/bundles/communication/MqttController.ts @@ -18,7 +18,7 @@ export class MqttController { private messageCallback: (topic: string, message: string) => void; address: string = ''; - port: number = 8080; + port: number = 9001; constructor( connectionCallback: (status: string) => void, From 990969340101ee5a6323f014dbbc1ca52a92ac01 Mon Sep 17 00:00:00 2001 From: Chong Wen Hao <58220142+8kdesign@users.noreply.github.com> Date: Mon, 4 Mar 2024 11:13:20 +0800 Subject: [PATCH 04/14] Fix wrong state checked --- src/bundles/communication/Communications.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bundles/communication/Communications.ts b/src/bundles/communication/Communications.ts index bca01a76d..2ace3d861 100644 --- a/src/bundles/communication/Communications.ts +++ b/src/bundles/communication/Communications.ts @@ -23,7 +23,7 @@ class CommunicationModuleState { * @param port WebSocket port number for broker. */ export function initCommunications(address: string, port: number) { - if (getGlobalState() instanceof CommunicationModuleState) { + if (getModuleState() instanceof CommunicationModuleState) { return; } const newModuleState = new CommunicationModuleState(address, port); From d883ee9bbc3de51227fc3e7480d55dc3ee75b75f Mon Sep 17 00:00:00 2001 From: Chong Wen Hao <58220142+8kdesign@users.noreply.github.com> Date: Mon, 4 Mar 2024 14:31:23 +0800 Subject: [PATCH 05/14] Added QoS for subscription --- src/bundles/communication/MqttController.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/bundles/communication/MqttController.ts b/src/bundles/communication/MqttController.ts index 2ca3cfe05..f4f6792b6 100644 --- a/src/bundles/communication/MqttController.ts +++ b/src/bundles/communication/MqttController.ts @@ -83,12 +83,13 @@ export class MqttController { /** * Subscribes to a topic. + * Qos of 1 to prevent downgrading. * * @param topic Identifier for group of devices receiving the broadcast. */ public subscribe(topic: string) { if (this.client) { - this.client.subscribe(topic); + this.client.subscribe(topic, { qos: 1 }); } } From 93ac2d84cdb9ad31fde627051262cdf691b8932d Mon Sep 17 00:00:00 2001 From: Chong Wen Hao <58220142+8kdesign@users.noreply.github.com> Date: Thu, 7 Mar 2024 11:43:27 +0800 Subject: [PATCH 06/14] Added authentication --- src/bundles/communication/Communications.ts | 34 +++++++++++++++++-- src/bundles/communication/MqttController.ts | 4 ++- .../communication/MultiUserController.ts | 9 ++++- src/bundles/communication/index.ts | 1 + 4 files changed, 44 insertions(+), 4 deletions(-) diff --git a/src/bundles/communication/Communications.ts b/src/bundles/communication/Communications.ts index 2ace3d861..d26b06816 100644 --- a/src/bundles/communication/Communications.ts +++ b/src/bundles/communication/Communications.ts @@ -8,9 +8,14 @@ class CommunicationModuleState { globalState: GlobalStateController | null = null; rpc: RpcController | null = null; - constructor(address: string, port: number) { + constructor( + address: string, + port: number, + user: string = '', + password: string = '', + ) { const multiUser = new MultiUserController(); - multiUser.setupController(address, port); + multiUser.setupController(address, port, user, password); this.multiUser = multiUser; } } @@ -30,6 +35,31 @@ export function initCommunications(address: string, port: number) { context.moduleContexts.communication.state = newModuleState; } +/** + * Initializes connection with MQTT broker. + * Currently only supports WebSocket. + * + * @param address Address of broker. + * @param port WebSocket port number for broker. + */ +export function initCommunicationsSecure( + address: string, + port: number, + user: string, + password: string, +) { + if (getModuleState() instanceof CommunicationModuleState) { + return; + } + const newModuleState = new CommunicationModuleState( + address, + port, + user, + password, + ); + context.moduleContexts.communication.state = newModuleState; +} + function getModuleState() { return context.moduleContexts.communication.state; } diff --git a/src/bundles/communication/MqttController.ts b/src/bundles/communication/MqttController.ts index f4f6792b6..a558c8f9a 100644 --- a/src/bundles/communication/MqttController.ts +++ b/src/bundles/communication/MqttController.ts @@ -19,6 +19,8 @@ export class MqttController { address: string = ''; port: number = 9001; + user: string = ''; + password: string = ''; constructor( connectionCallback: (status: string) => void, @@ -34,7 +36,7 @@ export class MqttController { */ public connectClient() { if (this.connected || this.address.length === 0) return; - const link = `wss://${this.address}:${this.port}/mqtt`; + const link = `wss://${this.user}:${this.password}@${this.address}:${this.port}/mqtt`; this.client = connect(link); this.connected = true; this.client.on('connect', () => { diff --git a/src/bundles/communication/MultiUserController.ts b/src/bundles/communication/MultiUserController.ts index f1548dfa0..8d6ce4a32 100644 --- a/src/bundles/communication/MultiUserController.ts +++ b/src/bundles/communication/MultiUserController.ts @@ -17,7 +17,12 @@ export class MultiUserController { * @param address Address to connect to. * @param port MQTT port number. */ - public setupController(address: string, port: number) { + public setupController( + address: string, + port: number, + user: string, + password: string, + ) { let currentController = this.controller; if (currentController) { currentController.disconnect(); @@ -43,6 +48,8 @@ export class MultiUserController { } currentController.address = address; currentController.port = port; + currentController.user = user; + currentController.password = password; currentController.connectClient(); } diff --git a/src/bundles/communication/index.ts b/src/bundles/communication/index.ts index e1de7824b..fffcc13f5 100644 --- a/src/bundles/communication/index.ts +++ b/src/bundles/communication/index.ts @@ -18,6 +18,7 @@ export { export { initCommunications, + initCommunicationsSecure, initGlobalState, getGlobalState, updateGlobalState, From 035d3155a8fe1e291105943285e00df3f3e8e309 Mon Sep 17 00:00:00 2001 From: Chong Wen Hao <58220142+8kdesign@users.noreply.github.com> Date: Mon, 11 Mar 2024 14:32:43 +0800 Subject: [PATCH 07/14] Prepare for connection with private broker --- src/bundles/communication/Communications.ts | 21 +++++++-- src/bundles/communication/MqttController.ts | 26 ++++++++--- .../communication/MultiUserController.ts | 45 +++++++++++++------ src/bundles/communication/index.ts | 3 +- 4 files changed, 70 insertions(+), 25 deletions(-) diff --git a/src/bundles/communication/Communications.ts b/src/bundles/communication/Communications.ts index d26b06816..ec46b44de 100644 --- a/src/bundles/communication/Communications.ts +++ b/src/bundles/communication/Communications.ts @@ -9,13 +9,14 @@ class CommunicationModuleState { rpc: RpcController | null = null; constructor( - address: string, - port: number, + isPrivate: boolean, + address: string = '', + port: number = 443, user: string = '', password: string = '', ) { const multiUser = new MultiUserController(); - multiUser.setupController(address, port, user, password); + multiUser.setupController(isPrivate, address, port, user, password); this.multiUser = multiUser; } } @@ -31,7 +32,7 @@ export function initCommunications(address: string, port: number) { if (getModuleState() instanceof CommunicationModuleState) { return; } - const newModuleState = new CommunicationModuleState(address, port); + const newModuleState = new CommunicationModuleState(false, address, port); context.moduleContexts.communication.state = newModuleState; } @@ -52,6 +53,7 @@ export function initCommunicationsSecure( return; } const newModuleState = new CommunicationModuleState( + false, address, port, user, @@ -60,6 +62,17 @@ export function initCommunicationsSecure( context.moduleContexts.communication.state = newModuleState; } +/** + * Connects to NUS's private broker. + */ +export function initCommunicationsPrivate() { + if (getModuleState() instanceof CommunicationModuleState) { + return; + } + const newModuleState = new CommunicationModuleState(true); + context.moduleContexts.communication.state = newModuleState; +} + function getModuleState() { return context.moduleContexts.communication.state; } diff --git a/src/bundles/communication/MqttController.ts b/src/bundles/communication/MqttController.ts index a558c8f9a..b33cf86b2 100644 --- a/src/bundles/communication/MqttController.ts +++ b/src/bundles/communication/MqttController.ts @@ -2,7 +2,7 @@ import { connect, type MqttClient } from 'mqtt/dist/mqtt'; export const STATE_CONNECTED = 'Connected'; export const STATE_DISCONNECTED = 'Disconnected'; -export const STATE_RECONNECTED = 'Reconnected'; +export const STATE_RECONNECTING = 'Reconnecting'; export const STATE_OFFLINE = 'Offline'; /** @@ -18,9 +18,10 @@ export class MqttController { private messageCallback: (topic: string, message: string) => void; address: string = ''; - port: number = 9001; + port: number = 443; user: string = ''; password: string = ''; + isPrivateBroker = false; constructor( connectionCallback: (status: string) => void, @@ -34,10 +35,21 @@ export class MqttController { * Sets up MQTT client link and connects to it. * Also handles connection status callbacks. */ - public connectClient() { - if (this.connected || this.address.length === 0) return; - const link = `wss://${this.user}:${this.password}@${this.address}:${this.port}/mqtt`; - this.client = connect(link); + public async connectClient() { + if (this.connected) return; + if (this.isPrivateBroker) { + const result = await fetch( + 'https://api.sourceacademy.nus.edu.sg/v2/devices/random/mqtt_endpoint', + ); + const host = await result.text(); + const link = `wss://${host}`; + this.client = connect(link); + console.log(`Connecting to ${link}`); + } else { + if (this.address.length === 0) return; + const link = `wss://${this.user}:${this.password}@${this.address}:${this.port}/mqtt`; + this.client = connect(link); + } this.connected = true; this.client.on('connect', () => { this.connectionCallback(STATE_CONNECTED); @@ -46,7 +58,7 @@ export class MqttController { this.connectionCallback(STATE_DISCONNECTED); }); this.client.on('reconnect', () => { - this.connectionCallback(STATE_RECONNECTED); + this.connectionCallback(STATE_RECONNECTING); }); this.client.on('offline', () => { this.connectionCallback(STATE_OFFLINE); diff --git a/src/bundles/communication/MultiUserController.ts b/src/bundles/communication/MultiUserController.ts index 8d6ce4a32..cb9366492 100644 --- a/src/bundles/communication/MultiUserController.ts +++ b/src/bundles/communication/MultiUserController.ts @@ -14,10 +14,14 @@ export class MultiUserController { * Sets up and connect to the MQTT link. * Uses websocket implementation. * + * @param isPrivate Whether to use NUS private broker. * @param address Address to connect to. * @param port MQTT port number. + * @param user Username of account, leave empty if not required. + * @param password Password of account, leave empty if not required. */ public setupController( + isPrivate: boolean, address: string, port: number, user: string, @@ -31,28 +35,43 @@ export class MultiUserController { currentController = new MqttController( (status: string) => { this.connectionState = status; + console.log(status); }, (topic: string, message: string) => { - const splitTopic = topic.split('/'); - this.messageCallbacks.forEach((callback, identifier) => { - const splitIdentifier = identifier.split('/'); - if (splitTopic.length < splitIdentifier.length) return; - for (let i = 0; i < splitIdentifier.length; i++) { - if (splitIdentifier[i] !== splitTopic[i]) return; - } - callback(topic, message); - }); + this.handleIncomingMessage(topic, message); }, ); this.controller = currentController; } - currentController.address = address; - currentController.port = port; - currentController.user = user; - currentController.password = password; + if (isPrivate) { + currentController.isPrivateBroker = true; + } else { + currentController.address = address; + currentController.port = port; + currentController.user = user; + currentController.password = password; + } currentController.connectClient(); } + /** + * Parses topic and calls relevant callbacks with message. + * + * @param topic Topic of incoming message. + * @param message Contents of incoming message. + */ + handleIncomingMessage(topic: string, message: string) { + const splitTopic = topic.split('/'); + this.messageCallbacks.forEach((callback, identifier) => { + const splitIdentifier = identifier.split('/'); + if (splitTopic.length < splitIdentifier.length) return; + for (let i = 0; i < splitIdentifier.length; i++) { + if (splitIdentifier[i] !== splitTopic[i]) return; + } + callback(topic, message); + }); + } + /** * Adds a callback for a particular topic. * diff --git a/src/bundles/communication/index.ts b/src/bundles/communication/index.ts index fffcc13f5..200b2475f 100644 --- a/src/bundles/communication/index.ts +++ b/src/bundles/communication/index.ts @@ -13,12 +13,13 @@ export { STATE_CONNECTED, STATE_DISCONNECTED, STATE_OFFLINE, - STATE_RECONNECTED, + STATE_RECONNECTING as STATE_RECONNECTED, } from './MqttController'; export { initCommunications, initCommunicationsSecure, + initCommunicationsPrivate, initGlobalState, getGlobalState, updateGlobalState, From e0a4cf42d5b0c71ec9e5a1cc3673dd33a3ffe7e7 Mon Sep 17 00:00:00 2001 From: Chong Wen Hao <58220142+8kdesign@users.noreply.github.com> Date: Mon, 11 Mar 2024 14:33:42 +0800 Subject: [PATCH 08/14] Update index.ts --- src/bundles/communication/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bundles/communication/index.ts b/src/bundles/communication/index.ts index 200b2475f..b25e565e1 100644 --- a/src/bundles/communication/index.ts +++ b/src/bundles/communication/index.ts @@ -13,7 +13,7 @@ export { STATE_CONNECTED, STATE_DISCONNECTED, STATE_OFFLINE, - STATE_RECONNECTING as STATE_RECONNECTED, + STATE_RECONNECTING, } from './MqttController'; export { From 3e02fc75bd8c3c3cec6cb6f77256e6a2a31ac6f1 Mon Sep 17 00:00:00 2001 From: Chong Wen Hao <58220142+8kdesign@users.noreply.github.com> Date: Tue, 12 Mar 2024 12:32:27 +0800 Subject: [PATCH 09/14] Remove private and secure init --- src/bundles/communication/Communications.ts | 46 +++++-------------- src/bundles/communication/MqttController.ts | 19 +++----- .../communication/MultiUserController.ts | 15 +++--- src/bundles/communication/index.ts | 2 - 4 files changed, 25 insertions(+), 57 deletions(-) diff --git a/src/bundles/communication/Communications.ts b/src/bundles/communication/Communications.ts index ec46b44de..68e71955d 100644 --- a/src/bundles/communication/Communications.ts +++ b/src/bundles/communication/Communications.ts @@ -9,14 +9,14 @@ class CommunicationModuleState { rpc: RpcController | null = null; constructor( - isPrivate: boolean, - address: string = '', - port: number = 443, - user: string = '', - password: string = '', + address: string, + port: number, + user: string, + password: string, + isSecure: boolean, ) { const multiUser = new MultiUserController(); - multiUser.setupController(isPrivate, address, port, user, password); + multiUser.setupController(address, port, user, password, isSecure); this.multiUser = multiUser; } } @@ -27,52 +27,30 @@ class CommunicationModuleState { * * @param address Address of broker. * @param port WebSocket port number for broker. + * @param user Username of account, use empty string if none. + * @param password Password of account, use empty string if none. + * @param isSecure Whether to use TLS. */ -export function initCommunications(address: string, port: number) { - if (getModuleState() instanceof CommunicationModuleState) { - return; - } - const newModuleState = new CommunicationModuleState(false, address, port); - context.moduleContexts.communication.state = newModuleState; -} - -/** - * Initializes connection with MQTT broker. - * Currently only supports WebSocket. - * - * @param address Address of broker. - * @param port WebSocket port number for broker. - */ -export function initCommunicationsSecure( +export function initCommunications( address: string, port: number, user: string, password: string, + isSecure: boolean, ) { if (getModuleState() instanceof CommunicationModuleState) { return; } const newModuleState = new CommunicationModuleState( - false, address, port, user, password, + isSecure, ); context.moduleContexts.communication.state = newModuleState; } -/** - * Connects to NUS's private broker. - */ -export function initCommunicationsPrivate() { - if (getModuleState() instanceof CommunicationModuleState) { - return; - } - const newModuleState = new CommunicationModuleState(true); - context.moduleContexts.communication.state = newModuleState; -} - function getModuleState() { return context.moduleContexts.communication.state; } diff --git a/src/bundles/communication/MqttController.ts b/src/bundles/communication/MqttController.ts index b33cf86b2..ef4552469 100644 --- a/src/bundles/communication/MqttController.ts +++ b/src/bundles/communication/MqttController.ts @@ -21,7 +21,7 @@ export class MqttController { port: number = 443; user: string = ''; password: string = ''; - isPrivateBroker = false; + isSecure = false; constructor( connectionCallback: (status: string) => void, @@ -37,19 +37,14 @@ export class MqttController { */ public async connectClient() { if (this.connected) return; - if (this.isPrivateBroker) { - const result = await fetch( - 'https://api.sourceacademy.nus.edu.sg/v2/devices/random/mqtt_endpoint', - ); - const host = await result.text(); - const link = `wss://${host}`; - this.client = connect(link); - console.log(`Connecting to ${link}`); + if (this.address.length === 0) return; + var link = ''; + if (this.isSecure) { + link = `wss://${this.user}:${this.password}@${this.address}:${this.port}/mqtt`; } else { - if (this.address.length === 0) return; - const link = `wss://${this.user}:${this.password}@${this.address}:${this.port}/mqtt`; - this.client = connect(link); + link = `ws://${this.user}:${this.password}@${this.address}:${this.port}/mqtt`; } + this.client = connect(link); this.connected = true; this.client.on('connect', () => { this.connectionCallback(STATE_CONNECTED); diff --git a/src/bundles/communication/MultiUserController.ts b/src/bundles/communication/MultiUserController.ts index cb9366492..66461019a 100644 --- a/src/bundles/communication/MultiUserController.ts +++ b/src/bundles/communication/MultiUserController.ts @@ -21,11 +21,11 @@ export class MultiUserController { * @param password Password of account, leave empty if not required. */ public setupController( - isPrivate: boolean, address: string, port: number, user: string, password: string, + isSecure: boolean, ) { let currentController = this.controller; if (currentController) { @@ -43,14 +43,11 @@ export class MultiUserController { ); this.controller = currentController; } - if (isPrivate) { - currentController.isPrivateBroker = true; - } else { - currentController.address = address; - currentController.port = port; - currentController.user = user; - currentController.password = password; - } + currentController.address = address; + currentController.port = port; + currentController.user = user; + currentController.password = password; + currentController.isSecure = isSecure; currentController.connectClient(); } diff --git a/src/bundles/communication/index.ts b/src/bundles/communication/index.ts index b25e565e1..7373296dc 100644 --- a/src/bundles/communication/index.ts +++ b/src/bundles/communication/index.ts @@ -18,8 +18,6 @@ export { export { initCommunications, - initCommunicationsSecure, - initCommunicationsPrivate, initGlobalState, getGlobalState, updateGlobalState, From 10f302f74dc2e367048b37eb597e9dab6c48e5c5 Mon Sep 17 00:00:00 2001 From: Chong Wen Hao <58220142+8kdesign@users.noreply.github.com> Date: Tue, 19 Mar 2024 13:47:48 +0800 Subject: [PATCH 10/14] Remove nonsecure mqtt and added get user id --- src/bundles/communication/Communications.ts | 25 +++++++++++-------- src/bundles/communication/MqttController.ts | 8 +----- .../communication/MultiUserController.ts | 2 -- src/bundles/communication/RpcController.ts | 9 +++++++ src/bundles/communication/index.ts | 1 + 5 files changed, 25 insertions(+), 20 deletions(-) diff --git a/src/bundles/communication/Communications.ts b/src/bundles/communication/Communications.ts index 68e71955d..45917d0fc 100644 --- a/src/bundles/communication/Communications.ts +++ b/src/bundles/communication/Communications.ts @@ -8,15 +8,9 @@ class CommunicationModuleState { globalState: GlobalStateController | null = null; rpc: RpcController | null = null; - constructor( - address: string, - port: number, - user: string, - password: string, - isSecure: boolean, - ) { + constructor(address: string, port: number, user: string, password: string) { const multiUser = new MultiUserController(); - multiUser.setupController(address, port, user, password, isSecure); + multiUser.setupController(address, port, user, password); this.multiUser = multiUser; } } @@ -29,14 +23,12 @@ class CommunicationModuleState { * @param port WebSocket port number for broker. * @param user Username of account, use empty string if none. * @param password Password of account, use empty string if none. - * @param isSecure Whether to use TLS. */ export function initCommunications( address: string, port: number, user: string, password: string, - isSecure: boolean, ) { if (getModuleState() instanceof CommunicationModuleState) { return; @@ -46,7 +38,6 @@ export function initCommunications( port, user, password, - isSecure, ); context.moduleContexts.communication.state = newModuleState; } @@ -153,6 +144,18 @@ export function initRpc(topicHeader: string, userId?: string) { throw new Error('Error: Communication module not initialized.'); } +export function getUserId(): string { + const moduleState = getModuleState(); + if (moduleState instanceof CommunicationModuleState) { + let userId = moduleState.rpc?.getUserId(); + if (userId) { + return userId; + } + throw new Error('Error: UserID not found.'); + } + throw new Error('Error: Communication module not initialized.'); +} + /** * Exposes the specified function to other users. * Other users can use "callFunction" to call this function. diff --git a/src/bundles/communication/MqttController.ts b/src/bundles/communication/MqttController.ts index ef4552469..ead416dcc 100644 --- a/src/bundles/communication/MqttController.ts +++ b/src/bundles/communication/MqttController.ts @@ -21,7 +21,6 @@ export class MqttController { port: number = 443; user: string = ''; password: string = ''; - isSecure = false; constructor( connectionCallback: (status: string) => void, @@ -38,12 +37,7 @@ export class MqttController { public async connectClient() { if (this.connected) return; if (this.address.length === 0) return; - var link = ''; - if (this.isSecure) { - link = `wss://${this.user}:${this.password}@${this.address}:${this.port}/mqtt`; - } else { - link = `ws://${this.user}:${this.password}@${this.address}:${this.port}/mqtt`; - } + var link = `wss://${this.user}:${this.password}@${this.address}:${this.port}/mqtt`; this.client = connect(link); this.connected = true; this.client.on('connect', () => { diff --git a/src/bundles/communication/MultiUserController.ts b/src/bundles/communication/MultiUserController.ts index 66461019a..1d29f06a1 100644 --- a/src/bundles/communication/MultiUserController.ts +++ b/src/bundles/communication/MultiUserController.ts @@ -25,7 +25,6 @@ export class MultiUserController { port: number, user: string, password: string, - isSecure: boolean, ) { let currentController = this.controller; if (currentController) { @@ -47,7 +46,6 @@ export class MultiUserController { currentController.port = port; currentController.user = user; currentController.password = password; - currentController.isSecure = isSecure; currentController.connectClient(); } diff --git a/src/bundles/communication/RpcController.ts b/src/bundles/communication/RpcController.ts index 36c2243d8..1c8e170a6 100644 --- a/src/bundles/communication/RpcController.ts +++ b/src/bundles/communication/RpcController.ts @@ -57,6 +57,15 @@ export class RpcController { this.multiUser.controller?.publish(topic, JSON.stringify(message), false); } + /** + * Obtains user ID for RPC. + * + * @returns String user ID for the device. + */ + public getUserId() { + return this.userId; + } + /** * Exposes a function to other callers. * diff --git a/src/bundles/communication/index.ts b/src/bundles/communication/index.ts index 7373296dc..8971eb75d 100644 --- a/src/bundles/communication/index.ts +++ b/src/bundles/communication/index.ts @@ -22,6 +22,7 @@ export { getGlobalState, updateGlobalState, initRpc, + getUserId, expose, callFunction, keepRunning, From 77c71c12ee8ff1f637373de98f85d7532b56c9ac Mon Sep 17 00:00:00 2001 From: Chong Wen Hao <58220142+8kdesign@users.noreply.github.com> Date: Sat, 23 Mar 2024 07:20:15 +0800 Subject: [PATCH 11/14] Address issue raised in PR --- .../communication/GlobalStateController.ts | 4 ++- src/bundles/communication/MqttController.ts | 29 +++++++++++++------ 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/bundles/communication/GlobalStateController.ts b/src/bundles/communication/GlobalStateController.ts index 8eebf20b0..b1d36f604 100644 --- a/src/bundles/communication/GlobalStateController.ts +++ b/src/bundles/communication/GlobalStateController.ts @@ -81,7 +81,9 @@ export class GlobalStateController { currentJson[splitTopic[splitTopic.length - 1]] = jsonMessage; } this.setGlobalState(newGlobalState); - } catch {} + } catch (error) { + console.log('Failed to parse message', error); + } } /** diff --git a/src/bundles/communication/MqttController.ts b/src/bundles/communication/MqttController.ts index ead416dcc..e16cb4efe 100644 --- a/src/bundles/communication/MqttController.ts +++ b/src/bundles/communication/MqttController.ts @@ -6,14 +6,15 @@ export const STATE_RECONNECTING = 'Reconnecting'; export const STATE_OFFLINE = 'Offline'; /** - * Abstraction of MQTT. + * Abstraction of MQTT for web. + * Simplifies connection process to allow only WebSocket, as web app does not support MQTT over TCP. + * Combines callbacks for status change and decodes message to utf8. * * @param connectionCallback Callback when the connection state changed. * @param messageCallback Callback when a message has been received. */ export class MqttController { private client: MqttClient | null = null; - private connected: boolean = false; private connectionCallback: (status: string) => void; private messageCallback: (topic: string, message: string) => void; @@ -35,11 +36,10 @@ export class MqttController { * Also handles connection status callbacks. */ public async connectClient() { - if (this.connected) return; + if (this.client !== null) return; if (this.address.length === 0) return; var link = `wss://${this.user}:${this.password}@${this.address}:${this.port}/mqtt`; this.client = connect(link); - this.connected = true; this.client.on('connect', () => { this.connectionCallback(STATE_CONNECTED); }); @@ -65,21 +65,29 @@ export class MqttController { if (this.client) { this.client.end(true); } + this.client = null; this.connectionCallback = () => {}; this.messageCallback = () => {}; } /** * Broadcasts message to topic. - * QoS of 1, all listening devices receive the message at least once. * * @param topic Identifier for group of devices to broadcast to. * @param message Message to broadcast. * @param isRetain Whether the message should be retained. + * @param qos 0: Receiver might not receive + * 1: Receiver might receive more than once + * 2: Receiver will receive once and only once */ - public publish(topic: string, message: string, isRetain: boolean) { + public publish( + topic: string, + message: string, + isRetain: boolean, + qos: number = 1, + ) { this.client?.publish(topic, message, { - qos: 1, + qos: qos, retain: isRetain, }); } @@ -89,10 +97,13 @@ export class MqttController { * Qos of 1 to prevent downgrading. * * @param topic Identifier for group of devices receiving the broadcast. + * @param qos 0: Not guaranteed to receive every message + * 1: Receive each message at least once + * 2: Receive each message once and only once */ - public subscribe(topic: string) { + public subscribe(topic: string, qos: number = 1) { if (this.client) { - this.client.subscribe(topic, { qos: 1 }); + this.client.subscribe(topic, { qos: qos }); } } From 17c1d62145c12c6310a2de2ca2c269dea347c820 Mon Sep 17 00:00:00 2001 From: Chong Wen Hao <58220142+8kdesign@users.noreply.github.com> Date: Mon, 25 Mar 2024 12:27:03 +0800 Subject: [PATCH 12/14] Update MqttController.ts --- src/bundles/communication/MqttController.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/bundles/communication/MqttController.ts b/src/bundles/communication/MqttController.ts index e16cb4efe..0ef15c91a 100644 --- a/src/bundles/communication/MqttController.ts +++ b/src/bundles/communication/MqttController.ts @@ -1,4 +1,5 @@ import { connect, type MqttClient } from 'mqtt/dist/mqtt'; +// Need to use "mqtt/dist/mqtt" as "mqtt" requires global, which SA's compiller does not define. export const STATE_CONNECTED = 'Connected'; export const STATE_DISCONNECTED = 'Disconnected'; From e0ce1890a038ca6cceac05df0b6fcc7babf03ef6 Mon Sep 17 00:00:00 2001 From: Chong Wen Hao <58220142+8kdesign@users.noreply.github.com> Date: Mon, 25 Mar 2024 12:32:43 +0800 Subject: [PATCH 13/14] Improved comments --- src/bundles/communication/Communications.ts | 7 ++++++- src/bundles/communication/GlobalStateController.ts | 6 ++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/bundles/communication/Communications.ts b/src/bundles/communication/Communications.ts index 45917d0fc..690d6e21b 100644 --- a/src/bundles/communication/Communications.ts +++ b/src/bundles/communication/Communications.ts @@ -129,7 +129,7 @@ export function updateGlobalState(path: string, updatedState: any) { * Initializes RPC. * * @param topicHeader MQTT topic to use for rpc. - * @param userId Identifier for this user. + * @param userId Identifier for this user, set undefined to generate a random ID. */ export function initRpc(topicHeader: string, userId?: string) { const moduleState = getModuleState(); @@ -144,6 +144,11 @@ export function initRpc(topicHeader: string, userId?: string) { throw new Error('Error: Communication module not initialized.'); } +/** + * Obtains the user's ID. + * + * @returns String for user ID. + */ export function getUserId(): string { const moduleState = getModuleState(); if (moduleState instanceof CommunicationModuleState) { diff --git a/src/bundles/communication/GlobalStateController.ts b/src/bundles/communication/GlobalStateController.ts index b1d36f604..03c86b1fb 100644 --- a/src/bundles/communication/GlobalStateController.ts +++ b/src/bundles/communication/GlobalStateController.ts @@ -40,6 +40,12 @@ export class GlobalStateController { }); } + /** + * Parses the message received via MQTT and updates the global state. + * + * @param shortenedTopic Path of JSON branch. + * @param message New value to set. + */ public parseGlobalStateMessage(shortenedTopic: string, message: string) { let preSplitTopic = shortenedTopic.trim(); if (preSplitTopic.length === 0) { From 3a4274166ddbe44264486c8ae7827d60aa00de83 Mon Sep 17 00:00:00 2001 From: Chong Wen Hao <58220142+8kdesign@users.noreply.github.com> Date: Tue, 26 Mar 2024 19:43:26 +0800 Subject: [PATCH 14/14] Check interval !== undefined --- src/bundles/communication/Communications.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bundles/communication/Communications.ts b/src/bundles/communication/Communications.ts index 690d6e21b..66b9277cd 100644 --- a/src/bundles/communication/Communications.ts +++ b/src/bundles/communication/Communications.ts @@ -61,7 +61,7 @@ export function keepRunning() { * Removes interval that keeps the program running. */ export function stopRunning() { - if (interval) { + if (interval !== undefined) { window.clearInterval(interval); interval = undefined; }