UNPKG

scratch-vm

Version:
634 lines (585 loc) • 22.6 kB
(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