@scratch/scratch-vm 11.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.jsdoc.json +20 -0
- package/.nvmrc +1 -0
- package/CHANGELOG.md +7004 -0
- package/LICENSE +12 -0
- package/README.md +120 -0
- package/TRADEMARK +1 -0
- package/commitlint.config.js +4 -0
- package/docs/extensions.md +527 -0
- package/package.json +103 -0
- package/release.config.js +13 -0
- package/src/blocks/scratch3_control.js +206 -0
- package/src/blocks/scratch3_core_example.js +69 -0
- package/src/blocks/scratch3_data.js +257 -0
- package/src/blocks/scratch3_event.js +137 -0
- package/src/blocks/scratch3_looks.js +612 -0
- package/src/blocks/scratch3_motion.js +288 -0
- package/src/blocks/scratch3_operators.js +154 -0
- package/src/blocks/scratch3_procedures.js +79 -0
- package/src/blocks/scratch3_sensing.js +336 -0
- package/src/blocks/scratch3_sound.js +350 -0
- package/src/dispatch/central-dispatch.js +143 -0
- package/src/dispatch/shared-dispatch.js +235 -0
- package/src/dispatch/worker-dispatch.js +110 -0
- package/src/engine/adapter.js +176 -0
- package/src/engine/block-utility.js +242 -0
- package/src/engine/blocks-execute-cache.js +19 -0
- package/src/engine/blocks-runtime-cache.js +78 -0
- package/src/engine/blocks.js +1299 -0
- package/src/engine/comment.js +56 -0
- package/src/engine/execute.js +580 -0
- package/src/engine/monitor-record.js +23 -0
- package/src/engine/mutation-adapter.js +48 -0
- package/src/engine/profiler.js +390 -0
- package/src/engine/runtime.js +2686 -0
- package/src/engine/scratch-blocks-constants.js +27 -0
- package/src/engine/sequencer.js +361 -0
- package/src/engine/stage-layering.js +29 -0
- package/src/engine/target.js +799 -0
- package/src/engine/thread.js +404 -0
- package/src/engine/variable.js +70 -0
- package/src/extension-support/argument-type.js +47 -0
- package/src/extension-support/block-type.js +50 -0
- package/src/extension-support/define-messages.js +18 -0
- package/src/extension-support/extension-manager.js +440 -0
- package/src/extension-support/extension-metadata.js +64 -0
- package/src/extension-support/extension-worker.js +59 -0
- package/src/extension-support/reporter-scope.js +18 -0
- package/src/extension-support/target-type.js +17 -0
- package/src/extensions/scratch3_boost/index.js +2113 -0
- package/src/extensions/scratch3_ev3/index.js +1355 -0
- package/src/extensions/scratch3_gdx_for/index.js +981 -0
- package/src/extensions/scratch3_gdx_for/scratch-link-device-adapter.js +44 -0
- package/src/extensions/scratch3_makeymakey/index.js +396 -0
- package/src/extensions/scratch3_microbit/index.js +984 -0
- package/src/extensions/scratch3_music/assets/drums/1-snare.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/10-wood-block.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/11-cowbell.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/12-triangle.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/13-bongo.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/14-conga.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/15-cabasa.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/16-guiro.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/17-vibraslap.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/18-cuica.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/2-bass-drum.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/3-side-stick.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/4-crash-cymbal.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/5-open-hi-hat.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/6-closed-hi-hat.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/7-tambourine.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/8-hand-clap.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/9-claves.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/108.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/24.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/72.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/84.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/96.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/10-clarinet/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/10-clarinet/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/11-saxophone/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/11-saxophone/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/11-saxophone/84.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/12-flute/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/12-flute/72.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/13-wooden-flute/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/13-wooden-flute/72.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/14-bassoon/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/14-bassoon/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/14-bassoon/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/15-choir/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/15-choir/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/15-choir/72.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/16-vibraphone/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/16-vibraphone/72.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/17-music-box/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/18-steel-drum/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/19-marimba/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/2-electric-piano/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/20-synth-lead/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/21-synth-pad/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/3-organ/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/4-guitar/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/5-electric-guitar/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/6-bass/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/6-bass/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/7-pizzicato/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/8-cello/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/8-cello/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/8-cello/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/9-trombone/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/9-trombone/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/9-trombone/60.mp3 +0 -0
- package/src/extensions/scratch3_music/index.js +1333 -0
- package/src/extensions/scratch3_music/manifest.js +63 -0
- package/src/extensions/scratch3_pen/index.js +770 -0
- package/src/extensions/scratch3_speech2text/index.js +700 -0
- package/src/extensions/scratch3_text2speech/index.js +766 -0
- package/src/extensions/scratch3_translate/index.js +286 -0
- package/src/extensions/scratch3_video_sensing/debug.js +13 -0
- package/src/extensions/scratch3_video_sensing/index.js +594 -0
- package/src/extensions/scratch3_video_sensing/library.js +384 -0
- package/src/extensions/scratch3_video_sensing/math.js +76 -0
- package/src/extensions/scratch3_video_sensing/view.js +509 -0
- package/src/extensions/scratch3_wedo2/index.js +1616 -0
- package/src/import/load-costume.js +416 -0
- package/src/import/load-sound.js +116 -0
- package/src/index.js +10 -0
- package/src/io/ble.js +256 -0
- package/src/io/bt.js +202 -0
- package/src/io/clock.js +39 -0
- package/src/io/cloud.js +169 -0
- package/src/io/keyboard.js +146 -0
- package/src/io/mouse.js +145 -0
- package/src/io/mouseWheel.js +28 -0
- package/src/io/userData.js +24 -0
- package/src/io/video.js +210 -0
- package/src/playground/benchmark.css +83 -0
- package/src/playground/benchmark.js +753 -0
- package/src/playground/index.html +111 -0
- package/src/playground/suite.css +78 -0
- package/src/playground/suite.html +25 -0
- package/src/playground/suite.js +544 -0
- package/src/playground/video-sensing.html +18 -0
- package/src/playground/video-sensing.js +133 -0
- package/src/serialization/deserialize-assets.js +178 -0
- package/src/serialization/sb2.js +1295 -0
- package/src/serialization/sb2_specmap.js +1818 -0
- package/src/serialization/sb3.js +1320 -0
- package/src/serialization/serialize-assets.js +60 -0
- package/src/sprites/rendered-target.js +1115 -0
- package/src/sprites/sprite.js +177 -0
- package/src/util/base64-util.js +48 -0
- package/src/util/cast.js +218 -0
- package/src/util/clone.js +17 -0
- package/src/util/color.js +204 -0
- package/src/util/fetch-with-timeout.js +53 -0
- package/src/util/get-monitor-id.js +33 -0
- package/src/util/jsonrpc.js +114 -0
- package/src/util/log.js +4 -0
- package/src/util/math-util.js +122 -0
- package/src/util/maybe-format-message.js +18 -0
- package/src/util/new-block-ids.js +33 -0
- package/src/util/rateLimiter.js +73 -0
- package/src/util/scratch-link-websocket.js +133 -0
- package/src/util/string-util.js +94 -0
- package/src/util/task-queue.js +203 -0
- package/src/util/timer.js +103 -0
- package/src/util/uid.js +29 -0
- package/src/util/variable-util.js +48 -0
- package/src/util/xml-escape.js +33 -0
- package/src/virtual-machine.js +1586 -0
- package/webpack.config.js +134 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
const SharedDispatch = require('./shared-dispatch');
|
|
2
|
+
|
|
3
|
+
const log = require('../util/log');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* This class serves as the central broker for message dispatch. It expects to operate on the main thread / Window and
|
|
7
|
+
* it must be informed of any Worker threads which will participate in the messaging system. From any context in the
|
|
8
|
+
* messaging system, the dispatcher's "call" method can call any method on any "service" provided in any participating
|
|
9
|
+
* context. The dispatch system will forward function arguments and return values across worker boundaries as needed.
|
|
10
|
+
* @see {WorkerDispatch}
|
|
11
|
+
*/
|
|
12
|
+
class CentralDispatch extends SharedDispatch {
|
|
13
|
+
constructor () {
|
|
14
|
+
super();
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Map of channel name to worker or local service provider.
|
|
18
|
+
* If the entry is a Worker, the service is provided by an object on that worker.
|
|
19
|
+
* Otherwise, the service is provided locally and methods on the service will be called directly.
|
|
20
|
+
* @see {setService}
|
|
21
|
+
* @type {object.<Worker|object>}
|
|
22
|
+
*/
|
|
23
|
+
this.services = {};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The constructor we will use to recognize workers.
|
|
27
|
+
* @type {Function}
|
|
28
|
+
*/
|
|
29
|
+
this.workerClass = (typeof Worker === 'undefined' ? null : Worker);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* List of workers attached to this dispatcher.
|
|
33
|
+
* @type {Array}
|
|
34
|
+
*/
|
|
35
|
+
this.workers = [];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Synchronously call a particular method on a particular service provided locally.
|
|
40
|
+
* Calling this function on a remote service will fail.
|
|
41
|
+
* @param {string} service - the name of the service.
|
|
42
|
+
* @param {string} method - the name of the method.
|
|
43
|
+
* @param {*} [args] - the arguments to be copied to the method, if any.
|
|
44
|
+
* @returns {*} - the return value of the service method.
|
|
45
|
+
*/
|
|
46
|
+
callSync (service, method, ...args) {
|
|
47
|
+
const {provider, isRemote} = this._getServiceProvider(service);
|
|
48
|
+
if (provider) {
|
|
49
|
+
if (isRemote) {
|
|
50
|
+
throw new Error(`Cannot use 'callSync' on remote provider for service ${service}.`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// TODO: verify correct `this` after switching from apply to spread
|
|
54
|
+
// eslint-disable-next-line prefer-spread
|
|
55
|
+
return provider[method].apply(provider, args);
|
|
56
|
+
}
|
|
57
|
+
throw new Error(`Provider not found for service: ${service}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Synchronously set a local object as the global provider of the specified service.
|
|
62
|
+
* WARNING: Any method on the provider can be called from any worker within the dispatch system.
|
|
63
|
+
* @param {string} service - a globally unique string identifying this service. Examples: 'vm', 'gui', 'extension9'.
|
|
64
|
+
* @param {object} provider - a local object which provides this service.
|
|
65
|
+
*/
|
|
66
|
+
setServiceSync (service, provider) {
|
|
67
|
+
if (Object.prototype.hasOwnProperty.call(this.services, service)) {
|
|
68
|
+
log.warn(`Central dispatch replacing existing service provider for ${service}`);
|
|
69
|
+
}
|
|
70
|
+
this.services[service] = provider;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Set a local object as the global provider of the specified service.
|
|
75
|
+
* WARNING: Any method on the provider can be called from any worker within the dispatch system.
|
|
76
|
+
* @param {string} service - a globally unique string identifying this service. Examples: 'vm', 'gui', 'extension9'.
|
|
77
|
+
* @param {object} provider - a local object which provides this service.
|
|
78
|
+
* @returns {Promise} - a promise which will resolve once the service is registered.
|
|
79
|
+
*/
|
|
80
|
+
setService (service, provider) {
|
|
81
|
+
/** Return a promise for consistency with {@link WorkerDispatch#setService} */
|
|
82
|
+
try {
|
|
83
|
+
this.setServiceSync(service, provider);
|
|
84
|
+
return Promise.resolve();
|
|
85
|
+
} catch (e) {
|
|
86
|
+
return Promise.reject(e);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Add a worker to the message dispatch system. The worker must implement a compatible message dispatch framework.
|
|
92
|
+
* The dispatcher will immediately attempt to "handshake" with the worker.
|
|
93
|
+
* @param {Worker} worker - the worker to add into the dispatch system.
|
|
94
|
+
*/
|
|
95
|
+
addWorker (worker) {
|
|
96
|
+
if (this.workers.indexOf(worker) === -1) {
|
|
97
|
+
this.workers.push(worker);
|
|
98
|
+
worker.onmessage = this._onMessage.bind(this, worker);
|
|
99
|
+
this._remoteCall(worker, 'dispatch', 'handshake').catch(e => {
|
|
100
|
+
log.error(`Could not handshake with worker: ${JSON.stringify(e)}`);
|
|
101
|
+
});
|
|
102
|
+
} else {
|
|
103
|
+
log.warn('Central dispatch ignoring attempt to add duplicate worker');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Fetch the service provider object for a particular service name.
|
|
109
|
+
* @override
|
|
110
|
+
* @param {string} service - the name of the service to look up
|
|
111
|
+
* @returns {{provider:(object|Worker), isRemote:boolean}} - the means to contact the service, if found
|
|
112
|
+
* @protected
|
|
113
|
+
*/
|
|
114
|
+
_getServiceProvider (service) {
|
|
115
|
+
const provider = this.services[service];
|
|
116
|
+
return provider && {
|
|
117
|
+
provider,
|
|
118
|
+
isRemote: Boolean(this.workerClass && provider instanceof this.workerClass)
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Handle a call message sent to the dispatch service itself
|
|
124
|
+
* @override
|
|
125
|
+
* @param {Worker} worker - the worker which sent the message.
|
|
126
|
+
* @param {DispatchCallMessage} message - the message to be handled.
|
|
127
|
+
* @returns {Promise|undefined} - a promise for the results of this operation, if appropriate
|
|
128
|
+
* @protected
|
|
129
|
+
*/
|
|
130
|
+
_onDispatchMessage (worker, message) {
|
|
131
|
+
let promise;
|
|
132
|
+
switch (message.method) {
|
|
133
|
+
case 'setService':
|
|
134
|
+
promise = this.setService(message.args[0], worker);
|
|
135
|
+
break;
|
|
136
|
+
default:
|
|
137
|
+
log.error(`Central dispatch received message for unknown method: ${message.method}`);
|
|
138
|
+
}
|
|
139
|
+
return promise;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
module.exports = new CentralDispatch();
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
const log = require('../util/log');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {object} DispatchCallMessage - a message to the dispatch system representing a service method call
|
|
5
|
+
* @property {*} responseId - send a response message with this response ID. See {@link DispatchResponseMessage}
|
|
6
|
+
* @property {string} service - the name of the service to be called
|
|
7
|
+
* @property {string} method - the name of the method to be called
|
|
8
|
+
* @property {Array|undefined} args - the arguments to be passed to the method
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {object} DispatchResponseMessage - a message to the dispatch system representing the results of a call
|
|
13
|
+
* @property {*} responseId - a copy of the response ID from the call which generated this response
|
|
14
|
+
* @property {*|undefined} error - if this is truthy, then it contains results from a failed call (such as an exception)
|
|
15
|
+
* @property {*|undefined} result - if error is not truthy, then this contains the return value of the call (if any)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {DispatchCallMessage|DispatchResponseMessage} DispatchMessage
|
|
20
|
+
* Any message to the dispatch system.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The SharedDispatch class is responsible for dispatch features shared by
|
|
25
|
+
* {@link CentralDispatch} and {@link WorkerDispatch}.
|
|
26
|
+
*/
|
|
27
|
+
class SharedDispatch {
|
|
28
|
+
constructor () {
|
|
29
|
+
/**
|
|
30
|
+
* List of callback registrations for promises waiting for a response from a call to a service on another
|
|
31
|
+
* worker. A callback registration is an array of [resolve,reject] Promise functions.
|
|
32
|
+
* Calls to local services don't enter this list.
|
|
33
|
+
* @type {Array.<Function[]>}
|
|
34
|
+
*/
|
|
35
|
+
this.callbacks = [];
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The next response ID to be used.
|
|
39
|
+
* @type {int}
|
|
40
|
+
*/
|
|
41
|
+
this.nextResponseId = 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Call a particular method on a particular service, regardless of whether that service is provided locally or on
|
|
46
|
+
* a worker. If the service is provided by a worker, the `args` will be copied using the Structured Clone
|
|
47
|
+
* algorithm, except for any items which are also in the `transfer` list. Ownership of those items will be
|
|
48
|
+
* transferred to the worker, and they should not be used after this call.
|
|
49
|
+
* @example
|
|
50
|
+
* dispatcher.call('vm', 'setData', 'cat', 42);
|
|
51
|
+
* // this finds the worker for the 'vm' service, then on that worker calls:
|
|
52
|
+
* vm.setData('cat', 42);
|
|
53
|
+
* @param {string} service - the name of the service.
|
|
54
|
+
* @param {string} method - the name of the method.
|
|
55
|
+
* @param {*} [args] - the arguments to be copied to the method, if any.
|
|
56
|
+
* @returns {Promise} - a promise for the return value of the service method.
|
|
57
|
+
*/
|
|
58
|
+
call (service, method, ...args) {
|
|
59
|
+
return this.transferCall(service, method, null, ...args);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Call a particular method on a particular service, regardless of whether that service is provided locally or on
|
|
64
|
+
* a worker. If the service is provided by a worker, the `args` will be copied using the Structured Clone
|
|
65
|
+
* algorithm, except for any items which are also in the `transfer` list. Ownership of those items will be
|
|
66
|
+
* transferred to the worker, and they should not be used after this call.
|
|
67
|
+
* @example
|
|
68
|
+
* dispatcher.transferCall('vm', 'setData', [myArrayBuffer], 'cat', myArrayBuffer);
|
|
69
|
+
* // this finds the worker for the 'vm' service, transfers `myArrayBuffer` to it, then on that worker calls:
|
|
70
|
+
* vm.setData('cat', myArrayBuffer);
|
|
71
|
+
* @param {string} service - the name of the service.
|
|
72
|
+
* @param {string} method - the name of the method.
|
|
73
|
+
* @param {Array} [transfer] - objects to be transferred instead of copied. Must be present in `args` to be useful.
|
|
74
|
+
* @param {*} [args] - the arguments to be copied to the method, if any.
|
|
75
|
+
* @returns {Promise} - a promise for the return value of the service method.
|
|
76
|
+
*/
|
|
77
|
+
transferCall (service, method, transfer, ...args) {
|
|
78
|
+
try {
|
|
79
|
+
const {provider, isRemote} = this._getServiceProvider(service);
|
|
80
|
+
if (provider) {
|
|
81
|
+
if (isRemote) {
|
|
82
|
+
return this._remoteTransferCall(provider, service, method, transfer, ...args);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// TODO: verify correct `this` after switching from apply to spread
|
|
86
|
+
// eslint-disable-next-line prefer-spread
|
|
87
|
+
const result = provider[method].apply(provider, args);
|
|
88
|
+
return Promise.resolve(result);
|
|
89
|
+
}
|
|
90
|
+
return Promise.reject(new Error(`Service not found: ${service}`));
|
|
91
|
+
} catch (e) {
|
|
92
|
+
return Promise.reject(e);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Check if a particular service lives on another worker.
|
|
98
|
+
* @param {string} service - the service to check.
|
|
99
|
+
* @returns {boolean} - true if the service is remote (calls must cross a Worker boundary), false otherwise.
|
|
100
|
+
* @private
|
|
101
|
+
*/
|
|
102
|
+
_isRemoteService (service) {
|
|
103
|
+
return this._getServiceProvider(service).isRemote;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Like {@link call}, but force the call to be posted through a particular communication channel.
|
|
108
|
+
* @param {object} provider - send the call through this object's `postMessage` function.
|
|
109
|
+
* @param {string} service - the name of the service.
|
|
110
|
+
* @param {string} method - the name of the method.
|
|
111
|
+
* @param {*} [args] - the arguments to be copied to the method, if any.
|
|
112
|
+
* @returns {Promise} - a promise for the return value of the service method.
|
|
113
|
+
*/
|
|
114
|
+
_remoteCall (provider, service, method, ...args) {
|
|
115
|
+
return this._remoteTransferCall(provider, service, method, null, ...args);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Like {@link transferCall}, but force the call to be posted through a particular communication channel.
|
|
120
|
+
* @param {object} provider - send the call through this object's `postMessage` function.
|
|
121
|
+
* @param {string} service - the name of the service.
|
|
122
|
+
* @param {string} method - the name of the method.
|
|
123
|
+
* @param {Array} [transfer] - objects to be transferred instead of copied. Must be present in `args` to be useful.
|
|
124
|
+
* @param {*} [args] - the arguments to be copied to the method, if any.
|
|
125
|
+
* @returns {Promise} - a promise for the return value of the service method.
|
|
126
|
+
*/
|
|
127
|
+
_remoteTransferCall (provider, service, method, transfer, ...args) {
|
|
128
|
+
return new Promise((resolve, reject) => {
|
|
129
|
+
const responseId = this._storeCallbacks(resolve, reject);
|
|
130
|
+
|
|
131
|
+
/** @TODO: remove this hack! this is just here so we don't try to send `util` to a worker */
|
|
132
|
+
if ((args.length > 0) && (typeof args[args.length - 1].yield === 'function')) {
|
|
133
|
+
args.pop();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (transfer) {
|
|
137
|
+
provider.postMessage({service, method, responseId, args}, transfer);
|
|
138
|
+
} else {
|
|
139
|
+
provider.postMessage({service, method, responseId, args});
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Store callback functions pending a response message.
|
|
146
|
+
* @param {Function} resolve - function to call if the service method returns.
|
|
147
|
+
* @param {Function} reject - function to call if the service method throws.
|
|
148
|
+
* @returns {*} - a unique response ID for this set of callbacks. See {@link _deliverResponse}.
|
|
149
|
+
* @protected
|
|
150
|
+
*/
|
|
151
|
+
_storeCallbacks (resolve, reject) {
|
|
152
|
+
const responseId = this.nextResponseId++;
|
|
153
|
+
this.callbacks[responseId] = [resolve, reject];
|
|
154
|
+
return responseId;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Deliver call response from a worker. This should only be called as the result of a message from a worker.
|
|
159
|
+
* @param {int} responseId - the response ID of the callback set to call.
|
|
160
|
+
* @param {DispatchResponseMessage} message - the message containing the response value(s).
|
|
161
|
+
* @protected
|
|
162
|
+
*/
|
|
163
|
+
_deliverResponse (responseId, message) {
|
|
164
|
+
try {
|
|
165
|
+
const [resolve, reject] = this.callbacks[responseId];
|
|
166
|
+
delete this.callbacks[responseId];
|
|
167
|
+
if (message.error) {
|
|
168
|
+
reject(message.error);
|
|
169
|
+
} else {
|
|
170
|
+
resolve(message.result);
|
|
171
|
+
}
|
|
172
|
+
} catch (e) {
|
|
173
|
+
log.error(`Dispatch callback failed: ${JSON.stringify(e)}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Handle a message event received from a connected worker.
|
|
179
|
+
* @param {Worker} worker - the worker which sent the message, or the global object if running in a worker.
|
|
180
|
+
* @param {MessageEvent} event - the message event to be handled.
|
|
181
|
+
* @protected
|
|
182
|
+
*/
|
|
183
|
+
_onMessage (worker, event) {
|
|
184
|
+
/** @type {DispatchMessage} */
|
|
185
|
+
const message = event.data;
|
|
186
|
+
message.args = message.args || [];
|
|
187
|
+
let promise;
|
|
188
|
+
if (message.service) {
|
|
189
|
+
if (message.service === 'dispatch') {
|
|
190
|
+
promise = this._onDispatchMessage(worker, message);
|
|
191
|
+
} else {
|
|
192
|
+
promise = this.call(message.service, message.method, ...message.args);
|
|
193
|
+
}
|
|
194
|
+
} else if (typeof message.responseId === 'undefined') {
|
|
195
|
+
log.error(`Dispatch caught malformed message from a worker: ${JSON.stringify(event)}`);
|
|
196
|
+
} else {
|
|
197
|
+
this._deliverResponse(message.responseId, message);
|
|
198
|
+
}
|
|
199
|
+
if (promise) {
|
|
200
|
+
if (typeof message.responseId === 'undefined') {
|
|
201
|
+
log.error(`Dispatch message missing required response ID: ${JSON.stringify(event)}`);
|
|
202
|
+
} else {
|
|
203
|
+
promise.then(
|
|
204
|
+
result => worker.postMessage({responseId: message.responseId, result}),
|
|
205
|
+
error => worker.postMessage({responseId: message.responseId, error})
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Fetch the service provider object for a particular service name.
|
|
213
|
+
* @abstract
|
|
214
|
+
* @param {string} service - the name of the service to look up
|
|
215
|
+
* @returns {{provider:(object|Worker), isRemote:boolean}} - the means to contact the service, if found
|
|
216
|
+
* @protected
|
|
217
|
+
*/
|
|
218
|
+
_getServiceProvider (service) {
|
|
219
|
+
throw new Error(`Could not get provider for ${service}: _getServiceProvider not implemented`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Handle a call message sent to the dispatch service itself
|
|
224
|
+
* @abstract
|
|
225
|
+
* @param {Worker} worker - the worker which sent the message.
|
|
226
|
+
* @param {DispatchCallMessage} message - the message to be handled.
|
|
227
|
+
* @returns {Promise|undefined} - a promise for the results of this operation, if appropriate
|
|
228
|
+
* @private
|
|
229
|
+
*/
|
|
230
|
+
_onDispatchMessage (worker, message) {
|
|
231
|
+
throw new Error(`Unimplemented dispatch message handler cannot handle ${message.method} method`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
module.exports = SharedDispatch;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
const SharedDispatch = require('./shared-dispatch');
|
|
2
|
+
|
|
3
|
+
const log = require('../util/log');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* This class provides a Worker with the means to participate in the message dispatch system managed by CentralDispatch.
|
|
7
|
+
* From any context in the messaging system, the dispatcher's "call" method can call any method on any "service"
|
|
8
|
+
* provided in any participating context. The dispatch system will forward function arguments and return values across
|
|
9
|
+
* worker boundaries as needed.
|
|
10
|
+
* @see {CentralDispatch}
|
|
11
|
+
*/
|
|
12
|
+
class WorkerDispatch extends SharedDispatch {
|
|
13
|
+
constructor () {
|
|
14
|
+
super();
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* This promise will be resolved when we have successfully connected to central dispatch.
|
|
18
|
+
* @type {Promise}
|
|
19
|
+
* @see {waitForConnection}
|
|
20
|
+
* @private
|
|
21
|
+
*/
|
|
22
|
+
this._connectionPromise = new Promise(resolve => {
|
|
23
|
+
this._onConnect = resolve;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Map of service name to local service provider.
|
|
28
|
+
* If a service is not listed here, it is assumed to be provided by another context (another Worker or the main
|
|
29
|
+
* thread).
|
|
30
|
+
* @see {setService}
|
|
31
|
+
* @type {object}
|
|
32
|
+
*/
|
|
33
|
+
this.services = {};
|
|
34
|
+
|
|
35
|
+
this._onMessage = this._onMessage.bind(this, self);
|
|
36
|
+
if (typeof self !== 'undefined') {
|
|
37
|
+
self.onmessage = this._onMessage;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @returns {Promise} a promise which will resolve upon connection to central dispatch. If you need to make a call
|
|
43
|
+
* immediately on "startup" you can attach a 'then' to this promise.
|
|
44
|
+
* @example
|
|
45
|
+
* dispatch.waitForConnection.then(() => {
|
|
46
|
+
* dispatch.call('myService', 'hello');
|
|
47
|
+
* })
|
|
48
|
+
*/
|
|
49
|
+
get waitForConnection () {
|
|
50
|
+
return this._connectionPromise;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Set a local object as the global provider of the specified service.
|
|
55
|
+
* WARNING: Any method on the provider can be called from any worker within the dispatch system.
|
|
56
|
+
* @param {string} service - a globally unique string identifying this service. Examples: 'vm', 'gui', 'extension9'.
|
|
57
|
+
* @param {object} provider - a local object which provides this service.
|
|
58
|
+
* @returns {Promise} - a promise which will resolve once the service is registered.
|
|
59
|
+
*/
|
|
60
|
+
setService (service, provider) {
|
|
61
|
+
if (Object.prototype.hasOwnProperty.call(this.services, service)) {
|
|
62
|
+
log.warn(`Worker dispatch replacing existing service provider for ${service}`);
|
|
63
|
+
}
|
|
64
|
+
this.services[service] = provider;
|
|
65
|
+
return this.waitForConnection.then(() => this._remoteCall(self, 'dispatch', 'setService', service));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Fetch the service provider object for a particular service name.
|
|
70
|
+
* @override
|
|
71
|
+
* @param {string} service - the name of the service to look up
|
|
72
|
+
* @returns {{provider:(object|Worker), isRemote:boolean}} - the means to contact the service, if found
|
|
73
|
+
* @protected
|
|
74
|
+
*/
|
|
75
|
+
_getServiceProvider (service) {
|
|
76
|
+
// if we don't have a local service by this name, contact central dispatch by calling `postMessage` on self
|
|
77
|
+
const provider = this.services[service];
|
|
78
|
+
return {
|
|
79
|
+
provider: provider || self,
|
|
80
|
+
isRemote: !provider
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Handle a call message sent to the dispatch service itself
|
|
86
|
+
* @override
|
|
87
|
+
* @param {Worker} worker - the worker which sent the message.
|
|
88
|
+
* @param {DispatchCallMessage} message - the message to be handled.
|
|
89
|
+
* @returns {Promise|undefined} - a promise for the results of this operation, if appropriate
|
|
90
|
+
* @protected
|
|
91
|
+
*/
|
|
92
|
+
_onDispatchMessage (worker, message) {
|
|
93
|
+
let promise;
|
|
94
|
+
switch (message.method) {
|
|
95
|
+
case 'handshake':
|
|
96
|
+
promise = this._onConnect();
|
|
97
|
+
break;
|
|
98
|
+
case 'terminate':
|
|
99
|
+
// Don't close until next tick, after sending confirmation back
|
|
100
|
+
setTimeout(() => self.close(), 0);
|
|
101
|
+
promise = Promise.resolve();
|
|
102
|
+
break;
|
|
103
|
+
default:
|
|
104
|
+
log.error(`Worker dispatch received message for unknown method: ${message.method}`);
|
|
105
|
+
}
|
|
106
|
+
return promise;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
module.exports = new WorkerDispatch();
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
const mutationAdapter = require('./mutation-adapter');
|
|
2
|
+
const html = require('htmlparser2');
|
|
3
|
+
const uid = require('../util/uid');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Convert and an individual block DOM to the representation tree.
|
|
7
|
+
* Based on Blockly's `domToBlockHeadless_`.
|
|
8
|
+
* @param {Element} blockDOM DOM tree for an individual block.
|
|
9
|
+
* @param {object} blocks Collection of blocks to add to.
|
|
10
|
+
* @param {boolean} isTopBlock Whether blocks at this level are "top blocks."
|
|
11
|
+
* @param {?string} parent Parent block ID.
|
|
12
|
+
* @return {undefined}
|
|
13
|
+
*/
|
|
14
|
+
const domToBlock = function (blockDOM, blocks, isTopBlock, parent) {
|
|
15
|
+
if (!blockDOM.attribs.id) {
|
|
16
|
+
blockDOM.attribs.id = uid();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Block skeleton.
|
|
20
|
+
const block = {
|
|
21
|
+
id: blockDOM.attribs.id, // Block ID
|
|
22
|
+
opcode: blockDOM.attribs.type, // For execution, "event_whengreenflag".
|
|
23
|
+
inputs: {}, // Inputs to this block and the blocks they point to.
|
|
24
|
+
fields: {}, // Fields on this block and their values.
|
|
25
|
+
next: null, // Next block in the stack, if one exists.
|
|
26
|
+
topLevel: isTopBlock, // If this block starts a stack.
|
|
27
|
+
parent: parent, // Parent block ID, if available.
|
|
28
|
+
shadow: blockDOM.name === 'shadow', // If this represents a shadow/slot.
|
|
29
|
+
x: blockDOM.attribs.x, // X position of script, if top-level.
|
|
30
|
+
y: blockDOM.attribs.y // Y position of script, if top-level.
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Add the block to the representation tree.
|
|
34
|
+
blocks[block.id] = block;
|
|
35
|
+
|
|
36
|
+
// Process XML children and find enclosed blocks, fields, etc.
|
|
37
|
+
for (let i = 0; i < blockDOM.children.length; i++) {
|
|
38
|
+
const xmlChild = blockDOM.children[i];
|
|
39
|
+
// Enclosed blocks and shadows
|
|
40
|
+
let childBlockNode = null;
|
|
41
|
+
let childShadowNode = null;
|
|
42
|
+
for (let j = 0; j < xmlChild.children.length; j++) {
|
|
43
|
+
const grandChildNode = xmlChild.children[j];
|
|
44
|
+
if (!grandChildNode.name) {
|
|
45
|
+
// Non-XML tag node.
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const grandChildNodeName = grandChildNode.name.toLowerCase();
|
|
49
|
+
if (grandChildNodeName === 'block') {
|
|
50
|
+
childBlockNode = grandChildNode;
|
|
51
|
+
} else if (grandChildNodeName === 'shadow') {
|
|
52
|
+
childShadowNode = grandChildNode;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Use shadow block only if there's no real block node.
|
|
57
|
+
if (!childBlockNode && childShadowNode) {
|
|
58
|
+
childBlockNode = childShadowNode;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Not all Blockly-type blocks are handled here,
|
|
62
|
+
// as we won't be using all of them for Scratch.
|
|
63
|
+
switch (xmlChild.name.toLowerCase()) {
|
|
64
|
+
case 'field':
|
|
65
|
+
{
|
|
66
|
+
// Add the field to this block.
|
|
67
|
+
const fieldName = xmlChild.attribs.name;
|
|
68
|
+
// Add id in case it is a variable field
|
|
69
|
+
const fieldId = xmlChild.attribs.id;
|
|
70
|
+
let fieldData = '';
|
|
71
|
+
if (xmlChild.children.length > 0 && xmlChild.children[0].data) {
|
|
72
|
+
fieldData = xmlChild.children[0].data;
|
|
73
|
+
} else {
|
|
74
|
+
// If the child of the field with a data property
|
|
75
|
+
// doesn't exist, set the data to an empty string.
|
|
76
|
+
fieldData = '';
|
|
77
|
+
}
|
|
78
|
+
block.fields[fieldName] = {
|
|
79
|
+
name: fieldName,
|
|
80
|
+
id: fieldId,
|
|
81
|
+
value: fieldData
|
|
82
|
+
};
|
|
83
|
+
const fieldVarType = xmlChild.attribs.variabletype;
|
|
84
|
+
if (typeof fieldVarType === 'string') {
|
|
85
|
+
block.fields[fieldName].variableType = fieldVarType;
|
|
86
|
+
}
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
case 'comment':
|
|
90
|
+
{
|
|
91
|
+
block.comment = xmlChild.attribs.id;
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
case 'value':
|
|
95
|
+
case 'statement':
|
|
96
|
+
{
|
|
97
|
+
// Recursively generate block structure for input block.
|
|
98
|
+
domToBlock(childBlockNode, blocks, false, block.id);
|
|
99
|
+
if (childShadowNode && childBlockNode !== childShadowNode) {
|
|
100
|
+
// Also generate the shadow block.
|
|
101
|
+
domToBlock(childShadowNode, blocks, false, block.id);
|
|
102
|
+
}
|
|
103
|
+
// Link this block's input to the child block.
|
|
104
|
+
const inputName = xmlChild.attribs.name;
|
|
105
|
+
block.inputs[inputName] = {
|
|
106
|
+
name: inputName,
|
|
107
|
+
block: childBlockNode.attribs.id,
|
|
108
|
+
shadow: childShadowNode ? childShadowNode.attribs.id : null
|
|
109
|
+
};
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
case 'next':
|
|
113
|
+
{
|
|
114
|
+
if (!childBlockNode || !childBlockNode.attribs) {
|
|
115
|
+
// Invalid child block.
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
// Recursively generate block structure for next block.
|
|
119
|
+
domToBlock(childBlockNode, blocks, false, block.id);
|
|
120
|
+
// Link next block to this block.
|
|
121
|
+
block.next = childBlockNode.attribs.id;
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
case 'mutation':
|
|
125
|
+
{
|
|
126
|
+
block.mutation = mutationAdapter(xmlChild);
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Convert outer blocks DOM from a Blockly CREATE event
|
|
135
|
+
* to a usable form for the Scratch runtime.
|
|
136
|
+
* This structure is based on Blockly xml.js:`domToWorkspace` and `domToBlock`.
|
|
137
|
+
* @param {Element} blocksDOM DOM tree for this event.
|
|
138
|
+
* @return {Array.<object>} Usable list of blocks from this CREATE event.
|
|
139
|
+
*/
|
|
140
|
+
const domToBlocks = function (blocksDOM) {
|
|
141
|
+
// At this level, there could be multiple blocks adjacent in the DOM tree.
|
|
142
|
+
const blocks = {};
|
|
143
|
+
for (let i = 0; i < blocksDOM.length; i++) {
|
|
144
|
+
const block = blocksDOM[i];
|
|
145
|
+
if (!block.name || !block.attribs) {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const tagName = block.name.toLowerCase();
|
|
149
|
+
if (tagName === 'block' || tagName === 'shadow') {
|
|
150
|
+
domToBlock(block, blocks, true, null);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// Flatten blocks object into a list.
|
|
154
|
+
const blocksList = [];
|
|
155
|
+
for (const b in blocks) {
|
|
156
|
+
if (!Object.prototype.hasOwnProperty.call(blocks, b)) continue;
|
|
157
|
+
blocksList.push(blocks[b]);
|
|
158
|
+
}
|
|
159
|
+
return blocksList;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Adapter between block creation events and block representation which can be
|
|
164
|
+
* used by the Scratch runtime.
|
|
165
|
+
* @param {object} e `Blockly.events.create` or `Blockly.events.endDrag`
|
|
166
|
+
* @return {Array.<object>} List of blocks from this CREATE event.
|
|
167
|
+
*/
|
|
168
|
+
const adapter = function (e) {
|
|
169
|
+
// Validate input
|
|
170
|
+
if (typeof e !== 'object') return;
|
|
171
|
+
if (typeof e.xml !== 'object') return;
|
|
172
|
+
|
|
173
|
+
return domToBlocks(html.parseDOM(e.xml.outerHTML, {decodeEntities: true}));
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
module.exports = adapter;
|