diff --git a/src/engine/blocks.js b/src/engine/blocks.js index be97b5a0c..b89a41b5d 100644 --- a/src/engine/blocks.js +++ b/src/engine/blocks.js @@ -373,7 +373,7 @@ class Blocks { // into a state where a local var was requested for the stage, // create a stage (global) var after checking for name conflicts // on all the sprites. - if (e.isLocal && editingTarget && !editingTarget.isStage) { + if (e.isLocal && editingTarget && !editingTarget.isStage && !e.isCloud) { if (!editingTarget.lookupVariableById(e.varId)) { editingTarget.createVariable(e.varId, e.varName, e.varType); } @@ -385,7 +385,7 @@ class Blocks { return; } } - stage.createVariable(e.varId, e.varName, e.varType); + stage.createVariable(e.varId, e.varName, e.varType, e.isCloud); } break; case 'var_rename': diff --git a/src/engine/runtime.js b/src/engine/runtime.js index b635bbd1f..e16ac5411 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -322,7 +322,7 @@ class Runtime extends EventEmitter { /** @type {Object.} */ this.ioDevices = { clock: new Clock(), - cloud: new Cloud(), + cloud: new Cloud(this), deviceManager: new DeviceManager(), keyboard: new Keyboard(this), mouse: new Mouse(this), diff --git a/src/engine/target.js b/src/engine/target.js index a25d77da1..331cfa5de 100644 --- a/src/engine/target.js +++ b/src/engine/target.js @@ -233,11 +233,16 @@ class Target extends EventEmitter { * @param {string} id Id of variable * @param {string} name Name of variable. * @param {string} type Type of variable, '', 'broadcast_msg', or 'list' + * @param {boolean} isCloud Whether the variable to create has the isCloud flag set. + * Additional checks are made that the variable can be created as a cloud variable. */ - createVariable (id, name, type) { + createVariable (id, name, type, isCloud) { if (!this.variables.hasOwnProperty(id)) { const newVariable = new Variable(id, name, type, false); this.variables[id] = newVariable; + if (isCloud && this.isStage) { + this.runtime.ioDevices.cloud.requestCreateCloudVariable(newVariable); + } } } diff --git a/src/io/cloud.js b/src/io/cloud.js index e8c95a15c..244e914c8 100644 --- a/src/io/cloud.js +++ b/src/io/cloud.js @@ -26,6 +26,13 @@ class Cloud { * @property {(number | string)} value The scalar value to update the variable with */ + /** + * Part of a cloud io data post indicating a cloud variable was successfully + * created. + * @typedef {object} VarCreateData + * @property {string} name The name of the variable to create + */ + /** * A cloud io data post message. * @typedef {object} CloudIOData @@ -38,8 +45,9 @@ class Cloud { * Cloud IO Device responsible for sending and receiving messages from * cloud provider (mananging the cloud server connection) and interacting * with cloud variables in the current project. + * @param {Runtime} runtime The runtime context for this cloud io device. */ - constructor () { + constructor (runtime) { /** * Reference to the cloud data provider, responsible for mananging * the web socket connection to the cloud data server. @@ -47,6 +55,12 @@ class Cloud { */ this.provider = null; + /** + * Reference to the runtime that owns this cloud io device. + * @type {!Runtime} + */ + this.runtime = runtime; + /** * Reference to the stage target which owns the cloud variables * in the project. @@ -80,6 +94,21 @@ class Cloud { if (data.varUpdate) { this.updateCloudVariable(data.varUpdate); } + + if (data.varCreate) { + this.createCloudVariable(data.varCreate); + } + } + + requestCreateCloudVariable (variable) { + if (this.runtime.canAddCloudVariable()) { + if (this.provider) { + this.provider.createVariable(variable.name, variable.value); + // We'll set the cloud flag and update the + // cloud variable limit when we actually + // get a confirmation from the cloud data server + } + } } /** @@ -94,10 +123,28 @@ class Cloud { } } + /** + * Create a cloud variable based on the message + * received from the cloud provider. + * @param {VarCreateData} varCreate A {@link VarCreateData} object received from the + * cloud data provider confirming the creation of a cloud variable, + * providing its name and value. + */ + createCloudVariable (varCreate) { + const varName = varCreate.name; + + const variable = this.stage.lookupVariableByNameAndType(varName, Variable.SCALAR_TYPE); + if (!variable) { + log.error(`Could not find cloud variable with name: ${varName}`); + } + variable.isCloud = true; + this.runtime.addCloudVariable(); + } + /** * Update a cloud variable in the runtime based on the message received * from the cloud provider. - * @param {VarUpdateData} varUpdate A {@link VarUpdateData} object describing + * @param {VarData} varUpdate A {@link VarData} object describing * a cloud variable update received from the cloud data provider. */ updateCloudVariable (varUpdate) { diff --git a/test/unit/engine_target.js b/test/unit/engine_target.js index 986038774..6f7c61660 100644 --- a/test/unit/engine_target.js +++ b/test/unit/engine_target.js @@ -71,6 +71,58 @@ test('createListVariable creates a list', t => { t.end(); }); +test('createVariable calls cloud io device\'s requestCreateCloudVariable', t => { + const runtime = new Runtime(); + // Mock the requestCreateCloudVariable function + let requestCreateCloudWasCalled = false; + runtime.ioDevices.cloud.requestCreateCloudVariable = () => { + requestCreateCloudWasCalled = true; + }; + + const target = new Target(runtime); + target.isStage = true; + target.createVariable('foo', 'bar', Variable.SCALAR_TYPE, true /* isCloud */); + + const variables = target.variables; + t.equal(Object.keys(variables).length, 1); + const variable = variables[Object.keys(variables)[0]]; + t.equal(variable.id, 'foo'); + t.equal(variable.name, 'bar'); + t.equal(variable.type, Variable.SCALAR_TYPE); + t.equal(variable.value, 0); + // isCloud flag doesn't get set by the target createVariable function + t.equal(variable.isCloud, false); + t.equal(requestCreateCloudWasCalled, true); + + t.end(); +}); + +test('createVariable does not call cloud io device\'s requestCreateCloudVariable if target is not stage', t => { + const runtime = new Runtime(); + // Mock the requestCreateCloudVariable function + let requestCreateCloudWasCalled = false; + runtime.ioDevices.cloud.requestCreateCloudVariable = () => { + requestCreateCloudWasCalled = true; + }; + + const target = new Target(runtime); + target.isStage = false; + target.createVariable('foo', 'bar', Variable.SCALAR_TYPE, true /* isCloud */); + + const variables = target.variables; + t.equal(Object.keys(variables).length, 1); + const variable = variables[Object.keys(variables)[0]]; + t.equal(variable.id, 'foo'); + t.equal(variable.name, 'bar'); + t.equal(variable.type, Variable.SCALAR_TYPE); + t.equal(variable.value, 0); + // isCloud flag doesn't get set by the target createVariable function + t.equal(variable.isCloud, false); + t.equal(requestCreateCloudWasCalled, false); + + t.end(); +}); + test('createVariable throws when given invalid type', t => { const target = new Target(); t.throws( diff --git a/test/unit/io_cloud.js b/test/unit/io_cloud.js index d7f2c7843..0991a4f3d 100644 --- a/test/unit/io_cloud.js +++ b/test/unit/io_cloud.js @@ -2,14 +2,18 @@ const test = require('tap').test; const Cloud = require('../../src/io/cloud'); const Target = require('../../src/engine/target'); const Variable = require('../../src/engine/variable'); +const Runtime = require('../../src/engine/runtime'); test('spec', t => { - const cloud = new Cloud(); + const runtime = new Runtime(); + const cloud = new Cloud(runtime); t.type(cloud, 'object'); t.type(cloud.postData, 'function'); t.type(cloud.requestUpdateVariable, 'function'); t.type(cloud.updateCloudVariable, 'function'); + t.type(cloud.requestCreateCloudVariable, 'function'); + t.type(cloud.createCloudVariable, 'function'); t.type(cloud.setProvider, 'function'); t.type(cloud.setStage, 'function'); t.type(cloud.clear, 'function'); @@ -17,7 +21,8 @@ test('spec', t => { }); test('stage and provider are null initially', t => { - const cloud = new Cloud(); + const runtime = new Runtime(); + const cloud = new Cloud(runtime); t.strictEquals(cloud.provider, null); t.strictEquals(cloud.stage, null); @@ -25,7 +30,8 @@ test('stage and provider are null initially', t => { }); test('setProvider sets the provider', t => { - const cloud = new Cloud(); + const runtime = new Runtime(); + const cloud = new Cloud(runtime); const provider = { foo: 'a fake provider' @@ -37,7 +43,7 @@ test('setProvider sets the provider', t => { t.end(); }); -test('postData updates the variable', t => { +test('postData update message updates the variable', t => { const stage = new Target(); const fooVar = new Variable( 'a fake var id', @@ -49,7 +55,8 @@ test('postData updates the variable', t => { t.strictEquals(fooVar.value, 0); - const cloud = new Cloud(); + const runtime = new Runtime(); + const cloud = new Cloud(runtime); cloud.setStage(stage); cloud.postData({varUpdate: { name: 'foo', @@ -59,6 +66,30 @@ test('postData updates the variable', t => { t.end(); }); +test('postData create message sets isCloud flag on the variable and updates runtime cloud limit', t => { + const stage = new Target(); + const fooVar = new Variable( + 'a fake var id', + 'foo', + Variable.SCALAR_TYPE, + false /* isCloud */ + ); + stage.variables[fooVar.id] = fooVar; + + t.strictEquals(fooVar.value, 0); + t.strictEquals(fooVar.isCloud, false); + + const runtime = new Runtime(); + const cloud = new Cloud(runtime); + cloud.setStage(stage); + cloud.postData({varCreate: { + name: 'foo' + }}); + t.strictEquals(fooVar.isCloud, true); + t.strictEquals(runtime.hasCloudData(), true); + t.end(); +}); + test('requestUpdateVariable calls provider\'s updateVariable function', t => { let updateVariableCalled = false; let mockVarName = ''; @@ -74,7 +105,8 @@ test('requestUpdateVariable calls provider\'s updateVariable function', t => { updateVariable: mockUpdateVariable }; - const cloud = new Cloud(); + const runtime = new Runtime(); + const cloud = new Cloud(runtime); cloud.setProvider(provider); cloud.requestUpdateVariable('foo', 3); t.equals(updateVariableCalled, true); @@ -82,3 +114,31 @@ test('requestUpdateVariable calls provider\'s updateVariable function', t => { t.strictEquals(mockVarValue, 3); t.end(); }); + +test('requestCreateCloudVariable calls provider\'s createVariable function', t => { + let createVariableCalled = false; + const mockVariable = new Variable('a var id', 'my var', Variable.SCALAR_TYPE, false); + let mockVarName; + let mockVarValue; + const mockCreateVariable = (name, value) => { + createVariableCalled = true; + mockVarName = name; + mockVarValue = value; + return; + }; + + const provider = { + createVariable: mockCreateVariable + }; + + const runtime = new Runtime(); + const cloud = new Cloud(runtime); + cloud.setProvider(provider); + cloud.requestCreateCloudVariable(mockVariable); + t.equals(createVariableCalled, true); + t.strictEquals(mockVarName, 'my var'); + t.strictEquals(mockVarValue, 0); + // Calling requestCreateCloudVariable does not set isCloud flag on variable + t.strictEquals(mockVariable.isCloud, false); + t.end(); +});