ReactHooks.js

'use strict';

Object.defineProperty(exports, '__esModule', { value: true });

var BrainDevice = require('./BrainDevice.js');
var BrainClient = require('./BrainClient.js');

// import React from 'react';

// class SimpleObjectCache {
// 	_find(...arrayOfValues) {
// 		if(!this._list) {
// 			this._list = [];
// 		}
// 		let hasRecord = null;
// 		this._list.forEach(record => {
// 			hasRecord = record;
// 			let found = true;
// 			record.forEach(value => {
// 				if(!arrayOfValues.includes(value)) {
// 					found = false;
// 				}
// 			});

// 			if(!found) {
// 				hasRecord = null;
// 			}
// 		});
// 		return hasRecord;
// 	}

// 	get(...arrayOfValues) {
// 		return this._find(arrayOfValues);
// 	}

// 	delete(...arrayOfValues) {
// 		const record = this._find(arrayOfValues);
// 		this._list = this._list.filter(r => r !== record);
// 	}

// 	add(...arrayOfValues) {
// 		if(!this.get(...arrayOfValues)) {
// 			this._list.push(arrayOfValues);
// 		}
// 	}
// }


// function useAsync(createPromiseCallback = () => {}, depedantList=[], onEffect = () => {}, offEffect = () => {}) {
// 	const [ asyncData, setAsyncData ] = React.useState(null);
	
// 	const key = [ createPromiseCallback, ...(depedantList || []) ];
// 	if(!useAsync.__flags.get(key)) {
// 		useAsync.__flags.add(key);

// 		if (asyncData) {
// 			if(!asyncData[useAsync.LoadStarted]) {
// 				asyncData[useAsync.LoadStarted] = true;
// 				setAsyncData(asyncData);
// 			}
// 		} else {
// 			setAsyncData({ [useAsync.LoadStarted]: true })
// 		}

// 		createPromiseCallback(asyncData, setAsyncData)
// 		.then(response => {
// 			if(response) {
// 				response[useAsync.LoadDone] = true;
// 			} else {
// 				response = { 
// 					[useAsync.LoadDone]: true, 
// 					[useAsync.EmptyResult]: true
// 				};
// 			}
// 			useAsync.__flags.delete(key);
// 			setAsyncData(response);
// 		})
// 		.catch(error => {
// 			useAsync.__flags.delete(key);
// 			setAsyncData({
// 				[useAsync.LoadDone]: true,
// 				error
// 			})
// 		});
// 	}

// 	React.useEffect(() => {
// 		onEffect(asyncData, setAsyncData);
// 		return () => offEffect(asyncData, setAsyncData);
// 	}, [ asyncData, setAsyncData ]);

// 	return asyncData;
// }

// useAsync.LoadStarted = '__loadStarted__';
// useAsync.LoadDone    = '__loadDone__';
// useAsync.EmptyResult = '__emptyResult__';
// useAsync.__flags     = new SimpleObjectCache();

/**
 * The `BrainClient.ReactHooks` namespace has several utility hooks that make integrating
 * {@link BrainClient} into React apps easy without requiring you to attach event listeners
 * or handle async responses directly in your functional rendering component.
 *  
 * **Example usage:**
 * ```javascript
 * function BrainSecondTicker({ ipAddress }) {
 * 	// getBrainClient does not need a hook because it is not async and always
 *	// returns same client for the same ipAddress
 * 	const bc          = BrainClient.getBrainClient(ipAddress);
 * 	const sysDevice   = BrainClient.ReactHooks.useDevice(bc, 'System Device');
 *	const secondState = BrainClient.ReactHooks.useDeviceState(sysDevice, 'SECOND_STATE');
 * 
 * 	// Render the state as JSX
 *	// This will automatically re-render whenever
 *	// the Brain sends a new value for the state
 *	return (<>
 *		Current Seconds on {ipAddress}: <b>{secondState.normalizedValue}</b>
 *	</>);
 * }
 * ```
 *
 * @class BrainClient.ReactHooks
 */

	// /**
	//  * Utility hook used by the other hooks here to return the result of an async promise
	//  * while guarding against multiple invocations while promise is executing and catching any errors.
	//  * 
	//  * `useAsync` adds a `__loadStarted__` key to the return value when the promise starts, set to `true`.
	//  * When the promise resolves (with error or with data), that key is removed and the `__loadDone__` key is added
	//  * and set to true. If there is an error caught, it will be put in the `error` key on the result.
	//  * 
	//  * Note that even with those keys added, the type of the original data is unchanged **IF** the data from 
	//  * the promise is a truthy value.
	//  * Otherwise, the object will be set to an `object` containing just the relevant `__loadStarted__`/`__loadDone__` and error keys.
	//  * 
	//  * @param {function} createPromiseCallback - Function that returns a thenable/error promise
	//  * @param {array} depedantList - List of depednants that make this promise unique, used to uniquely guard against multiple invocations
	//  * @param {function} onEffect (optional) Effect to run when component mounted
	//  * @param {function} offEffect (optional) Effect to run when component unmounted
	//  * @returns {object} Object decorated with `__loadStarted__`, `__loadDone__`, and possibly `error` keys as needed, as well as the original data returned from the promise
	//  * @memberof BrainClient.ReactHooks
	//  */
	// export function useAsync(createPromiseCallback, depedantList, onEffect, offEffect) {
	// 	return useAsync(createPromiseCallback, depedantList, onEffect, offEffect);
	// }

	/**
	 * Provides a React Hook to retrieve a device from the Brain given the device Name or ID
	 * @param {BrainClient} client - Client to use to access devices
	 * @param {sring} deviceNameOrId - Name or ID of the device to retrieve
	 * @returns {BrainDevice} Returned value will eventually be set to {@link BrainDevice} object
	 * @memberof BrainClient.ReactHooks
	 */
	function useDevice(client, deviceNameOrId) {
		// Dynamically require react so that people don't have to have React
		// installed just to use BrainClient because index.js requires ReactHooks
		// for re-export
		const React = require('react');
		const [ device, setDevice ] = React.useState();

		// If client is a string, assume the user gave IP or Host and resolve 
		// that to a client instance before continuing
		if(typeof(client) === "string") {
			client = BrainClient.default.getBrainClient(client);
		}
	
		if(client) {
			client.getDevice(deviceNameOrId).then(foundDevice => {
				// console.log("[ReactHooks.useDevice] got foundDevice ", foundDevice, "for deviceNameOrId=", deviceNameOrId);
				setDevice(foundDevice);
			});
		} else {
			console.warn("[ReactHooks.useDevice] no client given, cannot get devices");
		}

		return device;
	}

	//
	// Disable these until we can test these out further:
	//
	// /**
	//  * Get the list of commands a device supports. See {@link BrainDevice#getCommands} for more documentation on this result.
	//  * @param {BrainDevice} device Device to get the commands from
	//  * @returns {object} Object of command IDs > command Info objects, see {@link BrainDevice#getCommands} for more documentation on this result.
	//  * @memberof BrainClient.ReactHooks
	//  */
	// static useDeviceCommands(device) {
	// 	return useAsync(async () => {
	// 		if(!device)
	// 			return {};
	// 		return device.getCommands();
	// 	}, [ device ])
	// }
	
	// /**
	//  * Get the list of states and their current values. See {@link BrainDevice#getStates} for more documentation on this result.
	//  * @param {BrainDevice} device Device to get the states from
	//  * @returns {object} Object of state IDs > state Info objects, see {@link BrainDevice#getStates} for more documentation on this result.
	//  * @memberof BrainClient.ReactHooks
	//  */
	// static useDeviceStates(device) {
	// 	return useAsync(async () => {
	// 		if(!device)
	// 			return {};
	// 		device.getStates();
	// 	}, [ device ]);
	// }
	
	/**
	 * Get a specific state from the device. This method will automatically attach a listener to the `BrainDevice.STATE_CHANGED`
	 * event and automatically update the returned value whenever a new state value is received from the brain.
	 * 
	 * Uses `React.useEffect` internally to attach/remove event listener for that event so event listener is removed when 
	 * component unmounts.
	 * 
	 * See {@link BrainDevice#getState} for more documentation on this result.
	 * 
	 * @param {BrainDevice} device Device to get the commands from
	 * @param {string} stateNameOrId Name or ID of state to retrieve and watch
	 * @returns {object} State information object, see {@link BrainDevice#getState} for more documentation on this result.
	 * @memberof BrainClient.ReactHooks
	 */
	function useDeviceState(device, stateNameOrId) {
		// Dynamically require react so that people don't have to have React
		// installed just to use BrainClient because index.js requires ReactHooks
		// for re-export
		const React = require('react');
		const [state, setState] = React.useState(null);

		(async () => {
			if (!device) 
				return null;
			setState(await device.getState(stateNameOrId));
		})();
		
		React.useEffect(() => {
			if(!useDeviceState._listenerCache) {
				useDeviceState._listenerCache = {};
			}

			const onStateChange = useDeviceState._listenerCache[stateNameOrId] = async newData => {
				// We will receive all state changes on the device,
				// so ignore states we don't want
				if(newData.id !== stateNameOrId) {
					return;
				}
				// Use the `getState` method so the data is the same shape as 
				// the original data returned from this hook
				const originalState = await device.getState(stateNameOrId);
				setState(Object.assign({}, originalState, newData));
			};
		
			if (device instanceof BrainDevice.default)
				device.on(BrainDevice.default.STATE_CHANGED, onStateChange);
		
			// Stop listening for changes on unmount
			return  () => {
				const onStateChange = useDeviceState._listenerCache[stateNameOrId];
				if (device instanceof BrainDevice.default)
					device.off(BrainDevice.default.STATE_CHANGED, onStateChange);
			};
		}, [ device, stateNameOrId ]);

		return state;
	}

	/**
	 * Get a React state variable containing current state value from {@link BrainClient.CONNECTION} for 
	 * the given {@link BrainClient}. 
	 * 
	 * Event listeners are attached in a `React.useEffect` hook to automatically attach/unattach
	 * from the `BrainClient.EVENTS.CONNECTION_STATUS_CHANGED` event and automatically update the
	 * variable returned from this hook with the new status whenever the status changes.
	 * 
	 * Related: See {@link BrainClient#getConnectionStatus} and {@link BrainClient.CONNECTION} for documentation on the 
	 * various possible connection states.
	 * 
	 * @param {BrainClient} brainClient Brain client from which to get connection status
	 * @returns {string} One of the connection state strings described in {@link BrainClient.CONNECTION} indicating the current connection state of the given {@link BrainClient}. Related, see {@link BrainClient#getConnectionStatus}.
	 * @memberof BrainClient.ReactHooks
	 */
	function useConnectionStatus(brainClient) {
		// Dynamically require react so that people don't have to have React
		// installed just to use BrainClient because index.js requires ReactHooks
		// for re-export
		const React = require('react');
		const [ connectionStatus, setConnectionStatus ] = React.useState(
			brainClient ?
			brainClient.getConnectionStatus() :
			null
		);
		
		React.useEffect(() => {

			if (brainClient) {
				brainClient.on(
					BrainClient.default.EVENTS.CONNECTION_STATUS_CHANGED,
					data => setConnectionStatus(data.status)
				);

				setConnectionStatus(brainClient.getConnectionStatus());
			}

			return () => {
				if (brainClient) {
					brainClient.off(
						BrainClient.default.EVENTS.CONNECTION_STATUS_CHANGED,
						data => setConnectionStatus(data.status)
					);
				}
			}

		}, [ brainClient ]);
		
		return connectionStatus;
	}


var ReactHooks = {
	useDevice,
	useDeviceState,
	useConnectionStatus
};

exports.default = ReactHooks;
exports.useConnectionStatus = useConnectionStatus;
exports.useDevice = useDevice;
exports.useDeviceState = useDeviceState;
//# sourceMappingURL=ReactHooks.js.map