Remove immutable.js (#329)
Why: * We use immutable.js 3.8.2 which is ancient and not getting updates * immutable.js adds 55KB to the bundle, of which we use almost nothing * immutable.js's treatment of 0 and -0 is nonsensical and would require weird workarounds like https://github.com/TurboWarp/scratch-vm/pull/320 * We don't benefit from the whole immutability thing What we're doing instead: * We have our own MonitorState and MonitorRecord classes * We track dirty-ness at modification time instead of equality checking every tick * scratch-gui and packager can continue to think we use immutable.js without causing issues in either direction * our objects have enough stubs that immutable.js-expecting consumers are ok * when receiving an object, we check if it looks like an immutable.js one and ask for the plain JS version so that works okay too
This commit is contained in:
1 parent
553a2a434d
commit
6abc33d787
18 files changed
+552
-116
No files matched your search
Generated
-9
@@ -21,7 +21,6 @@
|
||||
"diff-match-patch": "1.0.4",
|
||||
"format-message": "6.2.1",
|
||||
"htmlparser2": "3.10.0",
|
||||
"immutable": "3.8.2",
|
||||
"scratch-parser": "github:TurboWarp/scratch-parser#master",
|
||||
"scratch-sb1-converter": "0.2.7",
|
||||
"scratch-translate-extension-languages": "^1.0.7",
|
||||
@@ -8999,14 +8998,6 @@
|
||||
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="
|
||||
},
|
||||
"node_modules/immutable": {
|
||||
"version": "3.8.2",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz",
|
||||
"integrity": "sha512-15gZoQ38eYjEjxkorfbcgBKBL6R7T459OuK+CpcWt7O3KF4uPCx2tD0uFETlUDIyo+1789crbMhTvQBSR5yBMg==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/import-fresh": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
|
||||
|
||||
@@ -46,7 +46,6 @@
|
||||
"diff-match-patch": "1.0.4",
|
||||
"format-message": "6.2.1",
|
||||
"htmlparser2": "3.10.0",
|
||||
"immutable": "3.8.2",
|
||||
"scratch-parser": "github:TurboWarp/scratch-parser#master",
|
||||
"scratch-sb1-converter": "0.2.7",
|
||||
"scratch-translate-extension-languages": "^1.0.7",
|
||||
|
||||
@@ -99,7 +99,7 @@ class Scratch3DataBlocks {
|
||||
// Return original list value if up-to-date, which doesn't trigger monitor update.
|
||||
if (list._monitorUpToDate) return list.value;
|
||||
// If value changed, reset the flag and return a copy to trigger monitor update.
|
||||
// Because monitors use Immutable data structures, only new objects trigger updates.
|
||||
// MonitorState only detects updates when the object changes.
|
||||
list._monitorUpToDate = true;
|
||||
return list.value.slice();
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ const mutationAdapter = require('./mutation-adapter');
|
||||
const xmlEscape = require('../util/xml-escape');
|
||||
const MonitorRecord = require('./monitor-record');
|
||||
const Clone = require('../util/clone');
|
||||
const {Map} = require('immutable');
|
||||
const BlocksExecuteCache = require('./blocks-execute-cache');
|
||||
const BlocksRuntimeCache = require('./blocks-runtime-cache');
|
||||
const log = require('../util/log');
|
||||
@@ -711,10 +710,10 @@ class Blocks {
|
||||
|
||||
const flyoutBlock = block.shadow && block.parent ? this._blocks[block.parent] : block;
|
||||
if (flyoutBlock.isMonitored) {
|
||||
this.runtime.requestUpdateMonitor(Map({
|
||||
this.runtime.requestUpdateMonitor({
|
||||
id: flyoutBlock.id,
|
||||
params: this._getBlockParams(flyoutBlock)
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -773,7 +772,7 @@ class Blocks {
|
||||
} else if (!wasMonitored && block.isMonitored) {
|
||||
// Tries to show the monitor for specified block. If it doesn't exist, add the monitor.
|
||||
if (!this.runtime.requestShowMonitor(block.id)) {
|
||||
this.runtime.requestAddMonitor(MonitorRecord({
|
||||
this.runtime.requestAddMonitor(new MonitorRecord({
|
||||
id: block.id,
|
||||
targetId: block.targetId,
|
||||
spriteName: block.targetId ? this.runtime.getTargetById(block.targetId).getName() : null,
|
||||
|
||||
@@ -2,7 +2,6 @@ const BlockUtility = require('./block-utility');
|
||||
const BlocksExecuteCache = require('./blocks-execute-cache');
|
||||
const log = require('../util/log');
|
||||
const Thread = require('./thread');
|
||||
const {Map} = require('immutable');
|
||||
const cast = require('../util/cast');
|
||||
|
||||
/**
|
||||
@@ -100,11 +99,11 @@ const handleReport = function (resolvedValue, sequencer, thread, blockCached, la
|
||||
// Target no longer exists
|
||||
return;
|
||||
}
|
||||
sequencer.runtime.requestUpdateMonitor(Map({
|
||||
sequencer.runtime.requestUpdateMonitor({
|
||||
id: currentBlockId,
|
||||
spriteName: targetId ? sequencer.runtime.getTargetById(targetId).getName() : null,
|
||||
value: resolvedValue
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
||||
// Finished any yields.
|
||||
|
||||
+195
-20
@@ -1,23 +1,198 @@
|
||||
const {Record} = require('immutable');
|
||||
/**
|
||||
* @param {unknown} obj
|
||||
* @returns {boolean}
|
||||
*/
|
||||
const defined = obj => typeof obj !== 'undefined' && obj !== null;
|
||||
|
||||
const MonitorRecord = Record({
|
||||
id: null, // Block Id
|
||||
/** Present only if the monitor is sprite-specific, such as x position */
|
||||
spriteName: null,
|
||||
/** Present only if the monitor is sprite-specific, such as x position */
|
||||
targetId: null,
|
||||
opcode: null,
|
||||
value: null,
|
||||
params: null,
|
||||
mode: 'default',
|
||||
sliderMin: 0,
|
||||
sliderMax: 100,
|
||||
isDiscrete: true,
|
||||
x: null, // (x: null, y: null) Indicates that the monitor should be auto-positioned
|
||||
y: null,
|
||||
width: 0,
|
||||
height: 0,
|
||||
visible: true
|
||||
});
|
||||
/**
|
||||
* @typedef JSDelta Delta object using regular JS object.
|
||||
* @property {string|null} [id]
|
||||
* @property {string|null} [spriteName]
|
||||
* @property {string|null} [targetId]
|
||||
* @property {string|null} [opcode]
|
||||
* @property {unknown} [value]
|
||||
* @property {unknown} [params]
|
||||
* @property {string|null} [mode]
|
||||
* @property {number|null} [sliderMin]
|
||||
* @property {number|null} [sliderMax]
|
||||
* @property {boolean|null} [isDiscrete]
|
||||
* @property {number|null} [x]
|
||||
* @property {number|null} [y]
|
||||
* @property {number|null} [width]
|
||||
* @property {number|null} [height]
|
||||
* @property {boolean|null} [visible]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef ImmutableJSDelta Delta object that is an immutable.js Map/OrderedMap.
|
||||
* @property {() => JSDelta} toJS
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {JSDelta|ImmutableJSDelta} ExternalDelta Delta object that might be JS or immutable.js.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @implements {JSDelta}
|
||||
*/
|
||||
class MonitorRecord {
|
||||
/**
|
||||
* @param {JSDelta} delta
|
||||
*/
|
||||
constructor (delta) {
|
||||
/**
|
||||
* Block ID
|
||||
*/
|
||||
this.id = delta.id ?? null;
|
||||
/**
|
||||
* Present only if the monitor is sprite-specific, such as x position
|
||||
*/
|
||||
this.spriteName = delta.spriteName ?? null;
|
||||
/**
|
||||
* Present only if the monitor is sprite-specific, such as x position
|
||||
*/
|
||||
this.targetId = delta.targetId ?? null;
|
||||
this.opcode = delta.opcode ?? null;
|
||||
this.value = delta.value ?? null;
|
||||
this.params = delta.params ?? null;
|
||||
this.mode = delta.mode ?? 'default';
|
||||
this.sliderMin = delta.sliderMin ?? 0;
|
||||
this.sliderMax = delta.sliderMax ?? 100;
|
||||
this.isDiscrete = delta.isDiscrete ?? true;
|
||||
/**
|
||||
* (x: null, y: null) Indicates that the monitor should be auto-positioned
|
||||
*/
|
||||
this.x = delta.x ?? null;
|
||||
/**
|
||||
* (x: null, y: null) Indicates that the monitor should be auto-positioned
|
||||
*/
|
||||
this.y = delta.y ?? null;
|
||||
this.width = delta.width ?? 0;
|
||||
this.height = delta.height ?? 0;
|
||||
this.visible = delta.visible ?? true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exists for compatibility with code expecting an immutable.js Map
|
||||
* @param {string} property
|
||||
*/
|
||||
get (property) {
|
||||
switch (property) {
|
||||
case 'id': return this.id;
|
||||
case 'spriteName': return this.spriteName;
|
||||
case 'targetId': return this.targetId;
|
||||
case 'opcode': return this.opcode;
|
||||
case 'value': return this.value;
|
||||
case 'params': return this.params;
|
||||
case 'mode': return this.mode;
|
||||
case 'sliderMin': return this.sliderMin;
|
||||
case 'sliderMax': return this.sliderMax;
|
||||
case 'isDiscrete': return this.isDiscrete;
|
||||
case 'x': return this.x;
|
||||
case 'y': return this.y;
|
||||
case 'width': return this.width;
|
||||
case 'height': return this.height;
|
||||
case 'visible': return this.visible;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {JSDelta} delta
|
||||
* @returns {boolean} true if modified
|
||||
*/
|
||||
merge (delta) {
|
||||
let didChange = false;
|
||||
|
||||
if (defined(delta.id) && !Object.is(this.id, delta.id)) {
|
||||
this.id = delta.id;
|
||||
didChange = true;
|
||||
}
|
||||
|
||||
if (defined(delta.spriteName) && !Object.is(this.spriteName, delta.spriteName)) {
|
||||
this.spriteName = delta.spriteName;
|
||||
didChange = true;
|
||||
}
|
||||
|
||||
if (defined(delta.targetId) && !Object.is(this.targetId, delta.targetId)) {
|
||||
this.targetId = delta.targetId;
|
||||
didChange = true;
|
||||
}
|
||||
|
||||
if (defined(delta.opcode) && !Object.is(this.opcode, delta.opcode)) {
|
||||
this.opcode = delta.opcode;
|
||||
didChange = true;
|
||||
}
|
||||
|
||||
if (defined(delta.value) && !Object.is(this.value, delta.value)) {
|
||||
this.value = delta.value;
|
||||
didChange = true;
|
||||
}
|
||||
|
||||
if (defined(delta.params) && !Object.is(this.params, delta.params)) {
|
||||
this.params = delta.params;
|
||||
didChange = true;
|
||||
}
|
||||
|
||||
if (defined(delta.mode) && !Object.is(this.mode, delta.mode)) {
|
||||
this.mode = delta.mode;
|
||||
didChange = true;
|
||||
}
|
||||
|
||||
if (defined(delta.sliderMin) && !Object.is(this.sliderMin, delta.sliderMin)) {
|
||||
this.sliderMin = delta.sliderMin;
|
||||
didChange = true;
|
||||
}
|
||||
|
||||
if (defined(delta.sliderMax) && !Object.is(this.sliderMax, delta.sliderMax)) {
|
||||
this.sliderMax = delta.sliderMax;
|
||||
didChange = true;
|
||||
}
|
||||
|
||||
if (defined(delta.isDiscrete) && !Object.is(this.isDiscrete, delta.isDiscrete)) {
|
||||
this.isDiscrete = delta.isDiscrete;
|
||||
didChange = true;
|
||||
}
|
||||
|
||||
if (defined(delta.x) && !Object.is(this.x, delta.x)) {
|
||||
this.x = delta.x;
|
||||
didChange = true;
|
||||
}
|
||||
|
||||
if (defined(delta.y) && !Object.is(this.y, delta.y)) {
|
||||
this.y = delta.y;
|
||||
didChange = true;
|
||||
}
|
||||
|
||||
if (defined(delta.width) && !Object.is(this.width, delta.width)) {
|
||||
this.width = delta.width;
|
||||
didChange = true;
|
||||
}
|
||||
|
||||
if (defined(delta.height) && !Object.is(this.height, delta.height)) {
|
||||
this.height = delta.height;
|
||||
didChange = true;
|
||||
}
|
||||
|
||||
if (defined(delta.visible) && !Object.is(this.visible, delta.visible)) {
|
||||
this.visible = delta.visible;
|
||||
didChange = true;
|
||||
}
|
||||
|
||||
return didChange;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For compatibility, converts an immutable.js delta received from consumer to a plain JS delta for internal use.
|
||||
* @param {ExternalDelta} obj
|
||||
* @returns {JSDelta}
|
||||
*/
|
||||
MonitorRecord.externalDeltaToJS = obj => {
|
||||
if (typeof obj.toJS === 'function') {
|
||||
return obj.toJS();
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
|
||||
module.exports = MonitorRecord;
|
||||
+34
-45
@@ -1,5 +1,4 @@
|
||||
const EventEmitter = require('events');
|
||||
const {OrderedMap} = require('immutable');
|
||||
const ExtendedJSON = require('@turbowarp/json');
|
||||
const uuid = require('uuid');
|
||||
|
||||
@@ -24,6 +23,7 @@ const FontManager = require('./tw-font-manager');
|
||||
const fetchWithTimeout = require('../util/fetch-with-timeout');
|
||||
const platform = require('./tw-platform.js');
|
||||
const safeStringify = require('../util/tw-safe-stringify.js');
|
||||
const MonitorState = require('./tw-monitor-state.js');
|
||||
|
||||
// Virtual I/O devices.
|
||||
const Clock = require('../io/clock');
|
||||
@@ -51,6 +51,7 @@ const defaultBlockPackages = {
|
||||
|
||||
const interpolate = require('./tw-interpolate');
|
||||
const FrameLoop = require('./tw-frame-loop');
|
||||
const MonitorRecord = require('./monitor-record.js');
|
||||
|
||||
const defaultExtensionColors = ['#0FBD8C', '#0DA57A', '#0B8E69'];
|
||||
|
||||
@@ -320,14 +321,9 @@ class Runtime extends EventEmitter {
|
||||
this.monitorBlockInfo = {};
|
||||
|
||||
/**
|
||||
* Ordered map of all monitors, which are MonitorReporter objects.
|
||||
* Ordered map of all monitors, which are MonitorRecord objects.
|
||||
*/
|
||||
this._monitorState = OrderedMap({});
|
||||
|
||||
/**
|
||||
* Monitor state from last tick
|
||||
*/
|
||||
this._prevMonitorState = OrderedMap({});
|
||||
this._monitorState = new MonitorState();
|
||||
|
||||
/**
|
||||
* Whether the project is in "turbo mode."
|
||||
@@ -2285,10 +2281,9 @@ class Runtime extends EventEmitter {
|
||||
this.targets.map(this.disposeTarget, this);
|
||||
this.extensionStorage = {};
|
||||
// tw: explicitly emit a MONITORS_UPDATE instead of relying on implicit behavior of _step()
|
||||
const emptyMonitorState = OrderedMap({});
|
||||
if (!emptyMonitorState.equals(this._monitorState)) {
|
||||
this._monitorState = emptyMonitorState;
|
||||
this.emit(Runtime.MONITORS_UPDATE, this._monitorState);
|
||||
if (!this._monitorState.empty()) {
|
||||
this._monitorState = new MonitorState();
|
||||
this.emit(Runtime.MONITORS_UPDATE, this._monitorState.shallowClone());
|
||||
}
|
||||
this.emit(Runtime.RUNTIME_DISPOSED);
|
||||
this.ioDevices.clock.resetProjectTimer();
|
||||
@@ -2578,9 +2573,9 @@ class Runtime extends EventEmitter {
|
||||
this._refreshTargets = false;
|
||||
}
|
||||
|
||||
if (!this._prevMonitorState.equals(this._monitorState)) {
|
||||
this.emit(Runtime.MONITORS_UPDATE, this._monitorState);
|
||||
this._prevMonitorState = this._monitorState;
|
||||
if (this._monitorState.dirty) {
|
||||
this.emit(Runtime.MONITORS_UPDATE, this._monitorState.shallowClone());
|
||||
this._monitorState.dirty = false;
|
||||
}
|
||||
|
||||
if (this.profiler !== null) {
|
||||
@@ -2707,12 +2702,13 @@ class Runtime extends EventEmitter {
|
||||
const offsetX = deltaX / 2;
|
||||
const offsetY = deltaY / 2;
|
||||
for (const monitor of this._monitorState.valueSeq()) {
|
||||
const newMonitor = monitor
|
||||
.set('x', monitor.get('x') + offsetX)
|
||||
.set('y', monitor.get('y') + offsetY);
|
||||
this.requestUpdateMonitor(newMonitor);
|
||||
this.requestUpdateMonitor({
|
||||
id: monitor.id,
|
||||
x: monitor.get('x') + offsetX,
|
||||
y: monitor.get('y') + offsetY
|
||||
});
|
||||
}
|
||||
this.emit(Runtime.MONITORS_UPDATE, this._monitorState);
|
||||
this.emit(Runtime.MONITORS_UPDATE, this._monitorState.shallowClone());
|
||||
}
|
||||
|
||||
this.stageWidth = width;
|
||||
@@ -3095,34 +3091,27 @@ class Runtime extends EventEmitter {
|
||||
/**
|
||||
* Add a monitor to the state. If the monitor already exists in the state,
|
||||
* updates those properties that are defined in the given monitor record.
|
||||
* @param {!MonitorRecord} monitor Monitor to add.
|
||||
* @param {import('./monitor-record.js')} monitor Monitor to add.
|
||||
*/
|
||||
requestAddMonitor (monitor) {
|
||||
const id = monitor.get('id');
|
||||
if (!this.requestUpdateMonitor(monitor)) { // update monitor if it exists in the state
|
||||
// if the monitor did not exist in the state, add it
|
||||
this._monitorState = this._monitorState.set(id, monitor);
|
||||
this._monitorState.set(monitor.id, monitor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a monitor in the state and report success/failure of update.
|
||||
* @param {!Map} monitor Monitor values to update. Values on the monitor with overwrite
|
||||
* values on the old monitor with the same ID. If a value isn't defined on the new monitor,
|
||||
* @param {import('./monitor-record.js').ExternalDelta} delta Monitor values to update. Values on the monitor will
|
||||
* overwrite values on the old monitor with the same ID. If a value isn't defined on the new monitor,
|
||||
* the old monitor will keep its old value.
|
||||
* @return {boolean} true if monitor exists in the state and was updated, false if it did not exist.
|
||||
*/
|
||||
requestUpdateMonitor (monitor) {
|
||||
const id = monitor.get('id');
|
||||
requestUpdateMonitor (delta) {
|
||||
delta = MonitorRecord.externalDeltaToJS(delta);
|
||||
const id = delta.id;
|
||||
if (this._monitorState.has(id)) {
|
||||
this._monitorState =
|
||||
// Use mergeWith here to prevent undefined values from overwriting existing ones
|
||||
this._monitorState.set(id, this._monitorState.get(id).mergeWith((prev, next) => {
|
||||
if (typeof next === 'undefined' || next === null) {
|
||||
return prev;
|
||||
}
|
||||
return next;
|
||||
}, monitor));
|
||||
this._monitorState.set(id, delta);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -3134,7 +3123,7 @@ class Runtime extends EventEmitter {
|
||||
* @param {!string} monitorId ID of the monitor to remove.
|
||||
*/
|
||||
requestRemoveMonitor (monitorId) {
|
||||
this._monitorState = this._monitorState.delete(monitorId);
|
||||
this._monitorState.delete(monitorId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3143,10 +3132,10 @@ class Runtime extends EventEmitter {
|
||||
* @return {boolean} true if monitor exists and was updated, false otherwise
|
||||
*/
|
||||
requestHideMonitor (monitorId) {
|
||||
return this.requestUpdateMonitor(new Map([
|
||||
['id', monitorId],
|
||||
['visible', false]
|
||||
]));
|
||||
return this.requestUpdateMonitor({
|
||||
id: monitorId,
|
||||
visible: false
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3156,10 +3145,10 @@ class Runtime extends EventEmitter {
|
||||
* @return {boolean} true if monitor exists and was updated, false otherwise
|
||||
*/
|
||||
requestShowMonitor (monitorId) {
|
||||
return this.requestUpdateMonitor(new Map([
|
||||
['id', monitorId],
|
||||
['visible', true]
|
||||
]));
|
||||
return this.requestUpdateMonitor({
|
||||
id: monitorId,
|
||||
visible: true
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3168,7 +3157,7 @@ class Runtime extends EventEmitter {
|
||||
* @param {!string} targetId Remove all monitors with given target ID.
|
||||
*/
|
||||
requestRemoveMonitorByTargetId (targetId) {
|
||||
this._monitorState = this._monitorState.filterNot(value => value.targetId === targetId);
|
||||
this._monitorState.filter(value => value.targetId !== targetId);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,7 +4,6 @@ const Blocks = require('./blocks');
|
||||
const Variable = require('../engine/variable');
|
||||
const Comment = require('../engine/comment');
|
||||
const uid = require('../util/uid');
|
||||
const {Map} = require('immutable');
|
||||
const log = require('../util/log');
|
||||
const StringUtil = require('../util/string-util');
|
||||
const VariableUtil = require('../util/variable-util');
|
||||
@@ -352,10 +351,10 @@ class Target extends EventEmitter {
|
||||
}, this.runtime);
|
||||
const monitorBlock = blocks.getBlock(variable.id);
|
||||
if (monitorBlock) {
|
||||
this.runtime.requestUpdateMonitor(Map({
|
||||
this.runtime.requestUpdateMonitor({
|
||||
id: id,
|
||||
params: blocks._getBlockParams(monitorBlock)
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* @typedef {import('./monitor-record')} MonitorRecord
|
||||
*/
|
||||
|
||||
const MonitorRecord = require('./monitor-record');
|
||||
|
||||
class MonitorState {
|
||||
constructor () {
|
||||
/**
|
||||
* @type {Map<string, MonitorRecord>}
|
||||
*/
|
||||
this.map = new Map();
|
||||
|
||||
/**
|
||||
* True if modified.
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.dirty = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} id
|
||||
* @returns {MonitorRecord|null}
|
||||
*/
|
||||
get (id) {
|
||||
return this.map.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} id
|
||||
* @returns {boolean}
|
||||
*/
|
||||
has (id) {
|
||||
return this.map.has(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or update.
|
||||
* @param {string} id
|
||||
* @param {MonitorRecord.JSDelta} delta
|
||||
*/
|
||||
set (id, delta) {
|
||||
if (this.map.has(id)) {
|
||||
const oldRecord = this.map.get(id);
|
||||
if (oldRecord.merge(delta)) {
|
||||
this.dirty = true;
|
||||
}
|
||||
} else {
|
||||
this.map.set(id, delta instanceof MonitorRecord ? delta : new MonitorRecord(delta));
|
||||
this.dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} id
|
||||
*/
|
||||
delete (id) {
|
||||
if (this.map.has(id)) {
|
||||
this.map.delete(id);
|
||||
this.dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes monitors that do not satisfy callback.
|
||||
* @param {(record: MonitorRecord) => boolean} callback Returns true to keep.
|
||||
*/
|
||||
filter (callback) {
|
||||
for (const id of Array.from(this.map.keys())) {
|
||||
const record = this.map.get(id);
|
||||
if (!callback(record)) {
|
||||
this.map.delete(id);
|
||||
this.dirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean} true if no monitors
|
||||
*/
|
||||
empty () {
|
||||
return this.map.size === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {number}
|
||||
*/
|
||||
get size () {
|
||||
return this.map.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {MonitorRecord[]}
|
||||
*/
|
||||
values () {
|
||||
return Array.from(this.map.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* For compatibility with immutable.js.
|
||||
* @returns {MonitorRecord[]}
|
||||
*/
|
||||
valueSeq () {
|
||||
return this.values();
|
||||
}
|
||||
|
||||
/**
|
||||
* You should not perform write operations on the clone.
|
||||
* @returns {MonitorState}
|
||||
*/
|
||||
shallowClone () {
|
||||
const result = new MonitorState();
|
||||
result.map = this.map;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MonitorState;
|
||||
@@ -414,7 +414,7 @@ const parseMonitorObject = (object, runtime, targets, extensions) => {
|
||||
}
|
||||
|
||||
// Create a monitor record for the runtime's monitorState
|
||||
runtime.requestAddMonitor(MonitorRecord({
|
||||
runtime.requestAddMonitor(new MonitorRecord({
|
||||
id: block.id,
|
||||
targetId: block.targetId,
|
||||
spriteName: block.targetId ? object.target : null,
|
||||
|
||||
@@ -699,10 +699,7 @@ const serializeMonitors = function (monitors, runtime, extensions) {
|
||||
serializedMonitor.isDiscrete = monitorData.isDiscrete;
|
||||
}
|
||||
return serializedMonitor;
|
||||
})
|
||||
// By default the sequence is lazily evaluated, but we want it to be evaluated right
|
||||
// now to update the used extension list.
|
||||
.toArray();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1440,7 +1437,7 @@ const deserializeMonitor = function (monitorData, runtime, targets, extensions)
|
||||
}
|
||||
}
|
||||
|
||||
runtime.requestAddMonitor(MonitorRecord(monitorData));
|
||||
runtime.requestAddMonitor(new MonitorRecord(monitorData));
|
||||
};
|
||||
|
||||
// Replace variable IDs throughout the project with
|
||||
|
||||
@@ -4,12 +4,12 @@ const MonitorRecord = require('../../src/engine/monitor-record');
|
||||
|
||||
test('Correctly serializes native extension only used by monitors', t => {
|
||||
const vm = new VM();
|
||||
vm.runtime.requestAddMonitor(MonitorRecord({
|
||||
vm.runtime.requestAddMonitor(new MonitorRecord({
|
||||
id: 'fakeblock1',
|
||||
opcode: 'pen_fakeblock',
|
||||
visiblle: true
|
||||
}));
|
||||
vm.runtime.requestAddMonitor(MonitorRecord({
|
||||
vm.runtime.requestAddMonitor(new MonitorRecord({
|
||||
id: 'fakeblock2',
|
||||
opcode: 'translate_fakeblock',
|
||||
visiblle: false
|
||||
@@ -22,12 +22,12 @@ test('Correctly serializes native extension only used by monitors', t => {
|
||||
|
||||
test('Correctly serializes custom extension only used by monitors', t => {
|
||||
const vm = new VM();
|
||||
vm.runtime.requestAddMonitor(MonitorRecord({
|
||||
vm.runtime.requestAddMonitor(new MonitorRecord({
|
||||
id: 'fakeblock1',
|
||||
opcode: 'fetch_fakeblock',
|
||||
visible: true
|
||||
}));
|
||||
vm.runtime.requestAddMonitor(MonitorRecord({
|
||||
vm.runtime.requestAddMonitor(new MonitorRecord({
|
||||
// should not be serialized at all
|
||||
id: 'fakeblock2',
|
||||
opcode: 'bitwise_fakeblock',
|
||||
|
||||
@@ -5,12 +5,12 @@ const MonitorRecord = require('../../src/engine/monitor-record');
|
||||
|
||||
test('does not serialize hidden monitors from extensions', t => {
|
||||
const rt = new Runtime();
|
||||
rt.requestAddMonitor(MonitorRecord({
|
||||
rt.requestAddMonitor(new MonitorRecord({
|
||||
id: 'timer',
|
||||
opcode: 'sensing_timer',
|
||||
visible: true
|
||||
}));
|
||||
rt.requestAddMonitor(MonitorRecord({
|
||||
rt.requestAddMonitor(new MonitorRecord({
|
||||
id: 'other_monitor',
|
||||
opcode: 'tw_someOpcodeThatIsntPartOfACoreExtension',
|
||||
visible: true
|
||||
|
||||
+11
-11
@@ -4,7 +4,6 @@ const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer
|
||||
const VirtualMachine = require('../../src/virtual-machine');
|
||||
const Runtime = require('../../src/engine/runtime');
|
||||
const MonitorRecord = require('../../src/engine/monitor-record');
|
||||
const {Map} = require('immutable');
|
||||
|
||||
const test = tap.test;
|
||||
|
||||
@@ -28,15 +27,15 @@ test('spec', t => {
|
||||
test('monitorStateEquals', t => {
|
||||
const r = new Runtime();
|
||||
const id = 'xklj4#!';
|
||||
const prevMonitorState = MonitorRecord({
|
||||
const prevMonitorState = new MonitorRecord({
|
||||
id,
|
||||
opcode: 'turtle whereabouts',
|
||||
value: '25'
|
||||
});
|
||||
const newMonitorDelta = Map({
|
||||
const newMonitorDelta = {
|
||||
id,
|
||||
value: String(25)
|
||||
});
|
||||
};
|
||||
r.requestAddMonitor(prevMonitorState);
|
||||
r.requestUpdateMonitor(newMonitorDelta);
|
||||
|
||||
@@ -49,31 +48,32 @@ test('monitorStateDoesNotEqual', t => {
|
||||
const r = new Runtime();
|
||||
const id = 'xklj4#!';
|
||||
const params = {seven: 7};
|
||||
const prevMonitorState = MonitorRecord({
|
||||
const prevMonitorState = new MonitorRecord({
|
||||
id,
|
||||
opcode: 'turtle whereabouts',
|
||||
value: '25'
|
||||
});
|
||||
|
||||
// Value change
|
||||
let newMonitorDelta = Map({
|
||||
let newMonitorDelta = {
|
||||
id,
|
||||
value: String(24)
|
||||
});
|
||||
};
|
||||
r.requestAddMonitor(prevMonitorState);
|
||||
r.requestUpdateMonitor(newMonitorDelta);
|
||||
|
||||
t.equals(false, prevMonitorState.equals(r._monitorState.get(id)));
|
||||
t.equals(true, r._monitorState.dirty);
|
||||
t.equals(String(24), r._monitorState.get(id).get('value'));
|
||||
|
||||
// Prop change
|
||||
newMonitorDelta = Map({
|
||||
r._monitorState.dirty = false;
|
||||
newMonitorDelta = {
|
||||
id: 'xklj4#!',
|
||||
params: params
|
||||
});
|
||||
};
|
||||
r.requestUpdateMonitor(newMonitorDelta);
|
||||
|
||||
t.equals(false, prevMonitorState.equals(r._monitorState.get(id)));
|
||||
t.equals(true, r._monitorState.dirty);
|
||||
t.equals(String(24), r._monitorState.get(id).value);
|
||||
t.equals(params, r._monitorState.get(id).params);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const tap = require('tap');
|
||||
const Runtime = require('../../src/engine/runtime');
|
||||
const {Map} = require('immutable');
|
||||
const MonitorRecord = require('../../src/engine/monitor-record');
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const Target = require('../../src/engine/target');
|
||||
|
||||
@@ -158,12 +158,12 @@ test('debug', t => {
|
||||
|
||||
test('setStageSize preserves monitor position relative to center of stage', t => {
|
||||
const rt = new Runtime();
|
||||
rt.requestAddMonitor(new Map([
|
||||
['id', 'abc'],
|
||||
rt.requestAddMonitor(new MonitorRecord({
|
||||
id: 'abc',
|
||||
// top right corner
|
||||
['x', 0],
|
||||
['y', 0]
|
||||
]));
|
||||
x: 0,
|
||||
y: 0
|
||||
}));
|
||||
rt.setStageSize(640, 362);
|
||||
const finalState = rt.getMonitorState().get('abc');
|
||||
t.equal(finalState.get('x'), 80);
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
const {test} = require('tap');
|
||||
const MonitorRecord = require('../../src/engine/monitor-record');
|
||||
|
||||
test('new and get', t => {
|
||||
const record = new MonitorRecord({
|
||||
id: 'a',
|
||||
x: 10,
|
||||
y: 20
|
||||
});
|
||||
|
||||
t.equal(record.id, 'a');
|
||||
t.equal(record.x, 10);
|
||||
t.equal(record.y, 20);
|
||||
|
||||
t.equal(record.get('id'), 'a');
|
||||
t.equal(record.get('x'), 10);
|
||||
t.equal(record.get('y'), 20);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('merge', t => {
|
||||
const record = new MonitorRecord({
|
||||
id: 'a',
|
||||
x: 10,
|
||||
y: 20
|
||||
});
|
||||
|
||||
t.equal(record.merge({
|
||||
x: 40,
|
||||
y: null
|
||||
}), true);
|
||||
t.equal(record.x, 40);
|
||||
t.equal(record.y, 20);
|
||||
|
||||
t.equal(record.merge({
|
||||
x: 40,
|
||||
y: undefined
|
||||
}), false);
|
||||
t.equal(record.x, 40);
|
||||
t.equal(record.y, 20);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('externalDeltaToJS', t => {
|
||||
t.same(MonitorRecord.externalDeltaToJS({
|
||||
whatever: 'a'
|
||||
}), {whatever: 'a'});
|
||||
|
||||
t.same(MonitorRecord.externalDeltaToJS({
|
||||
toJS: () => ({whatever: 'a'})
|
||||
}), {whatever: 'a'});
|
||||
|
||||
t.end();
|
||||
});
|
||||
@@ -0,0 +1,115 @@
|
||||
const {test} = require('tap');
|
||||
const MonitorState = require('../../src/engine/tw-monitor-state');
|
||||
const MonitorRecord = require('../../src/engine/monitor-record');
|
||||
|
||||
test('basic operation', t => {
|
||||
const state = new MonitorState();
|
||||
|
||||
t.equal(state.dirty, false);
|
||||
t.equal(state.has('id'), false);
|
||||
t.equal(state.size, 0);
|
||||
t.equal(state.empty(), true);
|
||||
|
||||
state.set('id', new MonitorRecord({
|
||||
id: 'id',
|
||||
value: 0
|
||||
}));
|
||||
t.equal(state.has('id'), true);
|
||||
t.equal(state.get('id').id, 'id');
|
||||
t.equal(state.get('id').value, 0);
|
||||
t.equal(state.size, 1);
|
||||
t.equal(state.empty(), false);
|
||||
t.equal(state.dirty, true);
|
||||
|
||||
state.dirty = false;
|
||||
state.set('id', new MonitorRecord({
|
||||
id: 'id',
|
||||
value: -0
|
||||
}));
|
||||
t.equal(state.get('id').value, -0);
|
||||
t.equal(state.dirty, true);
|
||||
|
||||
state.dirty = false;
|
||||
state.set('id', new MonitorRecord({
|
||||
id: 'id',
|
||||
value: -0
|
||||
}));
|
||||
t.equal(state.dirty, false);
|
||||
|
||||
state.dirty = false;
|
||||
state.delete('id');
|
||||
t.equal(state.has('id'), false);
|
||||
t.equal(state.get('id'), undefined);
|
||||
t.equal(state.size, 0);
|
||||
t.equal(state.empty(), true);
|
||||
t.equal(state.dirty, true);
|
||||
|
||||
state.dirty = false;
|
||||
state.delete('id');
|
||||
t.equal(state.dirty, false);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('filter', t => {
|
||||
const state = new MonitorState();
|
||||
|
||||
state.set('id1', new MonitorRecord({
|
||||
id: 'id1',
|
||||
value: 5
|
||||
}));
|
||||
state.set('id2', new MonitorRecord({
|
||||
id: 'id2',
|
||||
value: 10
|
||||
}));
|
||||
state.set('id3', new MonitorRecord({
|
||||
id: 'id3',
|
||||
value: 15
|
||||
}));
|
||||
|
||||
state.dirty = false;
|
||||
state.filter(record => record.value !== 10);
|
||||
t.equal(state.dirty, true);
|
||||
t.equal(state.has('id1'), true);
|
||||
t.equal(state.has('id2'), false);
|
||||
t.equal(state.has('id3'), true);
|
||||
|
||||
state.dirty = false;
|
||||
state.filter(record => record.value !== 10);
|
||||
t.equal(state.dirty, false);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('value', t => {
|
||||
const state = new MonitorState();
|
||||
|
||||
const a = new MonitorRecord({
|
||||
id: 'a'
|
||||
});
|
||||
const b = new MonitorRecord({
|
||||
id: 'b'
|
||||
});
|
||||
state.set('a', a);
|
||||
state.set('b', b);
|
||||
|
||||
t.same(state.values(), [a, b]);
|
||||
t.same(state.valueSeq(), [a, b]);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('shallowClone', t => {
|
||||
const state = new MonitorState();
|
||||
|
||||
state.set('id', new MonitorRecord({
|
||||
id: 'id'
|
||||
}));
|
||||
|
||||
const clone = state.shallowClone();
|
||||
t.not(state, clone);
|
||||
t.equal(state.map, clone.map);
|
||||
t.equal(clone.dirty, false);
|
||||
|
||||
t.end();
|
||||
});
|
||||
@@ -69,7 +69,6 @@ module.exports = [
|
||||
'decode-html': true,
|
||||
'format-message': true,
|
||||
'htmlparser2': true,
|
||||
'immutable': true,
|
||||
'scratch-parser': true,
|
||||
'socket.io-client': true,
|
||||
'text-encoding': true
|
||||
|
||||
Reference in new issue
Block a user