mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2025-07-05 10:41:03 -04:00
Merge pull request #85 from tmickel/feature/worker
Update to allow running in a WebWorker
This commit is contained in:
commit
578d02cba0
10 changed files with 235 additions and 37 deletions
|
@ -13,7 +13,8 @@
|
||||||
},
|
},
|
||||||
"env": {
|
"env": {
|
||||||
"node": true,
|
"node": true,
|
||||||
"browser": true
|
"browser": true,
|
||||||
|
"worker": true
|
||||||
},
|
},
|
||||||
"extends": "eslint:recommended"
|
"extends": "eslint:recommended"
|
||||||
}
|
}
|
||||||
|
|
4
Makefile
4
Makefile
|
@ -2,6 +2,7 @@ ESLINT=./node_modules/.bin/eslint
|
||||||
NODE=node
|
NODE=node
|
||||||
TAP=./node_modules/.bin/tap
|
TAP=./node_modules/.bin/tap
|
||||||
WEBPACK=./node_modules/.bin/webpack --progress --colors
|
WEBPACK=./node_modules/.bin/webpack --progress --colors
|
||||||
|
WEBPACK_DEV_SERVER=./node_modules/.bin/webpack-dev-server
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@ -11,6 +12,9 @@ build:
|
||||||
watch:
|
watch:
|
||||||
$(WEBPACK) --watch
|
$(WEBPACK) --watch
|
||||||
|
|
||||||
|
serve:
|
||||||
|
$(WEBPACK_DEV_SERVER) --content-base ./
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
"json-loader": "0.5.4",
|
"json-loader": "0.5.4",
|
||||||
"scratch-blocks": "git+https://git@github.com/LLK/scratch-blocks.git",
|
"scratch-blocks": "git+https://git@github.com/LLK/scratch-blocks.git",
|
||||||
"tap": "5.7.1",
|
"tap": "5.7.1",
|
||||||
"webpack": "1.13.0"
|
"webpack": "1.13.0",
|
||||||
|
"webpack-dev-server": "1.14.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -255,8 +255,8 @@
|
||||||
<script src="../node_modules/scratch-blocks/blockly_compressed_vertical.js"></script>
|
<script src="../node_modules/scratch-blocks/blockly_compressed_vertical.js"></script>
|
||||||
<script src="../node_modules/scratch-blocks/blocks_compressed.js"></script>
|
<script src="../node_modules/scratch-blocks/blocks_compressed.js"></script>
|
||||||
<script src="../node_modules/scratch-blocks/blocks_compressed_vertical.js"></script>
|
<script src="../node_modules/scratch-blocks/blocks_compressed_vertical.js"></script>
|
||||||
<!-- Compiled VM -->
|
<!-- VM Worker -->
|
||||||
<script src="../vm.js"></script>
|
<script src="../vm.worker.js"></script>
|
||||||
<!-- Playground -->
|
<!-- Playground -->
|
||||||
<script src="./playground.js"></script>
|
<script src="./playground.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
@ -26,55 +26,61 @@ window.onload = function() {
|
||||||
window.workspace = workspace;
|
window.workspace = workspace;
|
||||||
|
|
||||||
// Block events.
|
// Block events.
|
||||||
workspace.addChangeListener(vm.blockListener);
|
|
||||||
// @todo: Re-enable flyout listening after fixing GH-69.
|
// @todo: Re-enable flyout listening after fixing GH-69.
|
||||||
//var flyoutWorkspace = workspace.toolbox_.flyout_.workspace_;
|
workspace.addChangeListener(vm.blockListener);
|
||||||
//flyoutWorkspace.addChangeListener(vm.flyoutBlockListener);
|
|
||||||
|
|
||||||
|
// Playground data
|
||||||
var blockexplorer = document.getElementById('blockexplorer');
|
var blockexplorer = document.getElementById('blockexplorer');
|
||||||
workspace.addChangeListener(function() {
|
var updateBlockExplorer = function(blocks) {
|
||||||
// On a change, update the block explorer.
|
blockexplorer.innerHTML = JSON.stringify(blocks, null, 2);
|
||||||
blockexplorer.innerHTML = JSON.stringify(vm.runtime.blocks, null, 2);
|
|
||||||
window.hljs.highlightBlock(blockexplorer);
|
window.hljs.highlightBlock(blockexplorer);
|
||||||
});
|
};
|
||||||
|
|
||||||
var threadexplorer = document.getElementById('threadexplorer');
|
var threadexplorer = document.getElementById('threadexplorer');
|
||||||
var cachedThreadJSON = '';
|
var cachedThreadJSON = '';
|
||||||
var updateThreadExplorer = function () {
|
var updateThreadExplorer = function (threads) {
|
||||||
var newJSON = JSON.stringify(vm.runtime.threads, null, 2);
|
var newJSON = JSON.stringify(threads, null, 2);
|
||||||
if (newJSON != cachedThreadJSON) {
|
if (newJSON != cachedThreadJSON) {
|
||||||
cachedThreadJSON = newJSON;
|
cachedThreadJSON = newJSON;
|
||||||
threadexplorer.innerHTML = cachedThreadJSON;
|
threadexplorer.innerHTML = cachedThreadJSON;
|
||||||
window.hljs.highlightBlock(threadexplorer);
|
window.hljs.highlightBlock(threadexplorer);
|
||||||
}
|
}
|
||||||
window.requestAnimationFrame(updateThreadExplorer);
|
|
||||||
};
|
};
|
||||||
updateThreadExplorer();
|
|
||||||
|
var getPlaygroundData = function () {
|
||||||
|
vm.getPlaygroundData();
|
||||||
|
window.requestAnimationFrame(getPlaygroundData);
|
||||||
|
};
|
||||||
|
getPlaygroundData();
|
||||||
|
|
||||||
|
vm.on('playgroundData', function(data) {
|
||||||
|
updateThreadExplorer(data.threads);
|
||||||
|
updateBlockExplorer(data.blocks);
|
||||||
|
});
|
||||||
|
|
||||||
// Feedback for stacks and blocks running.
|
// Feedback for stacks and blocks running.
|
||||||
vm.runtime.on('STACK_GLOW_ON', function(blockId) {
|
vm.on('STACK_GLOW_ON', function(data) {
|
||||||
workspace.glowStack(blockId, true);
|
workspace.glowStack(data.id, true);
|
||||||
});
|
});
|
||||||
vm.runtime.on('STACK_GLOW_OFF', function(blockId) {
|
vm.on('STACK_GLOW_OFF', function(data) {
|
||||||
workspace.glowStack(blockId, false);
|
workspace.glowStack(data.id, false);
|
||||||
});
|
});
|
||||||
vm.runtime.on('BLOCK_GLOW_ON', function(blockId) {
|
vm.on('BLOCK_GLOW_ON', function(data) {
|
||||||
workspace.glowBlock(blockId, true);
|
workspace.glowBlock(data.id, true);
|
||||||
});
|
});
|
||||||
vm.runtime.on('BLOCK_GLOW_OFF', function(blockId) {
|
vm.on('BLOCK_GLOW_OFF', function(data) {
|
||||||
workspace.glowBlock(blockId, false);
|
workspace.glowBlock(data.id, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// Run threads
|
// Run threads
|
||||||
vm.runtime.start();
|
vm.start();
|
||||||
|
|
||||||
// Handlers for green flag and stop all.
|
// Handlers for green flag and stop all.
|
||||||
document.getElementById('greenflag').addEventListener('click', function() {
|
document.getElementById('greenflag').addEventListener('click', function() {
|
||||||
vm.runtime.greenFlag();
|
vm.greenFlag();
|
||||||
});
|
});
|
||||||
document.getElementById('stopall').addEventListener('click', function() {
|
document.getElementById('stopall').addEventListener('click', function() {
|
||||||
vm.runtime.stopAll();
|
vm.stopAll();
|
||||||
});
|
});
|
||||||
|
|
||||||
var tabBlockExplorer = document.getElementById('tab-blockexplorer');
|
var tabBlockExplorer = document.getElementById('tab-blockexplorer');
|
||||||
|
|
|
@ -64,7 +64,7 @@ WeDo2Blocks.prototype._motorOnFor = function(direction, durationSeconds, util) {
|
||||||
YieldTimers.resolve(this._motorTimeout);
|
YieldTimers.resolve(this._motorTimeout);
|
||||||
this._motorTimeout = null;
|
this._motorTimeout = null;
|
||||||
}
|
}
|
||||||
if (window.native) {
|
if (typeof window !== 'undefined' && window.native) {
|
||||||
window.native.motorRun(direction, this._motorSpeed);
|
window.native.motorRun(direction, this._motorSpeed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,7 +73,7 @@ WeDo2Blocks.prototype._motorOnFor = function(direction, durationSeconds, util) {
|
||||||
if (instance._motorTimeout == myTimeout) {
|
if (instance._motorTimeout == myTimeout) {
|
||||||
instance._motorTimeout = null;
|
instance._motorTimeout = null;
|
||||||
}
|
}
|
||||||
if (window.native) {
|
if (typeof window !== 'undefined' && window.native) {
|
||||||
window.native.motorStop();
|
window.native.motorStop();
|
||||||
}
|
}
|
||||||
util.done();
|
util.done();
|
||||||
|
@ -132,7 +132,7 @@ WeDo2Blocks.prototype._getColor = function(colorName) {
|
||||||
};
|
};
|
||||||
|
|
||||||
WeDo2Blocks.prototype.setColor = function(argValues, util) {
|
WeDo2Blocks.prototype.setColor = function(argValues, util) {
|
||||||
if (window.native) {
|
if (typeof window !== 'undefined' && window.native) {
|
||||||
var colorIndex = this._getColor(argValues[0]);
|
var colorIndex = this._getColor(argValues[0]);
|
||||||
window.native.setLedColor(colorIndex);
|
window.native.setLedColor(colorIndex);
|
||||||
}
|
}
|
||||||
|
|
|
@ -210,10 +210,6 @@ Runtime.prototype.stopAll = function () {
|
||||||
// Actually remove the thread.
|
// Actually remove the thread.
|
||||||
this._removeThread(poppedThread);
|
this._removeThread(poppedThread);
|
||||||
}
|
}
|
||||||
// @todo call stop function in all extensions/packages/WeDo stub
|
|
||||||
if (window.native) {
|
|
||||||
window.native.motorStop();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -243,12 +239,29 @@ Runtime.prototype.glowBlock = function (blockId, isGlowing) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* setInterval implementation that works in a WebWorker or not.
|
||||||
|
* @param {?Function} fcn Function to call.
|
||||||
|
* @param {number} interval Interval at which to call it.
|
||||||
|
* @return {number} Value returned by setInterval.
|
||||||
|
*/
|
||||||
|
Runtime.prototype._setInterval = function(fcn, interval) {
|
||||||
|
var setInterval = null;
|
||||||
|
if (typeof window !== 'undefined' && window.setInterval) {
|
||||||
|
setInterval = window.setInterval;
|
||||||
|
} else if (typeof self !== 'undefined' && self.setInterval) {
|
||||||
|
setInterval = self.setInterval;
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return setInterval(fcn, interval);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set up timers to repeatedly step in a browser
|
* Set up timers to repeatedly step in a browser
|
||||||
*/
|
*/
|
||||||
Runtime.prototype.start = function () {
|
Runtime.prototype.start = function () {
|
||||||
if (!window.setInterval) return;
|
this._setInterval(function() {
|
||||||
window.setInterval(function() {
|
|
||||||
this._step();
|
this._step();
|
||||||
}.bind(this), Runtime.THREAD_STEP_INTERVAL);
|
}.bind(this), Runtime.THREAD_STEP_INTERVAL);
|
||||||
};
|
};
|
||||||
|
|
102
src/index.js
102
src/index.js
|
@ -4,6 +4,12 @@ var util = require('util');
|
||||||
var Blocks = require('./engine/blocks');
|
var Blocks = require('./engine/blocks');
|
||||||
var Runtime = require('./engine/runtime');
|
var Runtime = require('./engine/runtime');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the environment is a WebWorker.
|
||||||
|
* @const{boolean}
|
||||||
|
*/
|
||||||
|
var ENV_WORKER = typeof importScripts === 'function';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles connections between blocks, stage, and extensions.
|
* Handles connections between blocks, stage, and extensions.
|
||||||
*
|
*
|
||||||
|
@ -28,6 +34,20 @@ function VirtualMachine () {
|
||||||
instance.flyoutBlockListener = (
|
instance.flyoutBlockListener = (
|
||||||
instance.blocks.generateBlockListener(true, instance.runtime)
|
instance.blocks.generateBlockListener(true, instance.runtime)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Runtime emits are passed along as VM emits.
|
||||||
|
instance.runtime.on(Runtime.STACK_GLOW_ON, function (id) {
|
||||||
|
instance.emit(Runtime.STACK_GLOW_ON, {id: id});
|
||||||
|
});
|
||||||
|
instance.runtime.on(Runtime.STACK_GLOW_OFF, function (id) {
|
||||||
|
instance.emit(Runtime.STACK_GLOW_OFF, {id: id});
|
||||||
|
});
|
||||||
|
instance.runtime.on(Runtime.BLOCK_GLOW_ON, function (id) {
|
||||||
|
instance.emit(Runtime.BLOCK_GLOW_ON, {id: id});
|
||||||
|
});
|
||||||
|
instance.runtime.on(Runtime.BLOCK_GLOW_OFF, function (id) {
|
||||||
|
instance.emit(Runtime.BLOCK_GLOW_OFF, {id: id});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -35,6 +55,88 @@ function VirtualMachine () {
|
||||||
*/
|
*/
|
||||||
util.inherits(VirtualMachine, EventEmitter);
|
util.inherits(VirtualMachine, EventEmitter);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start running the VM - do this before anything else.
|
||||||
|
*/
|
||||||
|
VirtualMachine.prototype.start = function () {
|
||||||
|
this.runtime.start();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* "Green flag" handler - start all threads starting with a green flag.
|
||||||
|
*/
|
||||||
|
VirtualMachine.prototype.greenFlag = function () {
|
||||||
|
this.runtime.greenFlag();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop all threads and running activities.
|
||||||
|
*/
|
||||||
|
VirtualMachine.prototype.stopAll = function () {
|
||||||
|
this.runtime.stopAll();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get data for playground. Data comes back in an emitted event.
|
||||||
|
*/
|
||||||
|
VirtualMachine.prototype.getPlaygroundData = function () {
|
||||||
|
this.emit('playgroundData', {
|
||||||
|
blocks: this.blocks,
|
||||||
|
threads: this.runtime.threads
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Worker handlers: for all public methods available above,
|
||||||
|
* we must also provide a message handler in case the VM is run
|
||||||
|
* from a worker environment.
|
||||||
|
*/
|
||||||
|
if (ENV_WORKER) {
|
||||||
|
self.vmInstance = new VirtualMachine();
|
||||||
|
self.onmessage = function (e) {
|
||||||
|
var messageData = e.data;
|
||||||
|
switch (messageData.method) {
|
||||||
|
case 'start':
|
||||||
|
self.vmInstance.runtime.start();
|
||||||
|
break;
|
||||||
|
case 'greenFlag':
|
||||||
|
self.vmInstance.runtime.greenFlag();
|
||||||
|
break;
|
||||||
|
case 'stopAll':
|
||||||
|
self.vmInstance.runtime.stopAll();
|
||||||
|
break;
|
||||||
|
case 'blockListener':
|
||||||
|
self.vmInstance.blockListener(messageData.args);
|
||||||
|
break;
|
||||||
|
case 'flyoutBlockListener':
|
||||||
|
self.vmInstance.flyoutBlockListener(messageData.args);
|
||||||
|
break;
|
||||||
|
case 'getPlaygroundData':
|
||||||
|
self.postMessage({
|
||||||
|
method: 'playgroundData',
|
||||||
|
blocks: self.vmInstance.blocks,
|
||||||
|
threads: self.vmInstance.runtime.threads
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw 'Unknown method' + messageData.method;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Bind runtime's emitted events to postmessages.
|
||||||
|
self.vmInstance.runtime.on(Runtime.STACK_GLOW_ON, function (id) {
|
||||||
|
self.postMessage({method: Runtime.STACK_GLOW_ON, id: id});
|
||||||
|
});
|
||||||
|
self.vmInstance.runtime.on(Runtime.STACK_GLOW_OFF, function (id) {
|
||||||
|
self.postMessage({method: Runtime.STACK_GLOW_OFF, id: id});
|
||||||
|
});
|
||||||
|
self.vmInstance.runtime.on(Runtime.BLOCK_GLOW_ON, function (id) {
|
||||||
|
self.postMessage({method: Runtime.BLOCK_GLOW_ON, id: id});
|
||||||
|
});
|
||||||
|
self.vmInstance.runtime.on(Runtime.BLOCK_GLOW_OFF, function (id) {
|
||||||
|
self.postMessage({method: Runtime.BLOCK_GLOW_OFF, id: id});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Export and bind to `window`
|
* Export and bind to `window`
|
||||||
*/
|
*/
|
||||||
|
|
70
src/worker.js
Normal file
70
src/worker.js
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
var EventEmitter = require('events');
|
||||||
|
var util = require('util');
|
||||||
|
|
||||||
|
function VirtualMachine () {
|
||||||
|
if (!window.Worker) {
|
||||||
|
console.error('WebWorkers not supported in this environment.' +
|
||||||
|
' Please use the non-worker version (vm.js or vm.min.js).');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var instance = this;
|
||||||
|
EventEmitter.call(instance);
|
||||||
|
instance.vmWorker = new Worker('../vm.js');
|
||||||
|
|
||||||
|
// onmessage calls are converted into emitted events.
|
||||||
|
instance.vmWorker.onmessage = function (e) {
|
||||||
|
instance.emit(e.data.method, e.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
instance.blockListener = function (e) {
|
||||||
|
// Messages from Blockly are not serializable by default.
|
||||||
|
// Pull out the necessary, serializable components to pass across.
|
||||||
|
var serializableE = {
|
||||||
|
blockId: e.blockId,
|
||||||
|
element: e.element,
|
||||||
|
type: e.type,
|
||||||
|
name: e.name,
|
||||||
|
newValue: e.newValue,
|
||||||
|
oldParentId: e.oldParentId,
|
||||||
|
oldInputName: e.oldInputName,
|
||||||
|
newParentId: e.newParentId,
|
||||||
|
newInputName: e.newInputName,
|
||||||
|
xml: {
|
||||||
|
outerHTML: (e.xml) ? e.xml.outerHTML : null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
instance.vmWorker.postMessage({
|
||||||
|
method: 'blockListener',
|
||||||
|
args: serializableE
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inherit from EventEmitter
|
||||||
|
*/
|
||||||
|
util.inherits(VirtualMachine, EventEmitter);
|
||||||
|
|
||||||
|
// For documentation, please see index.js.
|
||||||
|
// These mirror the functionality provided there, with the worker wrapper.
|
||||||
|
VirtualMachine.prototype.getPlaygroundData = function () {
|
||||||
|
this.vmWorker.postMessage({method: 'getPlaygroundData'});
|
||||||
|
};
|
||||||
|
|
||||||
|
VirtualMachine.prototype.start = function () {
|
||||||
|
this.vmWorker.postMessage({method: 'start'});
|
||||||
|
};
|
||||||
|
|
||||||
|
VirtualMachine.prototype.greenFlag = function () {
|
||||||
|
this.vmWorker.postMessage({method: 'greenFlag'});
|
||||||
|
};
|
||||||
|
|
||||||
|
VirtualMachine.prototype.stopAll = function () {
|
||||||
|
this.vmWorker.postMessage({method: 'stopAll'});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export and bind to `window`
|
||||||
|
*/
|
||||||
|
module.exports = VirtualMachine;
|
||||||
|
if (typeof window !== 'undefined') window.VirtualMachine = module.exports;
|
|
@ -3,7 +3,8 @@ var webpack = require('webpack');
|
||||||
module.exports = {
|
module.exports = {
|
||||||
entry: {
|
entry: {
|
||||||
'vm': './src/index.js',
|
'vm': './src/index.js',
|
||||||
'vm.min': './src/index.js'
|
'vm.min': './src/index.js',
|
||||||
|
'vm.worker': './src/worker.js'
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
path: __dirname,
|
path: __dirname,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue