@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.
Files changed (175) hide show
  1. package/.jsdoc.json +20 -0
  2. package/.nvmrc +1 -0
  3. package/CHANGELOG.md +7004 -0
  4. package/LICENSE +12 -0
  5. package/README.md +120 -0
  6. package/TRADEMARK +1 -0
  7. package/commitlint.config.js +4 -0
  8. package/docs/extensions.md +527 -0
  9. package/package.json +103 -0
  10. package/release.config.js +13 -0
  11. package/src/blocks/scratch3_control.js +206 -0
  12. package/src/blocks/scratch3_core_example.js +69 -0
  13. package/src/blocks/scratch3_data.js +257 -0
  14. package/src/blocks/scratch3_event.js +137 -0
  15. package/src/blocks/scratch3_looks.js +612 -0
  16. package/src/blocks/scratch3_motion.js +288 -0
  17. package/src/blocks/scratch3_operators.js +154 -0
  18. package/src/blocks/scratch3_procedures.js +79 -0
  19. package/src/blocks/scratch3_sensing.js +336 -0
  20. package/src/blocks/scratch3_sound.js +350 -0
  21. package/src/dispatch/central-dispatch.js +143 -0
  22. package/src/dispatch/shared-dispatch.js +235 -0
  23. package/src/dispatch/worker-dispatch.js +110 -0
  24. package/src/engine/adapter.js +176 -0
  25. package/src/engine/block-utility.js +242 -0
  26. package/src/engine/blocks-execute-cache.js +19 -0
  27. package/src/engine/blocks-runtime-cache.js +78 -0
  28. package/src/engine/blocks.js +1299 -0
  29. package/src/engine/comment.js +56 -0
  30. package/src/engine/execute.js +580 -0
  31. package/src/engine/monitor-record.js +23 -0
  32. package/src/engine/mutation-adapter.js +48 -0
  33. package/src/engine/profiler.js +390 -0
  34. package/src/engine/runtime.js +2686 -0
  35. package/src/engine/scratch-blocks-constants.js +27 -0
  36. package/src/engine/sequencer.js +361 -0
  37. package/src/engine/stage-layering.js +29 -0
  38. package/src/engine/target.js +799 -0
  39. package/src/engine/thread.js +404 -0
  40. package/src/engine/variable.js +70 -0
  41. package/src/extension-support/argument-type.js +47 -0
  42. package/src/extension-support/block-type.js +50 -0
  43. package/src/extension-support/define-messages.js +18 -0
  44. package/src/extension-support/extension-manager.js +440 -0
  45. package/src/extension-support/extension-metadata.js +64 -0
  46. package/src/extension-support/extension-worker.js +59 -0
  47. package/src/extension-support/reporter-scope.js +18 -0
  48. package/src/extension-support/target-type.js +17 -0
  49. package/src/extensions/scratch3_boost/index.js +2113 -0
  50. package/src/extensions/scratch3_ev3/index.js +1355 -0
  51. package/src/extensions/scratch3_gdx_for/index.js +981 -0
  52. package/src/extensions/scratch3_gdx_for/scratch-link-device-adapter.js +44 -0
  53. package/src/extensions/scratch3_makeymakey/index.js +396 -0
  54. package/src/extensions/scratch3_microbit/index.js +984 -0
  55. package/src/extensions/scratch3_music/assets/drums/1-snare.mp3 +0 -0
  56. package/src/extensions/scratch3_music/assets/drums/10-wood-block.mp3 +0 -0
  57. package/src/extensions/scratch3_music/assets/drums/11-cowbell.mp3 +0 -0
  58. package/src/extensions/scratch3_music/assets/drums/12-triangle.mp3 +0 -0
  59. package/src/extensions/scratch3_music/assets/drums/13-bongo.mp3 +0 -0
  60. package/src/extensions/scratch3_music/assets/drums/14-conga.mp3 +0 -0
  61. package/src/extensions/scratch3_music/assets/drums/15-cabasa.mp3 +0 -0
  62. package/src/extensions/scratch3_music/assets/drums/16-guiro.mp3 +0 -0
  63. package/src/extensions/scratch3_music/assets/drums/17-vibraslap.mp3 +0 -0
  64. package/src/extensions/scratch3_music/assets/drums/18-cuica.mp3 +0 -0
  65. package/src/extensions/scratch3_music/assets/drums/2-bass-drum.mp3 +0 -0
  66. package/src/extensions/scratch3_music/assets/drums/3-side-stick.mp3 +0 -0
  67. package/src/extensions/scratch3_music/assets/drums/4-crash-cymbal.mp3 +0 -0
  68. package/src/extensions/scratch3_music/assets/drums/5-open-hi-hat.mp3 +0 -0
  69. package/src/extensions/scratch3_music/assets/drums/6-closed-hi-hat.mp3 +0 -0
  70. package/src/extensions/scratch3_music/assets/drums/7-tambourine.mp3 +0 -0
  71. package/src/extensions/scratch3_music/assets/drums/8-hand-clap.mp3 +0 -0
  72. package/src/extensions/scratch3_music/assets/drums/9-claves.mp3 +0 -0
  73. package/src/extensions/scratch3_music/assets/instruments/1-piano/108.mp3 +0 -0
  74. package/src/extensions/scratch3_music/assets/instruments/1-piano/24.mp3 +0 -0
  75. package/src/extensions/scratch3_music/assets/instruments/1-piano/36.mp3 +0 -0
  76. package/src/extensions/scratch3_music/assets/instruments/1-piano/48.mp3 +0 -0
  77. package/src/extensions/scratch3_music/assets/instruments/1-piano/60.mp3 +0 -0
  78. package/src/extensions/scratch3_music/assets/instruments/1-piano/72.mp3 +0 -0
  79. package/src/extensions/scratch3_music/assets/instruments/1-piano/84.mp3 +0 -0
  80. package/src/extensions/scratch3_music/assets/instruments/1-piano/96.mp3 +0 -0
  81. package/src/extensions/scratch3_music/assets/instruments/10-clarinet/48.mp3 +0 -0
  82. package/src/extensions/scratch3_music/assets/instruments/10-clarinet/60.mp3 +0 -0
  83. package/src/extensions/scratch3_music/assets/instruments/11-saxophone/36.mp3 +0 -0
  84. package/src/extensions/scratch3_music/assets/instruments/11-saxophone/60.mp3 +0 -0
  85. package/src/extensions/scratch3_music/assets/instruments/11-saxophone/84.mp3 +0 -0
  86. package/src/extensions/scratch3_music/assets/instruments/12-flute/60.mp3 +0 -0
  87. package/src/extensions/scratch3_music/assets/instruments/12-flute/72.mp3 +0 -0
  88. package/src/extensions/scratch3_music/assets/instruments/13-wooden-flute/60.mp3 +0 -0
  89. package/src/extensions/scratch3_music/assets/instruments/13-wooden-flute/72.mp3 +0 -0
  90. package/src/extensions/scratch3_music/assets/instruments/14-bassoon/36.mp3 +0 -0
  91. package/src/extensions/scratch3_music/assets/instruments/14-bassoon/48.mp3 +0 -0
  92. package/src/extensions/scratch3_music/assets/instruments/14-bassoon/60.mp3 +0 -0
  93. package/src/extensions/scratch3_music/assets/instruments/15-choir/48.mp3 +0 -0
  94. package/src/extensions/scratch3_music/assets/instruments/15-choir/60.mp3 +0 -0
  95. package/src/extensions/scratch3_music/assets/instruments/15-choir/72.mp3 +0 -0
  96. package/src/extensions/scratch3_music/assets/instruments/16-vibraphone/60.mp3 +0 -0
  97. package/src/extensions/scratch3_music/assets/instruments/16-vibraphone/72.mp3 +0 -0
  98. package/src/extensions/scratch3_music/assets/instruments/17-music-box/60.mp3 +0 -0
  99. package/src/extensions/scratch3_music/assets/instruments/18-steel-drum/60.mp3 +0 -0
  100. package/src/extensions/scratch3_music/assets/instruments/19-marimba/60.mp3 +0 -0
  101. package/src/extensions/scratch3_music/assets/instruments/2-electric-piano/60.mp3 +0 -0
  102. package/src/extensions/scratch3_music/assets/instruments/20-synth-lead/60.mp3 +0 -0
  103. package/src/extensions/scratch3_music/assets/instruments/21-synth-pad/60.mp3 +0 -0
  104. package/src/extensions/scratch3_music/assets/instruments/3-organ/60.mp3 +0 -0
  105. package/src/extensions/scratch3_music/assets/instruments/4-guitar/60.mp3 +0 -0
  106. package/src/extensions/scratch3_music/assets/instruments/5-electric-guitar/60.mp3 +0 -0
  107. package/src/extensions/scratch3_music/assets/instruments/6-bass/36.mp3 +0 -0
  108. package/src/extensions/scratch3_music/assets/instruments/6-bass/48.mp3 +0 -0
  109. package/src/extensions/scratch3_music/assets/instruments/7-pizzicato/60.mp3 +0 -0
  110. package/src/extensions/scratch3_music/assets/instruments/8-cello/36.mp3 +0 -0
  111. package/src/extensions/scratch3_music/assets/instruments/8-cello/48.mp3 +0 -0
  112. package/src/extensions/scratch3_music/assets/instruments/8-cello/60.mp3 +0 -0
  113. package/src/extensions/scratch3_music/assets/instruments/9-trombone/36.mp3 +0 -0
  114. package/src/extensions/scratch3_music/assets/instruments/9-trombone/48.mp3 +0 -0
  115. package/src/extensions/scratch3_music/assets/instruments/9-trombone/60.mp3 +0 -0
  116. package/src/extensions/scratch3_music/index.js +1333 -0
  117. package/src/extensions/scratch3_music/manifest.js +63 -0
  118. package/src/extensions/scratch3_pen/index.js +770 -0
  119. package/src/extensions/scratch3_speech2text/index.js +700 -0
  120. package/src/extensions/scratch3_text2speech/index.js +766 -0
  121. package/src/extensions/scratch3_translate/index.js +286 -0
  122. package/src/extensions/scratch3_video_sensing/debug.js +13 -0
  123. package/src/extensions/scratch3_video_sensing/index.js +594 -0
  124. package/src/extensions/scratch3_video_sensing/library.js +384 -0
  125. package/src/extensions/scratch3_video_sensing/math.js +76 -0
  126. package/src/extensions/scratch3_video_sensing/view.js +509 -0
  127. package/src/extensions/scratch3_wedo2/index.js +1616 -0
  128. package/src/import/load-costume.js +416 -0
  129. package/src/import/load-sound.js +116 -0
  130. package/src/index.js +10 -0
  131. package/src/io/ble.js +256 -0
  132. package/src/io/bt.js +202 -0
  133. package/src/io/clock.js +39 -0
  134. package/src/io/cloud.js +169 -0
  135. package/src/io/keyboard.js +146 -0
  136. package/src/io/mouse.js +145 -0
  137. package/src/io/mouseWheel.js +28 -0
  138. package/src/io/userData.js +24 -0
  139. package/src/io/video.js +210 -0
  140. package/src/playground/benchmark.css +83 -0
  141. package/src/playground/benchmark.js +753 -0
  142. package/src/playground/index.html +111 -0
  143. package/src/playground/suite.css +78 -0
  144. package/src/playground/suite.html +25 -0
  145. package/src/playground/suite.js +544 -0
  146. package/src/playground/video-sensing.html +18 -0
  147. package/src/playground/video-sensing.js +133 -0
  148. package/src/serialization/deserialize-assets.js +178 -0
  149. package/src/serialization/sb2.js +1295 -0
  150. package/src/serialization/sb2_specmap.js +1818 -0
  151. package/src/serialization/sb3.js +1320 -0
  152. package/src/serialization/serialize-assets.js +60 -0
  153. package/src/sprites/rendered-target.js +1115 -0
  154. package/src/sprites/sprite.js +177 -0
  155. package/src/util/base64-util.js +48 -0
  156. package/src/util/cast.js +218 -0
  157. package/src/util/clone.js +17 -0
  158. package/src/util/color.js +204 -0
  159. package/src/util/fetch-with-timeout.js +53 -0
  160. package/src/util/get-monitor-id.js +33 -0
  161. package/src/util/jsonrpc.js +114 -0
  162. package/src/util/log.js +4 -0
  163. package/src/util/math-util.js +122 -0
  164. package/src/util/maybe-format-message.js +18 -0
  165. package/src/util/new-block-ids.js +33 -0
  166. package/src/util/rateLimiter.js +73 -0
  167. package/src/util/scratch-link-websocket.js +133 -0
  168. package/src/util/string-util.js +94 -0
  169. package/src/util/task-queue.js +203 -0
  170. package/src/util/timer.js +103 -0
  171. package/src/util/uid.js +29 -0
  172. package/src/util/variable-util.js +48 -0
  173. package/src/util/xml-escape.js +33 -0
  174. package/src/virtual-machine.js +1586 -0
  175. package/webpack.config.js +134 -0
@@ -0,0 +1,53 @@
1
+ /**
2
+ * @callback FetchFunction
3
+ * @param {RequestInfo|URL} input
4
+ * @param {RequestInit|undefined} [init]
5
+ * @returns {Promise<Response>}
6
+ */
7
+
8
+ /**
9
+ * @type {FetchFunction}
10
+ */
11
+ let myFetch = global.fetch;
12
+
13
+ /**
14
+ * Tell `fetchWithTimeout` to use a specific `fetch` function.
15
+ * By default, `fetchWithTimeout` will use the global `fetch` function.
16
+ * If there is no global `fetch`, then `fetchWithTimeout` will fail unless provided with an alternative.
17
+ * @param {FetchFunction} newFetch The new `fetch` function to use within fetchWithTimeout.
18
+ */
19
+ const setFetch = newFetch => {
20
+ myFetch = newFetch;
21
+ };
22
+
23
+ /**
24
+ * Fetch a remote resource like `fetch` does, but with a time limit.
25
+ * @param {Request|string} resource Remote resource to fetch.
26
+ * @param {?object} init An options object containing any custom settings that you want to apply to the request.
27
+ * @param {number} timeout The amount of time before the request is canceled, in milliseconds
28
+ * @returns {Promise<Response>} The response from the server.
29
+ */
30
+ const fetchWithTimeout = (resource, init, timeout) => {
31
+ let timeoutID = null;
32
+ // Not supported in Safari <11
33
+ const controller = window.AbortController ? new window.AbortController() : null;
34
+ const signal = controller ? controller.signal : null;
35
+ // The fetch call races a timer.
36
+ return Promise.race([
37
+ myFetch(resource, Object.assign({signal}, init)).then(response => {
38
+ clearTimeout(timeoutID);
39
+ return response;
40
+ }),
41
+ new Promise((resolve, reject) => {
42
+ timeoutID = setTimeout(() => {
43
+ if (controller) controller.abort();
44
+ reject(new Error(`Fetch timed out after ${timeout} ms`));
45
+ }, timeout);
46
+ })
47
+ ]);
48
+ };
49
+
50
+ module.exports = {
51
+ fetchWithTimeout,
52
+ setFetch
53
+ };
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Returns a string representing a unique id for a monitored block
3
+ * where a single reporter block can have more than one monitor
4
+ * (and therefore more than one monitor block) associated
5
+ * with it (e.g. when reporter blocks have inputs).
6
+ * @param {string} baseId The base id to use for the different monitor blocks
7
+ * @param {object} fields The monitor block's fields object.
8
+ */
9
+ // TODO this function should eventually be the single place where all monitor
10
+ // IDs are obtained given an opcode for the reporter block and the list of
11
+ // selected parameters.
12
+ const getMonitorIdForBlockWithArgs = function (id, fields) {
13
+ let fieldString = '';
14
+ for (const fieldKey in fields) {
15
+ let fieldValue = fields[fieldKey].value;
16
+ if (fieldKey === 'CURRENTMENU') {
17
+ // The 'sensing_current' block has field values in all caps.
18
+ // However, when importing from scratch 2.0, these
19
+ // could have gotten imported as lower case field values.
20
+ // Normalize the field value here so that we don't ever
21
+ // end up with a different monitor ID representing the same
22
+ // block configuration
23
+ // Note: we are not doing this for every block field that comes into
24
+ // this function so as not to make the faulty assumption that block
25
+ // field values coming in would be unique after being made lower case
26
+ fieldValue = fieldValue.toLowerCase();
27
+ }
28
+ fieldString += `_${fieldValue}`;
29
+ }
30
+ return `${id}${fieldString}`;
31
+ };
32
+
33
+ module.exports = getMonitorIdForBlockWithArgs;
@@ -0,0 +1,114 @@
1
+ class JSONRPC {
2
+ constructor () {
3
+ this._requestID = 0;
4
+ this._openRequests = {};
5
+ }
6
+
7
+ /**
8
+ * Make an RPC request and retrieve the result.
9
+ * @param {string} method - the remote method to call.
10
+ * @param {object} params - the parameters to pass to the remote method.
11
+ * @returns {Promise} - a promise for the result of the call.
12
+ */
13
+ sendRemoteRequest (method, params) {
14
+ const requestID = this._requestID++;
15
+
16
+ const promise = new Promise((resolve, reject) => {
17
+ this._openRequests[requestID] = {resolve, reject};
18
+ });
19
+
20
+ this._sendRequest(method, params, requestID);
21
+
22
+ return promise;
23
+ }
24
+
25
+ /**
26
+ * Make an RPC notification with no expectation of a result or callback.
27
+ * @param {string} method - the remote method to call.
28
+ * @param {object} params - the parameters to pass to the remote method.
29
+ */
30
+ sendRemoteNotification (method, params) {
31
+ this._sendRequest(method, params);
32
+ }
33
+
34
+ /**
35
+ * Handle an RPC request from remote, should return a result or Promise for result, if appropriate.
36
+ * @param {string} method - the method requested by the remote caller.
37
+ * @param {object} params - the parameters sent with the remote caller's request.
38
+ */
39
+ didReceiveCall (/* method , params */) {
40
+ throw new Error('Must override didReceiveCall');
41
+ }
42
+
43
+ _sendMessage (/* jsonMessageObject */) {
44
+ throw new Error('Must override _sendMessage');
45
+ }
46
+
47
+ _sendRequest (method, params, id) {
48
+ const request = {
49
+ jsonrpc: '2.0',
50
+ method,
51
+ params
52
+ };
53
+
54
+ if (id !== null) {
55
+ request.id = id;
56
+ }
57
+
58
+ this._sendMessage(request);
59
+ }
60
+
61
+ _handleMessage (json) {
62
+ if (json.jsonrpc !== '2.0') {
63
+ throw new Error(`Bad or missing JSON-RPC version in message: ${json}`);
64
+ }
65
+ if (Object.prototype.hasOwnProperty.call(json, 'method')) {
66
+ this._handleRequest(json);
67
+ } else {
68
+ this._handleResponse(json);
69
+ }
70
+ }
71
+
72
+ _sendResponse (id, result, error) {
73
+ const response = {
74
+ jsonrpc: '2.0',
75
+ id
76
+ };
77
+ if (error) {
78
+ response.error = error;
79
+ } else {
80
+ response.result = result || null;
81
+ }
82
+ this._sendMessage(response);
83
+ }
84
+
85
+ _handleResponse (json) {
86
+ const {result, error, id} = json;
87
+ const openRequest = this._openRequests[id];
88
+ delete this._openRequests[id];
89
+ if (openRequest) {
90
+ if (error) {
91
+ openRequest.reject(error);
92
+ } else {
93
+ openRequest.resolve(result);
94
+ }
95
+ }
96
+ }
97
+
98
+ _handleRequest (json) {
99
+ const {method, params, id} = json;
100
+ const rawResult = this.didReceiveCall(method, params);
101
+ if (id !== null && typeof id !== 'undefined') {
102
+ Promise.resolve(rawResult).then(
103
+ result => {
104
+ this._sendResponse(id, result);
105
+ },
106
+ error => {
107
+ this._sendResponse(id, null, error);
108
+ }
109
+ );
110
+ }
111
+ }
112
+ }
113
+
114
+ module.exports = JSONRPC;
@@ -0,0 +1,4 @@
1
+ const minilog = require('minilog');
2
+ minilog.enable();
3
+
4
+ module.exports = minilog('vm');
@@ -0,0 +1,122 @@
1
+ class MathUtil {
2
+ /**
3
+ * Convert a value from degrees to radians.
4
+ * @param {!number} deg Value in degrees.
5
+ * @return {!number} Equivalent value in radians.
6
+ */
7
+ static degToRad (deg) {
8
+ return deg * Math.PI / 180;
9
+ }
10
+
11
+ /**
12
+ * Convert a value from radians to degrees.
13
+ * @param {!number} rad Value in radians.
14
+ * @return {!number} Equivalent value in degrees.
15
+ */
16
+ static radToDeg (rad) {
17
+ return rad * 180 / Math.PI;
18
+ }
19
+
20
+ /**
21
+ * Clamp a number between two limits.
22
+ * If n < min, return min. If n > max, return max. Else, return n.
23
+ * @param {!number} n Number to clamp.
24
+ * @param {!number} min Minimum limit.
25
+ * @param {!number} max Maximum limit.
26
+ * @return {!number} Value of n clamped to min and max.
27
+ */
28
+ static clamp (n, min, max) {
29
+ return Math.min(Math.max(n, min), max);
30
+ }
31
+
32
+ /**
33
+ * Keep a number between two limits, wrapping "extra" into the range.
34
+ * e.g., wrapClamp(7, 1, 5) == 2
35
+ * wrapClamp(0, 1, 5) == 5
36
+ * wrapClamp(-11, -10, 6) == 6, etc.
37
+ * @param {!number} n Number to wrap.
38
+ * @param {!number} min Minimum limit.
39
+ * @param {!number} max Maximum limit.
40
+ * @return {!number} Value of n wrapped between min and max.
41
+ */
42
+ static wrapClamp (n, min, max) {
43
+ const range = (max - min) + 1;
44
+ return n - (Math.floor((n - min) / range) * range);
45
+ }
46
+
47
+
48
+ /**
49
+ * Convert a value from tan function in degrees.
50
+ * @param {!number} angle in degrees
51
+ * @return {!number} Correct tan value
52
+ */
53
+ static tan (angle) {
54
+ angle = angle % 360;
55
+ switch (angle) {
56
+ case -270:
57
+ case 90:
58
+ return Infinity;
59
+ case -90:
60
+ case 270:
61
+ return -Infinity;
62
+ default:
63
+ return parseFloat(Math.tan((Math.PI * angle) / 180).toFixed(10));
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Given an array of unique numbers,
69
+ * returns a reduced array such that each element of the reduced array
70
+ * represents the position of that element in a sorted version of the
71
+ * original array.
72
+ * E.g. [5, 19. 13, 1] => [1, 3, 2, 0]
73
+ * @param {Array<number>} elts The elements to sort and reduce
74
+ * @return {Array<number>} The array of reduced orderings
75
+ */
76
+ static reducedSortOrdering (elts) {
77
+ const sorted = elts.slice(0).sort((a, b) => a - b);
78
+ return elts.map(e => sorted.indexOf(e));
79
+ }
80
+
81
+ /**
82
+ * Return a random number given an inclusive range and a number in that
83
+ * range that should be excluded.
84
+ *
85
+ * For instance, (1, 5, 3) will only pick 1, 2, 4, or 5 (with equal
86
+ * probability)
87
+ *
88
+ * @param {number} lower - The lower bound (inlcusive)
89
+ * @param {number} upper - The upper bound (inclusive), such that lower <= upper
90
+ * @param {number} excluded - The number to exclude (MUST be in the range)
91
+ * @return {number} A random integer in the range [lower, upper] that is not "excluded"
92
+ */
93
+ static inclusiveRandIntWithout (lower, upper, excluded) {
94
+ // Note that subtraction is the number of items in the
95
+ // inclusive range [lower, upper] minus 1 already
96
+ // (e.g. in the set {3, 4, 5}, 5 - 3 = 2).
97
+ const possibleOptions = upper - lower;
98
+
99
+ const randInt = lower + Math.floor(Math.random() * possibleOptions);
100
+ if (randInt >= excluded) {
101
+ return randInt + 1;
102
+ }
103
+
104
+ return randInt;
105
+ }
106
+
107
+ /**
108
+ * Scales a number from one range to another.
109
+ * @param {number} i number to be scaled
110
+ * @param {number} iMin input range minimum
111
+ * @param {number} iMax input range maximum
112
+ * @param {number} oMin output range minimum
113
+ * @param {number} oMax output range maximum
114
+ * @return {number} scaled number
115
+ */
116
+ static scale (i, iMin, iMax, oMin, oMax) {
117
+ const p = (i - iMin) / (iMax - iMin);
118
+ return (p * (oMax - oMin)) + oMin;
119
+ }
120
+ }
121
+
122
+ module.exports = MathUtil;
@@ -0,0 +1,18 @@
1
+ const formatMessage = require('format-message');
2
+
3
+ /**
4
+ * Check if `maybeMessage` looks like a message object, and if so pass it to `formatMessage`.
5
+ * Otherwise, return `maybeMessage` as-is.
6
+ * @param {*} maybeMessage - something that might be a message descriptor object.
7
+ * @param {object} [args] - the arguments to pass to `formatMessage` if it gets called.
8
+ * @param {string} [locale] - the locale to pass to `formatMessage` if it gets called.
9
+ * @return {string|*} - the formatted message OR the original `maybeMessage` input.
10
+ */
11
+ const maybeFormatMessage = function (maybeMessage, args, locale) {
12
+ if (maybeMessage && maybeMessage.id && maybeMessage.default) {
13
+ return formatMessage(maybeMessage, args, locale);
14
+ }
15
+ return maybeMessage;
16
+ };
17
+
18
+ module.exports = maybeFormatMessage;
@@ -0,0 +1,33 @@
1
+ const uid = require('./uid');
2
+
3
+ /**
4
+ * Mutate the given blocks to have new IDs and update all internal ID references.
5
+ * Does not return anything to make it clear that the blocks are updated in-place.
6
+ * @param {array} blocks - blocks to be mutated.
7
+ */
8
+ module.exports = blocks => {
9
+ const oldToNew = {};
10
+
11
+ // First update all top-level IDs and create old-to-new mapping
12
+ for (let i = 0; i < blocks.length; i++) {
13
+ const newId = uid();
14
+ const oldId = blocks[i].id;
15
+ blocks[i].id = oldToNew[oldId] = newId;
16
+ }
17
+
18
+ // Then go back through and update inputs (block/shadow)
19
+ // and next/parent properties
20
+ for (let i = 0; i < blocks.length; i++) {
21
+ for (const key in blocks[i].inputs) {
22
+ const input = blocks[i].inputs[key];
23
+ input.block = oldToNew[input.block];
24
+ input.shadow = oldToNew[input.shadow];
25
+ }
26
+ if (blocks[i].parent) {
27
+ blocks[i].parent = oldToNew[blocks[i].parent];
28
+ }
29
+ if (blocks[i].next) {
30
+ blocks[i].next = oldToNew[blocks[i].next];
31
+ }
32
+ }
33
+ };
@@ -0,0 +1,73 @@
1
+ const Timer = require('../util/timer');
2
+
3
+ class RateLimiter {
4
+ /**
5
+ * A utility for limiting the rate of repetitive send operations, such as
6
+ * bluetooth messages being sent to hardware devices. It uses the token bucket
7
+ * strategy: a counter accumulates tokens at a steady rate, and each send costs
8
+ * a token. If no tokens remain, it's not okay to send.
9
+ * @param {number} maxRate the maximum number of sends allowed per second
10
+ * @constructor
11
+ */
12
+ constructor (maxRate) {
13
+ /**
14
+ * The maximum number of tokens.
15
+ * @type {number}
16
+ */
17
+ this._maxTokens = maxRate;
18
+
19
+ /**
20
+ * The interval in milliseconds for refilling one token. It is calculated
21
+ * so that the tokens will be filled to maximum in one second.
22
+ * @type {number}
23
+ */
24
+ this._refillInterval = 1000 / maxRate;
25
+
26
+ /**
27
+ * The current number of tokens in the bucket.
28
+ * @type {number}
29
+ */
30
+ this._count = this._maxTokens;
31
+
32
+ this._timer = new Timer();
33
+ this._timer.start();
34
+
35
+ /**
36
+ * The last time in milliseconds when the token count was updated.
37
+ * @type {number}
38
+ */
39
+ this._lastUpdateTime = this._timer.timeElapsed();
40
+ }
41
+
42
+ /**
43
+ * Check if it is okay to send a message, by updating the token count,
44
+ * taking a token and then checking if we are still under the rate limit.
45
+ * @return {boolean} true if we are under the rate limit
46
+ */
47
+ okayToSend () {
48
+ // Calculate the number of tokens to refill the bucket with, based on the
49
+ // amount of time since the last refill.
50
+ const now = this._timer.timeElapsed();
51
+ const timeSinceRefill = now - this._lastUpdateTime;
52
+ const refillCount = Math.floor(timeSinceRefill / this._refillInterval);
53
+
54
+ // If we're adding at least one token, reset _lastUpdateTime to now.
55
+ // Otherwise, don't reset it so that we can continue measuring time until
56
+ // the next refill.
57
+ if (refillCount > 0) {
58
+ this._lastUpdateTime = now;
59
+ }
60
+
61
+ // Refill the tokens up to the maximum
62
+ this._count = Math.min(this._maxTokens, this._count + refillCount);
63
+
64
+ // If we have at least one token, use one, and it's okay to send.
65
+ if (this._count > 0) {
66
+ this._count--;
67
+ return true;
68
+ }
69
+ return false;
70
+ }
71
+ }
72
+
73
+ module.exports = RateLimiter;
@@ -0,0 +1,133 @@
1
+ /**
2
+ * This class provides a ScratchLinkSocket implementation using WebSockets,
3
+ * attempting to connect with the locally installed Scratch-Link.
4
+ *
5
+ * To connect with ScratchLink without WebSockets, you must implement all of the
6
+ * public methods in this class.
7
+ * - open()
8
+ * - close()
9
+ * - setOn[Open|Close|Error]
10
+ * - setHandleMessage
11
+ * - sendMessage(msgObj)
12
+ * - isOpen()
13
+ */
14
+ class ScratchLinkWebSocket {
15
+ constructor (type) {
16
+ this._type = type;
17
+ this._onOpen = null;
18
+ this._onClose = null;
19
+ this._onError = null;
20
+ this._handleMessage = null;
21
+
22
+ this._ws = null;
23
+ }
24
+
25
+ open () {
26
+ if (!(this._onOpen && this._onClose && this._onError && this._handleMessage)) {
27
+ throw new Error('Must set open, close, message and error handlers before calling open on the socket');
28
+ }
29
+
30
+ let pathname;
31
+ switch (this._type) {
32
+ case 'BLE':
33
+ pathname = 'scratch/ble';
34
+ break;
35
+ case 'BT':
36
+ pathname = 'scratch/bt';
37
+ break;
38
+ default:
39
+ throw new Error(`Unknown ScratchLink socket Type: ${this._type}`);
40
+ }
41
+
42
+ // Try ws:// (the new way) and wss:// (the old way) simultaneously. If either connects, close the other. If we
43
+ // were to try one and fall back to the other on failure, that could mean a delay of 30 seconds or more for
44
+ // those who need the fallback.
45
+ // If both connections fail we should report only one error.
46
+
47
+ const setSocket = (socketToUse, socketToClose) => {
48
+ socketToClose.onopen = socketToClose.onerror = null;
49
+ socketToClose.close();
50
+
51
+ this._ws = socketToUse;
52
+ this._ws.onopen = this._onOpen;
53
+ this._ws.onclose = this._onClose;
54
+ this._ws.onerror = this._onError;
55
+ this._ws.onmessage = this._onMessage.bind(this);
56
+ };
57
+
58
+ const ws = new WebSocket(`ws://127.0.0.1:20111/${pathname}`);
59
+ const wss = new WebSocket(`wss://device-manager.scratch.mit.edu:20110/${pathname}`);
60
+
61
+ const connectTimeout = setTimeout(() => {
62
+ // neither socket succeeded before the timeout
63
+ setSocket(ws, wss);
64
+ this._ws.onerror(new Event('timeout'));
65
+ }, 15 * 1000);
66
+ ws.onopen = openEvent => {
67
+ clearTimeout(connectTimeout);
68
+ setSocket(ws, wss);
69
+ this._ws.onopen(openEvent);
70
+ };
71
+ wss.onopen = openEvent => {
72
+ clearTimeout(connectTimeout);
73
+ setSocket(wss, ws);
74
+ this._ws.onopen(openEvent);
75
+ };
76
+
77
+ let wsError;
78
+ let wssError;
79
+ const errorHandler = () => {
80
+ // if only one has received an error, we haven't overall failed yet
81
+ if (wsError && wssError) {
82
+ clearTimeout(connectTimeout);
83
+ setSocket(ws, wss);
84
+ this._ws.onerror(wsError);
85
+ }
86
+ };
87
+ ws.onerror = errorEvent => {
88
+ wsError = errorEvent;
89
+ errorHandler();
90
+ };
91
+ wss.onerror = errorEvent => {
92
+ wssError = errorEvent;
93
+ errorHandler();
94
+ };
95
+ }
96
+
97
+ close () {
98
+ this._ws.close();
99
+ this._ws = null;
100
+ }
101
+
102
+ sendMessage (message) {
103
+ const messageText = JSON.stringify(message);
104
+ this._ws.send(messageText);
105
+ }
106
+
107
+ setOnOpen (fn) {
108
+ this._onOpen = fn;
109
+ }
110
+
111
+ setOnClose (fn) {
112
+ this._onClose = fn;
113
+ }
114
+
115
+ setOnError (fn) {
116
+ this._onError = fn;
117
+ }
118
+
119
+ setHandleMessage (fn) {
120
+ this._handleMessage = fn;
121
+ }
122
+
123
+ isOpen () {
124
+ return this._ws && this._ws.readyState === this._ws.OPEN;
125
+ }
126
+
127
+ _onMessage (e) {
128
+ const json = JSON.parse(e.data);
129
+ this._handleMessage(json);
130
+ }
131
+ }
132
+
133
+ module.exports = ScratchLinkWebSocket;
@@ -0,0 +1,94 @@
1
+ const log = require('./log');
2
+
3
+ class StringUtil {
4
+ static withoutTrailingDigits (s) {
5
+ let i = s.length - 1;
6
+ while ((i >= 0) && ('0123456789'.indexOf(s.charAt(i)) > -1)) i--;
7
+ return s.slice(0, i + 1);
8
+ }
9
+
10
+ static unusedName (name, existingNames) {
11
+ if (existingNames.indexOf(name) < 0) return name;
12
+ name = StringUtil.withoutTrailingDigits(name);
13
+ let i = 2;
14
+ while (existingNames.indexOf(name + i) >= 0) i++;
15
+ return name + i;
16
+ }
17
+
18
+ /**
19
+ * Split a string on the first occurrence of a split character.
20
+ * @param {string} text - the string to split.
21
+ * @param {string} separator - split the text on this character.
22
+ * @returns {string[]} - the two parts of the split string, or [text, null] if no split character found.
23
+ * @example
24
+ * // returns ['foo', 'tar.gz']
25
+ * splitFirst('foo.tar.gz', '.');
26
+ * @example
27
+ * // returns ['foo', null]
28
+ * splitFirst('foo', '.');
29
+ * @example
30
+ * // returns ['foo', '']
31
+ * splitFirst('foo.', '.');
32
+ */
33
+ static splitFirst (text, separator) {
34
+ const index = text.indexOf(separator);
35
+ if (index >= 0) {
36
+ return [text.substring(0, index), text.substring(index + 1)];
37
+ }
38
+ return [text, null];
39
+
40
+ }
41
+
42
+ /**
43
+ * A customized version of JSON.stringify that sets Infinity/NaN to 0,
44
+ * instead of the default (null).
45
+ * Needed because null is not of type number, but Infinity/NaN are, which
46
+ * can lead to serialization producing JSON that isn't valid based on the parser schema.
47
+ * It is also consistent with the behavior of saving 2.0 projects.
48
+ * This is only needed when stringifying an object for saving.
49
+ *
50
+ * @param {!object} obj - The object to serialize
51
+ * @return {!string} The JSON.stringified string with Infinity/NaN replaced with 0
52
+ */
53
+ static stringify (obj) {
54
+ return JSON.stringify(obj, (_key, value) => {
55
+ if (typeof value === 'number' &&
56
+ (value === Infinity || value === -Infinity || isNaN(value))){
57
+ return 0;
58
+ }
59
+ return value;
60
+ });
61
+ }
62
+ /**
63
+ * A function to replace unsafe characters (not allowed in XML) with safe ones. This is used
64
+ * in cases where we're replacing non-user facing strings (e.g. variable IDs).
65
+ * When replacing user facing strings, the xmlEscape utility function should be used
66
+ * instead so that the user facing string does not change how it displays.
67
+ * @param {!string | !Array.<string>} unsafe Unsafe string possibly containing unicode control characters.
68
+ * In some cases this argument may be an array (e.g. hacked inputs from 2.0)
69
+ * @return {string} String with control characters replaced.
70
+ */
71
+ static replaceUnsafeChars (unsafe) {
72
+ if (typeof unsafe !== 'string') {
73
+ if (Array.isArray(unsafe)) {
74
+ // This happens when we have hacked blocks from 2.0
75
+ // See #1030
76
+ unsafe = String(unsafe);
77
+ } else {
78
+ log.error('Unexpected input recieved in replaceUnsafeChars');
79
+ return unsafe;
80
+ }
81
+ }
82
+ return unsafe.replace(/[<>&'"]/g, c => {
83
+ switch (c) {
84
+ case '<': return 'lt';
85
+ case '>': return 'gt';
86
+ case '&': return 'amp';
87
+ case '\'': return 'apos';
88
+ case '"': return 'quot';
89
+ }
90
+ });
91
+ }
92
+ }
93
+
94
+ module.exports = StringUtil;