diff --git a/package-lock.json b/package-lock.json index c64871a0..d6759c7a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index c126de34..a22e531a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/blocks/scratch3_data.js b/src/blocks/scratch3_data.js index 1a2e5b86..1290434b 100644 --- a/src/blocks/scratch3_data.js +++ b/src/blocks/scratch3_data.js @@ -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(); } diff --git a/src/engine/blocks.js b/src/engine/blocks.js index 71ace3a2..68b3e606 100644 --- a/src/engine/blocks.js +++ b/src/engine/blocks.js @@ -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, diff --git a/src/engine/execute.js b/src/engine/execute.js index c411f244..c9f2f115 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -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. diff --git a/src/engine/monitor-record.js b/src/engine/monitor-record.js index 1259d525..694bcd84 100644 --- a/src/engine/monitor-record.js +++ b/src/engine/monitor-record.js @@ -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; diff --git a/src/engine/runtime.js b/src/engine/runtime.js index ab704865..e1802a7b 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -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); } /** diff --git a/src/engine/target.js b/src/engine/target.js index e9cecfe0..931ec3c1 100644 --- a/src/engine/target.js +++ b/src/engine/target.js @@ -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) - })); + }); } } diff --git a/src/engine/tw-monitor-state.js b/src/engine/tw-monitor-state.js new file mode 100644 index 00000000..435db9c4 --- /dev/null +++ b/src/engine/tw-monitor-state.js @@ -0,0 +1,118 @@ +/** + * @typedef {import('./monitor-record')} MonitorRecord + */ + +const MonitorRecord = require('./monitor-record'); + +class MonitorState { + constructor () { + /** + * @type {Map} + */ + 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; diff --git a/src/serialization/sb2.js b/src/serialization/sb2.js index 1cde8e26..5af4d4ec 100644 --- a/src/serialization/sb2.js +++ b/src/serialization/sb2.js @@ -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, diff --git a/src/serialization/sb3.js b/src/serialization/sb3.js index 815c2837..33c82220 100644 --- a/src/serialization/sb3.js +++ b/src/serialization/sb3.js @@ -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 diff --git a/test/integration/tw_serialize_extension_only_used_by_monitor.js b/test/integration/tw_serialize_extension_only_used_by_monitor.js index a10cf18b..05d55420 100644 --- a/test/integration/tw_serialize_extension_only_used_by_monitor.js +++ b/test/integration/tw_serialize_extension_only_used_by_monitor.js @@ -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', diff --git a/test/integration/tw_serialize_hidden_monitors.js b/test/integration/tw_serialize_hidden_monitors.js index 66e41155..ea110549 100644 --- a/test/integration/tw_serialize_hidden_monitors.js +++ b/test/integration/tw_serialize_hidden_monitors.js @@ -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 diff --git a/test/unit/engine_runtime.js b/test/unit/engine_runtime.js index 68341c29..d99ecbe5 100644 --- a/test/unit/engine_runtime.js +++ b/test/unit/engine_runtime.js @@ -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); diff --git a/test/unit/engine_runtime_tw.js b/test/unit/engine_runtime_tw.js index 273f815b..e69e736a 100644 --- a/test/unit/engine_runtime_tw.js +++ b/test/unit/engine_runtime_tw.js @@ -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); diff --git a/test/unit/tw_monitor_record.js b/test/unit/tw_monitor_record.js new file mode 100644 index 00000000..59cd3a13 --- /dev/null +++ b/test/unit/tw_monitor_record.js @@ -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(); +}); diff --git a/test/unit/tw_monitor_state.js b/test/unit/tw_monitor_state.js new file mode 100644 index 00000000..780edb6b --- /dev/null +++ b/test/unit/tw_monitor_state.js @@ -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(); +}); diff --git a/webpack.config.js b/webpack.config.js index 8686f7a7..25d241d6 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -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