scratch-vm
Version:
Virtual Machine for Scratch 3.0
634 lines (585 loc) • 22.6 kB
JavaScript
(function webpackUniversalModuleDefinition(root, factory) {
if(typeof exports === 'object' && typeof module === 'object')
module.exports = factory();
else if(typeof define === 'function' && define.amd)
define([], factory);
else if(typeof exports === 'object')
exports["VirtualMachine"] = factory();
else
root["VirtualMachine"] = factory();
})(global, () => {
return /******/ (() => { // webpackBootstrap
/******/ var __webpack_modules__ = ({
/***/ "./src/dispatch/shared-dispatch.js":
/*!*****************************************!*\
!*** ./src/dispatch/shared-dispatch.js ***!
\*****************************************/
/***/ ((module, __unused_webpack_exports, __webpack_require__) => {
const log = __webpack_require__(/*! ../util/log */ "./src/util/log.js");
/**
* @typedef {object} DispatchCallMessage - a message to the dispatch system representing a service method call
* @property {*} responseId - send a response message with this response ID. See {@link DispatchResponseMessage}
* @property {string} service - the name of the service to be called
* @property {string} method - the name of the method to be called
* @property {Array|undefined} args - the arguments to be passed to the method
*/
/**
* @typedef {object} DispatchResponseMessage - a message to the dispatch system representing the results of a call
* @property {*} responseId - a copy of the response ID from the call which generated this response
* @property {*|undefined} error - if this is truthy, then it contains results from a failed call (such as an exception)
* @property {*|undefined} result - if error is not truthy, then this contains the return value of the call (if any)
*/
/**
* @typedef {DispatchCallMessage|DispatchResponseMessage} DispatchMessage
* Any message to the dispatch system.
*/
/**
* The SharedDispatch class is responsible for dispatch features shared by
* {@link CentralDispatch} and {@link WorkerDispatch}.
*/
class SharedDispatch {
constructor() {
/**
* List of callback registrations for promises waiting for a response from a call to a service on another
* worker. A callback registration is an array of [resolve,reject] Promise functions.
* Calls to local services don't enter this list.
* @type {Array.<Function[]>}
*/
this.callbacks = [];
/**
* The next response ID to be used.
* @type {int}
*/
this.nextResponseId = 0;
}
/**
* Call a particular method on a particular service, regardless of whether that service is provided locally or on
* a worker. If the service is provided by a worker, the `args` will be copied using the Structured Clone
* algorithm, except for any items which are also in the `transfer` list. Ownership of those items will be
* transferred to the worker, and they should not be used after this call.
* @example
* dispatcher.call('vm', 'setData', 'cat', 42);
* // this finds the worker for the 'vm' service, then on that worker calls:
* vm.setData('cat', 42);
* @param {string} service - the name of the service.
* @param {string} method - the name of the method.
* @param {*} [args] - the arguments to be copied to the method, if any.
* @returns {Promise} - a promise for the return value of the service method.
*/
call(service, method) {
for (var _len = arguments.length, args = new Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) {
args[_key - 2] = arguments[_key];
}
return this.transferCall(service, method, null, ...args);
}
/**
* Call a particular method on a particular service, regardless of whether that service is provided locally or on
* a worker. If the service is provided by a worker, the `args` will be copied using the Structured Clone
* algorithm, except for any items which are also in the `transfer` list. Ownership of those items will be
* transferred to the worker, and they should not be used after this call.
* @example
* dispatcher.transferCall('vm', 'setData', [myArrayBuffer], 'cat', myArrayBuffer);
* // this finds the worker for the 'vm' service, transfers `myArrayBuffer` to it, then on that worker calls:
* vm.setData('cat', myArrayBuffer);
* @param {string} service - the name of the service.
* @param {string} method - the name of the method.
* @param {Array} [transfer] - objects to be transferred instead of copied. Must be present in `args` to be useful.
* @param {*} [args] - the arguments to be copied to the method, if any.
* @returns {Promise} - a promise for the return value of the service method.
*/
transferCall(service, method, transfer) {
try {
const {
provider,
isRemote
} = this._getServiceProvider(service);
if (provider) {
for (var _len2 = arguments.length, args = new Array(_len2 > 3 ? _len2 - 3 : 0), _key2 = 3; _key2 < _len2; _key2++) {
args[_key2 - 3] = arguments[_key2];
}
if (isRemote) {
return this._remoteTransferCall(provider, service, method, transfer, ...args);
}
// TODO: verify correct `this` after switching from apply to spread
// eslint-disable-next-line prefer-spread
const result = provider[method].apply(provider, args);
return Promise.resolve(result);
}
return Promise.reject(new Error("Service not found: ".concat(service)));
} catch (e) {
return Promise.reject(e);
}
}
/**
* Check if a particular service lives on another worker.
* @param {string} service - the service to check.
* @returns {boolean} - true if the service is remote (calls must cross a Worker boundary), false otherwise.
* @private
*/
_isRemoteService(service) {
return this._getServiceProvider(service).isRemote;
}
/**
* Like {@link call}, but force the call to be posted through a particular communication channel.
* @param {object} provider - send the call through this object's `postMessage` function.
* @param {string} service - the name of the service.
* @param {string} method - the name of the method.
* @param {*} [args] - the arguments to be copied to the method, if any.
* @returns {Promise} - a promise for the return value of the service method.
*/
_remoteCall(provider, service, method) {
for (var _len3 = arguments.length, args = new Array(_len3 > 3 ? _len3 - 3 : 0), _key3 = 3; _key3 < _len3; _key3++) {
args[_key3 - 3] = arguments[_key3];
}
return this._remoteTransferCall(provider, service, method, null, ...args);
}
/**
* Like {@link transferCall}, but force the call to be posted through a particular communication channel.
* @param {object} provider - send the call through this object's `postMessage` function.
* @param {string} service - the name of the service.
* @param {string} method - the name of the method.
* @param {Array} [transfer] - objects to be transferred instead of copied. Must be present in `args` to be useful.
* @param {*} [args] - the arguments to be copied to the method, if any.
* @returns {Promise} - a promise for the return value of the service method.
*/
_remoteTransferCall(provider, service, method, transfer) {
for (var _len4 = arguments.length, args = new Array(_len4 > 4 ? _len4 - 4 : 0), _key4 = 4; _key4 < _len4; _key4++) {
args[_key4 - 4] = arguments[_key4];
}
return new Promise((resolve, reject) => {
const responseId = this._storeCallbacks(resolve, reject);
/** @TODO: remove this hack! this is just here so we don't try to send `util` to a worker */
if (args.length > 0 && typeof args[args.length - 1].yield === 'function') {
args.pop();
}
if (transfer) {
provider.postMessage({
service,
method,
responseId,
args
}, transfer);
} else {
provider.postMessage({
service,
method,
responseId,
args
});
}
});
}
/**
* Store callback functions pending a response message.
* @param {Function} resolve - function to call if the service method returns.
* @param {Function} reject - function to call if the service method throws.
* @returns {*} - a unique response ID for this set of callbacks. See {@link _deliverResponse}.
* @protected
*/
_storeCallbacks(resolve, reject) {
const responseId = this.nextResponseId++;
this.callbacks[responseId] = [resolve, reject];
return responseId;
}
/**
* Deliver call response from a worker. This should only be called as the result of a message from a worker.
* @param {int} responseId - the response ID of the callback set to call.
* @param {DispatchResponseMessage} message - the message containing the response value(s).
* @protected
*/
_deliverResponse(responseId, message) {
try {
const [resolve, reject] = this.callbacks[responseId];
delete this.callbacks[responseId];
if (message.error) {
reject(message.error);
} else {
resolve(message.result);
}
} catch (e) {
log.error("Dispatch callback failed: ".concat(JSON.stringify(e)));
}
}
/**
* Handle a message event received from a connected worker.
* @param {Worker} worker - the worker which sent the message, or the global object if running in a worker.
* @param {MessageEvent} event - the message event to be handled.
* @protected
*/
_onMessage(worker, event) {
/** @type {DispatchMessage} */
const message = event.data;
message.args = message.args || [];
let promise;
if (message.service) {
if (message.service === 'dispatch') {
promise = this._onDispatchMessage(worker, message);
} else {
promise = this.call(message.service, message.method, ...message.args);
}
} else if (typeof message.responseId === 'undefined') {
log.error("Dispatch caught malformed message from a worker: ".concat(JSON.stringify(event)));
} else {
this._deliverResponse(message.responseId, message);
}
if (promise) {
if (typeof message.responseId === 'undefined') {
log.error("Dispatch message missing required response ID: ".concat(JSON.stringify(event)));
} else {
promise.then(result => worker.postMessage({
responseId: message.responseId,
result
}), error => worker.postMessage({
responseId: message.responseId,
error
}));
}
}
}
/**
* Fetch the service provider object for a particular service name.
* @abstract
* @param {string} service - the name of the service to look up
* @returns {{provider:(object|Worker), isRemote:boolean}} - the means to contact the service, if found
* @protected
*/
_getServiceProvider(service) {
throw new Error("Could not get provider for ".concat(service, ": _getServiceProvider not implemented"));
}
/**
* Handle a call message sent to the dispatch service itself
* @abstract
* @param {Worker} worker - the worker which sent the message.
* @param {DispatchCallMessage} message - the message to be handled.
* @returns {Promise|undefined} - a promise for the results of this operation, if appropriate
* @private
*/
_onDispatchMessage(worker, message) {
throw new Error("Unimplemented dispatch message handler cannot handle ".concat(message.method, " method"));
}
}
module.exports = SharedDispatch;
/***/ }),
/***/ "./src/dispatch/worker-dispatch.js":
/*!*****************************************!*\
!*** ./src/dispatch/worker-dispatch.js ***!
\*****************************************/
/***/ ((module, __unused_webpack_exports, __webpack_require__) => {
const SharedDispatch = __webpack_require__(/*! ./shared-dispatch */ "./src/dispatch/shared-dispatch.js");
const log = __webpack_require__(/*! ../util/log */ "./src/util/log.js");
/**
* This class provides a Worker with the means to participate in the message dispatch system managed by CentralDispatch.
* From any context in the messaging system, the dispatcher's "call" method can call any method on any "service"
* provided in any participating context. The dispatch system will forward function arguments and return values across
* worker boundaries as needed.
* @see {CentralDispatch}
*/
class WorkerDispatch extends SharedDispatch {
constructor() {
super();
/**
* This promise will be resolved when we have successfully connected to central dispatch.
* @type {Promise}
* @see {waitForConnection}
* @private
*/
this._connectionPromise = new Promise(resolve => {
this._onConnect = resolve;
});
/**
* Map of service name to local service provider.
* If a service is not listed here, it is assumed to be provided by another context (another Worker or the main
* thread).
* @see {setService}
* @type {object}
*/
this.services = {};
this._onMessage = this._onMessage.bind(this, self);
if (typeof self !== 'undefined') {
self.onmessage = this._onMessage;
}
}
/**
* @returns {Promise} a promise which will resolve upon connection to central dispatch. If you need to make a call
* immediately on "startup" you can attach a 'then' to this promise.
* @example
* dispatch.waitForConnection.then(() => {
* dispatch.call('myService', 'hello');
* })
*/
get waitForConnection() {
return this._connectionPromise;
}
/**
* Set a local object as the global provider of the specified service.
* WARNING: Any method on the provider can be called from any worker within the dispatch system.
* @param {string} service - a globally unique string identifying this service. Examples: 'vm', 'gui', 'extension9'.
* @param {object} provider - a local object which provides this service.
* @returns {Promise} - a promise which will resolve once the service is registered.
*/
setService(service, provider) {
if (Object.prototype.hasOwnProperty.call(this.services, service)) {
log.warn("Worker dispatch replacing existing service provider for ".concat(service));
}
this.services[service] = provider;
return this.waitForConnection.then(() => this._remoteCall(self, 'dispatch', 'setService', service));
}
/**
* Fetch the service provider object for a particular service name.
* @override
* @param {string} service - the name of the service to look up
* @returns {{provider:(object|Worker), isRemote:boolean}} - the means to contact the service, if found
* @protected
*/
_getServiceProvider(service) {
// if we don't have a local service by this name, contact central dispatch by calling `postMessage` on self
const provider = this.services[service];
return {
provider: provider || self,
isRemote: !provider
};
}
/**
* Handle a call message sent to the dispatch service itself
* @override
* @param {Worker} worker - the worker which sent the message.
* @param {DispatchCallMessage} message - the message to be handled.
* @returns {Promise|undefined} - a promise for the results of this operation, if appropriate
* @protected
*/
_onDispatchMessage(worker, message) {
let promise;
switch (message.method) {
case 'handshake':
promise = this._onConnect();
break;
case 'terminate':
// Don't close until next tick, after sending confirmation back
setTimeout(() => self.close(), 0);
promise = Promise.resolve();
break;
default:
log.error("Worker dispatch received message for unknown method: ".concat(message.method));
}
return promise;
}
}
module.exports = new WorkerDispatch();
/***/ }),
/***/ "./src/extension-support/argument-type.js":
/*!************************************************!*\
!*** ./src/extension-support/argument-type.js ***!
\************************************************/
/***/ ((module) => {
/**
* Block argument types
* @enum {string}
*/
const ArgumentType = {
/**
* Numeric value with angle picker
*/
ANGLE: 'angle',
/**
* Boolean value with hexagonal placeholder
*/
BOOLEAN: 'Boolean',
/**
* Numeric value with color picker
*/
COLOR: 'color',
/**
* Numeric value with text field
*/
NUMBER: 'number',
/**
* String value with text field
*/
STRING: 'string',
/**
* String value with matrix field
*/
MATRIX: 'matrix',
/**
* MIDI note number with note picker (piano) field
*/
NOTE: 'note',
/**
* Inline image on block (as part of the label)
*/
IMAGE: 'image'
};
module.exports = ArgumentType;
/***/ }),
/***/ "./src/extension-support/block-type.js":
/*!*********************************************!*\
!*** ./src/extension-support/block-type.js ***!
\*********************************************/
/***/ ((module) => {
/**
* Types of block
* @enum {string}
*/
const BlockType = {
/**
* Boolean reporter with hexagonal shape
*/
BOOLEAN: 'Boolean',
/**
* A button (not an actual block) for some special action, like making a variable
*/
BUTTON: 'button',
/**
* Command block
*/
COMMAND: 'command',
/**
* Specialized command block which may or may not run a child branch
* The thread continues with the next block whether or not a child branch ran.
*/
CONDITIONAL: 'conditional',
/**
* Specialized hat block with no implementation function
* This stack only runs if the corresponding event is emitted by other code.
*/
EVENT: 'event',
/**
* Hat block which conditionally starts a block stack
*/
HAT: 'hat',
/**
* Specialized command block which may or may not run a child branch
* If a child branch runs, the thread evaluates the loop block again.
*/
LOOP: 'loop',
/**
* General reporter with numeric or string value
*/
REPORTER: 'reporter'
};
module.exports = BlockType;
/***/ }),
/***/ "./src/extension-support/target-type.js":
/*!**********************************************!*\
!*** ./src/extension-support/target-type.js ***!
\**********************************************/
/***/ ((module) => {
/**
* Default types of Target supported by the VM
* @enum {string}
*/
const TargetType = {
/**
* Rendered target which can move, change costumes, etc.
*/
SPRITE: 'sprite',
/**
* Rendered target which cannot move but can change backdrops
*/
STAGE: 'stage'
};
module.exports = TargetType;
/***/ }),
/***/ "./src/util/log.js":
/*!*************************!*\
!*** ./src/util/log.js ***!
\*************************/
/***/ ((module, __unused_webpack_exports, __webpack_require__) => {
const minilog = __webpack_require__(/*! minilog */ "minilog");
minilog.enable();
module.exports = minilog('vm');
/***/ }),
/***/ "minilog":
/*!**************************!*\
!*** external "minilog" ***!
\**************************/
/***/ ((module) => {
"use strict";
module.exports = require("minilog");
/***/ })
/******/ });
/************************************************************************/
/******/ // The module cache
/******/ var __webpack_module_cache__ = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ var cachedModule = __webpack_module_cache__[moduleId];
/******/ if (cachedModule !== undefined) {
/******/ return cachedModule.exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = __webpack_module_cache__[moduleId] = {
/******/ // no module.id needed
/******/ // no module.loaded needed
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/************************************************************************/
var __webpack_exports__ = {};
// This entry needs to be wrapped in an IIFE because it needs to be isolated against other modules in the chunk.
(() => {
/*!***************************************************!*\
!*** ./src/extension-support/extension-worker.js ***!
\***************************************************/
/* eslint-env worker */
const ArgumentType = __webpack_require__(/*! ../extension-support/argument-type */ "./src/extension-support/argument-type.js");
const BlockType = __webpack_require__(/*! ../extension-support/block-type */ "./src/extension-support/block-type.js");
const dispatch = __webpack_require__(/*! ../dispatch/worker-dispatch */ "./src/dispatch/worker-dispatch.js");
const TargetType = __webpack_require__(/*! ../extension-support/target-type */ "./src/extension-support/target-type.js");
class ExtensionWorker {
constructor() {
this.nextExtensionId = 0;
this.initialRegistrations = [];
dispatch.waitForConnection.then(() => {
dispatch.call('extensions', 'allocateWorker').then(x => {
const [id, extension] = x;
this.workerId = id;
try {
importScripts(extension);
const initialRegistrations = this.initialRegistrations;
this.initialRegistrations = null;
Promise.all(initialRegistrations).then(() => dispatch.call('extensions', 'onWorkerInit', id));
} catch (e) {
dispatch.call('extensions', 'onWorkerInit', id, e);
}
});
});
this.extensions = [];
}
register(extensionObject) {
const extensionId = this.nextExtensionId++;
this.extensions.push(extensionObject);
const serviceName = "extension.".concat(this.workerId, ".").concat(extensionId);
const promise = dispatch.setService(serviceName, extensionObject).then(() => dispatch.call('extensions', 'registerExtensionService', serviceName));
if (this.initialRegistrations) {
this.initialRegistrations.push(promise);
}
return promise;
}
}
global.Scratch = global.Scratch || {};
global.Scratch.ArgumentType = ArgumentType;
global.Scratch.BlockType = BlockType;
global.Scratch.TargetType = TargetType;
/**
* Expose only specific parts of the worker to extensions.
*/
const extensionWorker = new ExtensionWorker();
global.Scratch.extensions = {
register: extensionWorker.register.bind(extensionWorker)
};
})();
/******/ return __webpack_exports__;
/******/ })()
;
});
//# sourceMappingURL=extension-worker.js.map