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 e0e57ad6a..b08c464d6 100644 --- a/package.json +++ b/package.json @@ -106,9 +106,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.48", "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", @@ -120,7 +123,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": [ diff --git a/src/bundles/communication/Communications.ts b/src/bundles/communication/Communications.ts new file mode 100644 index 000000000..66b9277cd --- /dev/null +++ b/src/bundles/communication/Communications.ts @@ -0,0 +1,200 @@ +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, user: string, password: string) { + const multiUser = new MultiUserController(); + multiUser.setupController(address, port, user, password); + this.multiUser = multiUser; + } +} + +/** + * Initializes connection with MQTT broker. + * Currently only supports WebSocket. + * + * @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. + */ +export function initCommunications( + 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; +} + +// 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 !== undefined) { + 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, +) { + 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.'); +} + +/** + * Obtains the current global state. + * + * @returns Current global state. + */ +export function getGlobalState() { + const moduleState = getModuleState(); + if (moduleState instanceof CommunicationModuleState) { + return moduleState.globalState?.globalState; + } + throw new Error('Error: Communication module not initialized.'); +} + +/** + * 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) { + const moduleState = getModuleState(); + if (moduleState instanceof CommunicationModuleState) { + moduleState.globalState?.updateGlobalState(path, updatedState); + return; + } + throw new Error('Error: Communication module not initialized.'); +} + +// Rpc + +/** + * Initializes RPC. + * + * @param topicHeader MQTT topic to use for rpc. + * @param userId Identifier for this user, set undefined to generate a random ID. + */ +export function initRpc(topicHeader: string, userId?: string) { + const moduleState = getModuleState(); + if (moduleState instanceof CommunicationModuleState) { + moduleState.rpc = new RpcController( + topicHeader, + moduleState.multiUser, + userId, + ); + return; + } + 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) { + 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. + * + * @param name Identifier for the function. + * @param func Function to call when request received. + */ +export function expose(name: string, func: (...args: any[]) => any) { + const moduleState = getModuleState(); + if (moduleState instanceof CommunicationModuleState) { + moduleState.rpc?.expose(name, func); + return; + } + throw new Error('Error: Communication module not initialized.'); +} + +/** + * 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, +) { + 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 new file mode 100644 index 000000000..03c86b1fb --- /dev/null +++ b/src/bundles/communication/GlobalStateController.ts @@ -0,0 +1,125 @@ +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) => { + const shortenedTopic = topic.substring( + this.topicHeader.length, + topic.length, + ); + this.parseGlobalStateMessage(shortenedTopic, message); + }); + } + + /** + * 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) { + try { + this.setGlobalState(JSON.parse(message)); + } catch { + this.setGlobalState(undefined); + } + return; + } + if (!preSplitTopic.startsWith('/')) { + preSplitTopic = `/${preSplitTopic}`; + } + const 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++) { + const 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 { + const jsonMessage = JSON.parse(message); + currentJson[splitTopic[splitTopic.length - 1]] = jsonMessage; + } + this.setGlobalState(newGlobalState); + } catch (error) { + console.log('Failed to parse message', error); + } + } + + /** + * 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..0ef15c91a --- /dev/null +++ b/src/bundles/communication/MqttController.ts @@ -0,0 +1,119 @@ +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'; +export const STATE_RECONNECTING = 'Reconnecting'; +export const STATE_OFFLINE = 'Offline'; + +/** + * 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 connectionCallback: (status: string) => void; + private messageCallback: (topic: string, message: string) => void; + + address: string = ''; + port: number = 443; + user: string = ''; + password: string = ''; + + 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 async connectClient() { + 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.client.on('connect', () => { + this.connectionCallback(STATE_CONNECTED); + }); + this.client.on('disconnect', () => { + this.connectionCallback(STATE_DISCONNECTED); + }); + this.client.on('reconnect', () => { + this.connectionCallback(STATE_RECONNECTING); + }); + this.client.on('offline', () => { + this.connectionCallback(STATE_OFFLINE); + }); + this.client.on('message', (topic, message) => { + const 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.client = null; + this.connectionCallback = () => {}; + this.messageCallback = () => {}; + } + + /** + * Broadcasts message to topic. + * + * @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, + qos: number = 1, + ) { + this.client?.publish(topic, message, { + qos: qos, + retain: isRetain, + }); + } + + /** + * Subscribes to a topic. + * 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, qos: number = 1) { + if (this.client) { + this.client.subscribe(topic, { qos: qos }); + } + } + + /** + * 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..1d29f06a1 --- /dev/null +++ b/src/bundles/communication/MultiUserController.ts @@ -0,0 +1,83 @@ +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 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( + address: string, + port: number, + user: string, + password: string, + ) { + let currentController = this.controller; + if (currentController) { + currentController.disconnect(); + this.connectionState = STATE_DISCONNECTED; + } else { + currentController = new MqttController( + (status: string) => { + this.connectionState = status; + console.log(status); + }, + (topic: string, message: string) => { + this.handleIncomingMessage(topic, message); + }, + ); + this.controller = currentController; + } + 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. + * + * @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..1c8e170a6 --- /dev/null +++ b/src/bundles/communication/RpcController.ts @@ -0,0 +1,131 @@ +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) => { + const messageJson = JSON.parse(message); + const callId = messageJson.callId; + const 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) { + const message = { + callId, + result, + }; + const topic = `${this.topicHeader}_return/${sender}`; + 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. + * + * @param name Name for the function, cannot include '/'. + * @param func Function to run. + */ + public expose(name: string, func: (...args: any[]) => any) { + const item = { + name, + func, + }; + this.functions.set(name, item); + const functionTopic = `${this.topicHeader}/${this.userId}/${name}`; + this.multiUser.addMessageCallback(functionTopic, (topic, message) => { + const splitTopic = topic.split('/'); + if (splitTopic.length !== 3) { + return; + } + const parsedMessage = JSON.parse(message); + const callId = parsedMessage.callId; + const sender = parsedMessage.sender; + if (!callId || !sender) return; + const calledName = splitTopic[2]; + const calledFunc = this.functions.get(calledName); + if (!calledFunc) { + this.returnResponse(sender, callId, null); + return; + } + try { + const result = calledFunc?.func(...parsedMessage.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, + ) { + const topic = `${this.topicHeader}/${receiver}/${name}`; + const callId = uniqid(); + this.pendingReturns.set(callId, callback); + const messageJson = { + sender: this.userId, + callId, + args, + }; + const 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..8971eb75d --- /dev/null +++ b/src/bundles/communication/index.ts @@ -0,0 +1,30 @@ +/** + * 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_RECONNECTING, +} from './MqttController'; + +export { + initCommunications, + initGlobalState, + getGlobalState, + updateGlobalState, + initRpc, + getUserId, + expose, + callFunction, + keepRunning, + stopRunning, +} from './Communications'; diff --git a/yarn.lock b/yarn.lock index 0c2c34a6a..3d625f02a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1572,7 +1572,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== @@ -1879,11 +1879,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" @@ -2003,7 +2021,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== @@ -2169,6 +2187,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" @@ -2634,6 +2662,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" @@ -2685,6 +2718,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" @@ -2953,17 +2991,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: @@ -2980,7 +3018,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== @@ -3143,6 +3181,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" @@ -4012,7 +4058,7 @@ js-base64@^3.7.5: resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-3.7.7.tgz#e51b84bf78fbf5702b9541e2cb7bfcb893b43e79" integrity sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw== -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== @@ -4168,6 +4214,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" @@ -4471,6 +4522,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" @@ -4481,10 +4564,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" @@ -4519,7 +4602,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== @@ -4531,12 +4621,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" @@ -4591,6 +4682,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" @@ -4720,6 +4819,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" @@ -4975,6 +5079,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" @@ -5023,11 +5132,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" @@ -5163,6 +5284,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" @@ -5208,6 +5338,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" @@ -5268,6 +5403,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" @@ -5546,6 +5686,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" @@ -5572,6 +5719,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" @@ -5900,6 +6052,11 @@ typed-array-length@^1.0.4: for-each "^0.3.3" is-typed-array "^1.1.9" +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" @@ -5942,6 +6099,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" @@ -6008,6 +6170,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" + use-sync-external-store@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" @@ -6194,6 +6364,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" @@ -6216,7 +6391,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==