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:
Thomas Weber authored and GitHub committed 2026-01-09 22:30:18 -06:00
1 parent 553a2a434d
commit 6abc33d787
18 files changed
+552 -116

No files matched your search

-9
View File
@@ -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",
-1
View File
@@ -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",
+1 -1
View File
@@ -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 -4
View File
@@ -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 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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);
}
/**
+2 -3
View File
@@ -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)
}));
});
}
}
+118
View File
@@ -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;
+1 -1
View File
@@ -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,
+2 -5
View File
@@ -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
View File
@@ -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);
+6 -6
View File
@@ -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);
+56
View File
@@ -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();
});
+115
View File
@@ -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();
});
-1
View File
@@ -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