diff --git a/package.json b/package.json index 04e0f6dac88cde6fe20307f862462c495498fa55..f60913a708682b400039336c9fedaf19f08bf238 100644 --- a/package.json +++ b/package.json @@ -105,7 +105,7 @@ "scratch-render": "0.1.0-prerelease.20181029125804", "scratch-storage": "1.1.0", "scratch-svg-renderer": "0.2.0-prerelease.20181024192149", - "scratch-vm": "0.2.0-prerelease.20181025092837", + "scratch-vm": "0.2.0-prerelease.20181030160328", "selenium-webdriver": "3.6.0", "startaudiocontext": "1.2.1", "style-loader": "^0.23.0", diff --git a/src/containers/gui.jsx b/src/containers/gui.jsx index 3146e55a55aadfd1d72e935e37e2397a83deda6d..88344203a72c714592e75fbc090fbb3032326fb7 100644 --- a/src/containers/gui.jsx +++ b/src/containers/gui.jsx @@ -73,6 +73,7 @@ class GUI extends React.Component { const { /* eslint-disable no-unused-vars */ assetHost, + cloudHost, error, hideIntro, isError, @@ -104,6 +105,7 @@ class GUI extends React.Component { GUI.propTypes = { assetHost: PropTypes.string, children: PropTypes.node, + cloudHost: PropTypes.string, error: PropTypes.oneOfType([PropTypes.object, PropTypes.string]), fetchingProject: PropTypes.bool, hideIntro: PropTypes.bool, diff --git a/src/lib/cloud-provider.js b/src/lib/cloud-provider.js new file mode 100644 index 0000000000000000000000000000000000000000..ff3d34c40bc8472e4652d037fa09e82bb211bb73 --- /dev/null +++ b/src/lib/cloud-provider.js @@ -0,0 +1,132 @@ +import log from './log.js'; + + +class CloudProvider { + /** + * A cloud data provider which creates and manages a web socket connection + * to the Scratch cloud data server. This provider is responsible for + * interfacing with the VM's cloud io device. + * @param {string} cloudHost The url for the cloud data server + * @param {VirtualMachine} vm The Scratch virtual machine to interface with + * @param {string} username The username to associate cloud data updates with + * @param {string} projectId The id associated with the project containing + * cloud data. + */ + constructor (cloudHost, vm, username, projectId) { + this.vm = vm; + this.username = username; + this.projectId = projectId; + + // Open a websocket connection to the clouddata server + this.openConnection(cloudHost); + } + + /** + * Open a new websocket connection to the clouddata server. + * @param {string} cloudHost The cloud data server to connect to. + */ + openConnection (cloudHost) { + if (window.WebSocket === null) { + log.warn('Websocket support is not available in this browser'); + this.connection = null; + return; + } + + this.connection = new WebSocket((location.protocol === 'http:' ? 'ws://' : 'wss://') + cloudHost); + + this.connection.onerror = e => { + log.error(`Websocket connection error: ${JSON.stringify(e)}`); + + // TODO Add re-connection attempt logic here + }; + + this.connection.onmessage = event => { + const messageString = event.data; + log.info(`Received websocket message: ${messageString}`); + const message = JSON.parse(messageString); + if (message.method === 'set') { + const varData = { + varUpdate: { + name: message.name, + value: message.value + } + }; + this.vm.postIOData('cloud', varData); + } + }; + + this.connection.onopen = () => { + this.writeToServer('handshake'); + log.info(`Successfully connected to clouddata server.`); + }; + + this.connection.onclose = () => { + log.info(`Closed connection to websocket`); + }; + } + + /** + * Format and send a message to the cloud data server. + * @param {string} methodName The message method, indicating the action to perform. + * @param {string} dataName The name of the cloud variable this message pertains to + * @param {string | number} dataValue The value to set the cloud variable to + * @param {number} dataIndex The index of the item to update (for cloud lists) + * @param {string} dataNewName The new name for the cloud variable (if renaming) + */ + writeToServer (methodName, dataName, dataValue, dataIndex, dataNewName) { + const msg = {}; + msg.method = methodName; + msg.user = this.username; + msg.project_id = this.projectId; + + if (dataName) msg.name = dataName; + if (dataValue) msg.value = dataValue; + if (dataIndex) msg.index = dataIndex; + if (dataNewName) msg.new_name = dataNewName; + + const dataToWrite = JSON.stringify(msg); + this.sendCloudData(dataToWrite); + } + + /** + * Send a formatted message to the cloud data server. + * @param {string} data The formatted message to send. + */ + sendCloudData (data) { + this.connection.send(`${data}\n`); + log.info(`Sent message to clouddata server: ${data}`); + } + + /** + * Provides an API for the VM's cloud IO device to update + * a cloud variable on the server. + * @param {string} name The name of the variable to update + * @param {string | number} value The new value for the variable + */ + updateVariable (name, value) { + this.writeToServer('set', name, value); + } + + /** + * Closes the connection to the web socket and clears the cloud + * provider of references related to the cloud data project. + */ + requestCloseConnection () { + this.connection.close(); + this.clear(); + } + + /** + * Clear this provider of references related to the project + * and current state. + */ + clear () { + this.connection = null; + this.vm = null; + this.username = null; + this.projectId = null; + } + +} + +export default CloudProvider; diff --git a/src/lib/vm-listener-hoc.jsx b/src/lib/vm-listener-hoc.jsx index 5e18cdaf1ec42a76509fc3ab05ab3994e5b77c66..d130cfcddda609f7b5a99b301d4a67da34b9aeff 100644 --- a/src/lib/vm-listener-hoc.jsx +++ b/src/lib/vm-listener-hoc.jsx @@ -102,7 +102,6 @@ const vmListenerHOC = function (WrappedComponent) { /* eslint-disable no-unused-vars */ attachKeyboardEvents, shouldEmitTargetsUpdate, - username, onBlockDragUpdate, onKeyDown, onKeyUp, diff --git a/src/lib/vm-manager-hoc.jsx b/src/lib/vm-manager-hoc.jsx index b8e3c2cd0a0d3d1e9d549414107b50d0e31f9f02..3d7e9a2e92cc1a629da00ee48368997ecf7f878c 100644 --- a/src/lib/vm-manager-hoc.jsx +++ b/src/lib/vm-manager-hoc.jsx @@ -5,6 +5,7 @@ import {connect} from 'react-redux'; import VM from 'scratch-vm'; import AudioEngine from 'scratch-audio'; +import CloudProvider from '../lib/cloud-provider'; import { LoadingStates, @@ -46,6 +47,18 @@ const vmManagerHOC = function (WrappedComponent) { return this.props.vm.loadProject(this.props.projectData) .then(() => { this.props.onLoadedProject(this.props.loadingState, this.props.canSave); + // If the cloud host exists, open a cloud connection and + // set the vm's cloud provider. + if (this.props.cloudHost) { + // TODO check if we should actually + // connect to cloud data based on info from the loaded project and + // info about the user (e.g. scratcher status) + this.props.vm.setCloudProvider(new CloudProvider( + this.props.cloudHost, + this.props.vm, + this.props.username, + this.props.projectId)); + } }) .catch(e => { this.props.onError(e); @@ -54,12 +67,14 @@ const vmManagerHOC = function (WrappedComponent) { render () { const { /* eslint-disable no-unused-vars */ + cloudHost, fontsLoaded, loadingState, onError: onErrorProp, onLoadedProject: onLoadedProjectProp, projectData, projectId, + username, /* eslint-enable no-unused-vars */ isLoadingWithId: isLoadingWithIdProp, vm, @@ -77,6 +92,7 @@ const vmManagerHOC = function (WrappedComponent) { VMManager.propTypes = { canSave: PropTypes.bool, + cloudHost: PropTypes.string, fontsLoaded: PropTypes.bool, isLoadingWithId: PropTypes.bool, loadingState: PropTypes.oneOf(LoadingStates), @@ -84,6 +100,7 @@ const vmManagerHOC = function (WrappedComponent) { onLoadedProject: PropTypes.func, projectData: PropTypes.oneOfType([PropTypes.object, PropTypes.string]), projectId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + username: PropTypes.string, vm: PropTypes.instanceOf(VM).isRequired };