linny-r 1.4.0 → 1.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/server.js +48 -6
- package/static/index.html +9 -5
- package/static/linny-r.css +0 -14
- package/static/scripts/linny-r-ctrl.js +69 -4
- package/static/scripts/linny-r-gui.js +37 -26
- package/static/scripts/linny-r-model.js +136 -71
- package/static/scripts/linny-r-utils.js +38 -15
- package/static/scripts/linny-r-vm.js +265 -169
package/package.json
CHANGED
package/server.js
CHANGED
@@ -910,12 +910,12 @@ function loadData(res, url) {
|
|
910
910
|
// the call-back Python script specified for the channel
|
911
911
|
|
912
912
|
function receiver(res, sp) {
|
913
|
-
//This function processes all receiver actions
|
913
|
+
// This function processes all receiver actions.
|
914
914
|
let
|
915
915
|
rpath = anyOSpath(sp.get('path') || ''),
|
916
916
|
rfile = anyOSpath(sp.get('file') || '');
|
917
|
-
// Assume that path is relative to
|
918
|
-
// a (back)slash or
|
917
|
+
// Assume that path is relative to working directory unless it starts
|
918
|
+
// with a (back)slash or specifies drive or volume.
|
919
919
|
if(!(rpath.startsWith(path.sep) || rpath.indexOf(':') >= 0 ||
|
920
920
|
rpath.startsWith(WORKING_DIRECTORY))) {
|
921
921
|
rpath = path.join(WORKING_DIRECTORY, rpath);
|
@@ -1038,8 +1038,49 @@ function rcvrAbort(res, rpath, rfile, log) {
|
|
1038
1038
|
}
|
1039
1039
|
|
1040
1040
|
function rcvrReport(res, rpath, rfile, run, data, stats, log) {
|
1041
|
+
// Purge reports older than 24 hours.
|
1041
1042
|
try {
|
1042
|
-
|
1043
|
+
const
|
1044
|
+
now = new Date(),
|
1045
|
+
flist = fs.readdirSync(WORKSPACE.reports);
|
1046
|
+
let n = 0;
|
1047
|
+
for(let i = 0; i < flist.length; i++) {
|
1048
|
+
const
|
1049
|
+
pp = path.parse(flist[i]),
|
1050
|
+
fp = path.join(WORKSPACE.reports, flist[i]);
|
1051
|
+
// NOTE: Only consider text files (extension .txt)
|
1052
|
+
if(pp.ext === '.txt') {
|
1053
|
+
// Delete only if file is older than 24 hours.
|
1054
|
+
const fstat = fs.statSync(fp);
|
1055
|
+
if(now - fstat.mtimeMs > 24 * 3600000) {
|
1056
|
+
// Delete text file
|
1057
|
+
try {
|
1058
|
+
fs.unlinkSync(fp);
|
1059
|
+
n++;
|
1060
|
+
} catch(err) {
|
1061
|
+
console.log('WARNING: Failed to delete', fp);
|
1062
|
+
console.log(err);
|
1063
|
+
}
|
1064
|
+
}
|
1065
|
+
}
|
1066
|
+
}
|
1067
|
+
if(n) console.log(n + 'report file' + (n > 1 ? 's' : '') + 'purged');
|
1068
|
+
} catch(err) {
|
1069
|
+
// Log error, but do not abort.
|
1070
|
+
console.log(err);
|
1071
|
+
}
|
1072
|
+
// Now save the reports.
|
1073
|
+
// NOTE: The optional @ indicates where the run number must be inserted.
|
1074
|
+
// If not specified, append run number to the base report file name.
|
1075
|
+
if(rfile.indexOf('@') < 0) {
|
1076
|
+
rfile += run;
|
1077
|
+
} else {
|
1078
|
+
rfile = rfile.replace('@', run);
|
1079
|
+
}
|
1080
|
+
const base = path.join(rpath, rfile);
|
1081
|
+
let fp;
|
1082
|
+
try {
|
1083
|
+
fp = path.join(base + '-data.txt');
|
1043
1084
|
fs.writeFileSync(fp, data);
|
1044
1085
|
} catch(err) {
|
1045
1086
|
console.log(err);
|
@@ -1048,7 +1089,7 @@ function rcvrReport(res, rpath, rfile, run, data, stats, log) {
|
|
1048
1089
|
return;
|
1049
1090
|
}
|
1050
1091
|
try {
|
1051
|
-
fp = path.join(
|
1092
|
+
fp = path.join(base + '-stats.txt');
|
1052
1093
|
fs.writeFileSync(fp, stats);
|
1053
1094
|
} catch(err) {
|
1054
1095
|
console.log(err);
|
@@ -1057,7 +1098,7 @@ function rcvrReport(res, rpath, rfile, run, data, stats, log) {
|
|
1057
1098
|
return;
|
1058
1099
|
}
|
1059
1100
|
try {
|
1060
|
-
fp = path.join(
|
1101
|
+
fp = path.join(base + '-log.txt');
|
1061
1102
|
fs.writeFileSync(fp, log);
|
1062
1103
|
} catch(err) {
|
1063
1104
|
console.log(err);
|
@@ -1630,6 +1671,7 @@ function createWorkspace() {
|
|
1630
1671
|
data: path.join(SETTINGS.user_dir, 'data'),
|
1631
1672
|
diagrams: path.join(SETTINGS.user_dir, 'diagrams'),
|
1632
1673
|
modules: path.join(SETTINGS.user_dir, 'modules'),
|
1674
|
+
reports: path.join(SETTINGS.user_dir, 'reports'),
|
1633
1675
|
solver_output: path.join(SETTINGS.user_dir, 'solver')
|
1634
1676
|
};
|
1635
1677
|
// Create these sub-directories if not aready there
|
package/static/index.html
CHANGED
@@ -165,8 +165,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
165
165
|
// Inform user that newer version exists
|
166
166
|
UI.check_update_modal.element('msg').innerHTML = [
|
167
167
|
'<a href="', GITHUB_REPOSITORY,
|
168
|
-
'/
|
169
|
-
info[1].replaceAll('.', ''), '" ',
|
168
|
+
'/releases/tag/v', info[1], '" ',
|
170
169
|
'title="Click to view version release notes" ',
|
171
170
|
'target="_blank">Version <strong>',
|
172
171
|
info[1], '</strong></a> released on ',
|
@@ -601,6 +600,14 @@ and move the cursor over the status bar">
|
|
601
600
|
</td>
|
602
601
|
<td style="padding-bottom:4px">Infer and display cost prices</td>
|
603
602
|
</tr>
|
603
|
+
<tr title="Reports will be saved in user/reports/, and removed after 24 h">
|
604
|
+
<td style="padding:0px">
|
605
|
+
<div id="settings-report-results" class="box clear"></div>
|
606
|
+
</td>
|
607
|
+
<td style="padding-bottom:4px">
|
608
|
+
Report results after each run
|
609
|
+
</td>
|
610
|
+
</tr>
|
604
611
|
<tr>
|
605
612
|
<td style="padding:0px">
|
606
613
|
<div id="settings-block-arrows" class="box clear"></div>
|
@@ -1280,9 +1287,6 @@ NOTE: Unit symbols are case-sensitive, so BTU ≠ Btu">
|
|
1280
1287
|
<div id="process-dlg" class="inp-dlg">
|
1281
1288
|
<div class="dlg-title">
|
1282
1289
|
Process properties
|
1283
|
-
<div class="simbtn">
|
1284
|
-
<img id="process-sim-btn" class="btn sim enab" src="images/process.png">
|
1285
|
-
</div>
|
1286
1290
|
<img class="cancel-btn" src="images/cancel.png">
|
1287
1291
|
<img class="ok-btn" src="images/ok.png">
|
1288
1292
|
</div>
|
package/static/linny-r.css
CHANGED
@@ -346,20 +346,6 @@ div.contbtn {
|
|
346
346
|
cursor: pointer;
|
347
347
|
}
|
348
348
|
|
349
|
-
div.simbtn {
|
350
|
-
display: none;
|
351
|
-
margin-left: 5px;
|
352
|
-
}
|
353
|
-
|
354
|
-
img.sim {
|
355
|
-
height: 14px;
|
356
|
-
width: 14px;
|
357
|
-
}
|
358
|
-
|
359
|
-
img.sim.enab:hover {
|
360
|
-
background-color: #9e96e5;
|
361
|
-
}
|
362
|
-
|
363
349
|
input {
|
364
350
|
vertical-align: baseline;
|
365
351
|
}
|
@@ -302,14 +302,20 @@ class Controller {
|
|
302
302
|
(name.startsWith(this.BLACK_BOX) || name[0].match(/[\w]/));
|
303
303
|
}
|
304
304
|
|
305
|
-
prefixesAndName(name) {
|
305
|
+
prefixesAndName(name, key=false) {
|
306
306
|
// Returns name split exclusively at '[non-space]: [non-space]'
|
307
|
+
let sep = this.PREFIXER,
|
308
|
+
space = ' ';
|
309
|
+
if(key) {
|
310
|
+
sep = ':_';
|
311
|
+
space = '_';
|
312
|
+
}
|
307
313
|
const
|
308
|
-
s = name.split(
|
314
|
+
s = name.split(sep),
|
309
315
|
pan = [s[0]];
|
310
316
|
for(let i = 1; i < s.length; i++) {
|
311
317
|
const j = pan.length - 1;
|
312
|
-
if(s[i].startsWith(
|
318
|
+
if(s[i].startsWith(space) || (i > 0 && pan[j].endsWith(space))) {
|
313
319
|
pan[j] += s[i];
|
314
320
|
} else {
|
315
321
|
pan.push(s[i]);
|
@@ -350,11 +356,70 @@ class Controller {
|
|
350
356
|
this.LINK_ARROW : this.CONSTRAINT_ARROW),
|
351
357
|
nodes = name.split(arrow);
|
352
358
|
for(let i = 0; i < nodes.length; i++) {
|
353
|
-
nodes[i] = nodes[i].replace(/^:\s*/, prefix)
|
359
|
+
nodes[i] = nodes[i].replace(/^:\s*/, prefix)
|
360
|
+
// NOTE: An embedded double prefix, e.g., "xxx: : yyy" indicates
|
361
|
+
// that the second colon+space should be replaced by the prefix.
|
362
|
+
// This "double prefix" may occur only once in an entity name,
|
363
|
+
// hence no global regexp.
|
364
|
+
.replace(/(\w+):\s+:\s+(\w+)/, `$1: ${prefix}$2`);
|
354
365
|
}
|
355
366
|
return nodes.join(arrow);
|
356
367
|
}
|
357
368
|
|
369
|
+
tailNumber(name) {
|
370
|
+
// Returns the string of digits at the end of `name`. If not there,
|
371
|
+
// check prefixes (if any) *from right to left* for a tail number.
|
372
|
+
// Thus, the number that is "closest" to the name part is returned.
|
373
|
+
const pan = UI.prefixesAndName(name);
|
374
|
+
let n = endsWithDigits(pan.pop());
|
375
|
+
while(!n && pan.length > 0) {
|
376
|
+
n = endsWithDigits(pan.pop());
|
377
|
+
}
|
378
|
+
return n;
|
379
|
+
}
|
380
|
+
|
381
|
+
compareFullNames(n1, n2, key=false) {
|
382
|
+
// Compare full names, considering prefixes in *left-to-right* order
|
383
|
+
// while taking into account the tailnumber for each part so that
|
384
|
+
// "xx: yy2: nnn" comes before "xx: yy10: nnn".
|
385
|
+
if(n1 === n2) return 0;
|
386
|
+
if(key) {
|
387
|
+
// NOTE: Replacing link and constraint arrows by two prefixers
|
388
|
+
// ensures that sort wil be first on FROM node, and then on TO node.
|
389
|
+
const p2 = UI.PREFIXER + UI.PREFIXER;
|
390
|
+
// Keys for links and constraints are not based on their names,
|
391
|
+
// so look up their names before comparing.
|
392
|
+
if(n1.indexOf('____') > 0 && MODEL.constraints[n1]) {
|
393
|
+
n1 = MODEL.constraints[n1].displayName
|
394
|
+
.replace(UI.CONSTRAINT_ARROW, p2);
|
395
|
+
} else if(n1.indexOf('___') > 0 && MODEL.links[n1]) {
|
396
|
+
n1 = MODEL.links[n1].displayName
|
397
|
+
.replace(UI.LINK_ARROW, p2);
|
398
|
+
}
|
399
|
+
if(n2.indexOf('____') > 0 && MODEL.constraints[n2]) {
|
400
|
+
n2 = MODEL.constraints[n2].displayName.
|
401
|
+
replace(UI.CONSTRAINT_ARROW, p2);
|
402
|
+
} else if(n2.indexOf('___') > 0 && MODEL.links[n2]) {
|
403
|
+
n2 = MODEL.links[n2].displayName
|
404
|
+
.replace(UI.LINK_ARROW, p2);
|
405
|
+
}
|
406
|
+
n1 = n1.toLowerCase().replaceAll(' ', '_');
|
407
|
+
n2 = n2.toLowerCase().replaceAll(' ', '_');
|
408
|
+
}
|
409
|
+
const
|
410
|
+
pan1 = UI.prefixesAndName(n1, key),
|
411
|
+
pan2 = UI.prefixesAndName(n2, key),
|
412
|
+
sl = Math.min(pan1.length, pan2.length);
|
413
|
+
let i = 0;
|
414
|
+
while(i < sl) {
|
415
|
+
const c = compareWithTailNumbers(pan1[i], pan2[i]);
|
416
|
+
if(c !== 0) return c;
|
417
|
+
i++;
|
418
|
+
}
|
419
|
+
return pan1.length - pan2.length;
|
420
|
+
}
|
421
|
+
|
422
|
+
|
358
423
|
nameToID(name) {
|
359
424
|
// Returns a name in lower case with link arrow replaced by three
|
360
425
|
// underscores, constraint link arrow by four underscores, and spaces
|
@@ -4901,7 +4901,7 @@ class GUIController extends Controller {
|
|
4901
4901
|
// Logs MB's of used heap memory to console (to detect memory leaks)
|
4902
4902
|
// NOTE: this feature is supported only by Chrome
|
4903
4903
|
if(msg) msg += ' -- ';
|
4904
|
-
if(
|
4904
|
+
if(performance.memory !== undefined) {
|
4905
4905
|
console.log(msg + 'Allocated memory: ' + Math.round(
|
4906
4906
|
performance.memory.usedJSHeapSize/1048576.0).toFixed(1) + ' MB');
|
4907
4907
|
}
|
@@ -5701,6 +5701,7 @@ console.log('HERE name conflicts', name_conflicts, mapping);
|
|
5701
5701
|
this.setBox('settings-decimal-comma', model.decimal_comma);
|
5702
5702
|
this.setBox('settings-align-to-grid', model.align_to_grid);
|
5703
5703
|
this.setBox('settings-cost-prices', model.infer_cost_prices);
|
5704
|
+
this.setBox('settings-report-results', model.report_results);
|
5704
5705
|
this.setBox('settings-block-arrows', model.show_block_arrows);
|
5705
5706
|
md.show('name');
|
5706
5707
|
}
|
@@ -5753,6 +5754,7 @@ console.log('HERE name conflicts', name_conflicts, mapping);
|
|
5753
5754
|
if(!model.scale_units.hasOwnProperty(dsu)) model.addScaleUnit(dsu);
|
5754
5755
|
model.default_unit = dsu;
|
5755
5756
|
model.currency_unit = md.element('currency-unit').value.trim();
|
5757
|
+
model.report_results = UI.boxChecked('settings-report-results');
|
5756
5758
|
model.encrypt = UI.boxChecked('settings-encrypt');
|
5757
5759
|
model.decimal_comma = UI.boxChecked('settings-decimal-comma');
|
5758
5760
|
// Some changes may necessitate redrawing the diagram
|
@@ -7354,7 +7356,8 @@ NOTE: Grouping groups results in a single group, e.g., (1;2);(3;4;5) evaluates a
|
|
7354
7356
|
// is passed to differentiate between the DOM elements to be used
|
7355
7357
|
const
|
7356
7358
|
type = document.getElementById(prefix + 'variable-obj').value,
|
7357
|
-
n_list = this.namesByType(VM.object_types[type]).sort(
|
7359
|
+
n_list = this.namesByType(VM.object_types[type]).sort(
|
7360
|
+
(a, b) => UI.compareFullNames(a, b)),
|
7358
7361
|
vn = document.getElementById(prefix + 'variable-name'),
|
7359
7362
|
options = [];
|
7360
7363
|
// Add "empty" as first and initial option, but disable it.
|
@@ -7396,7 +7399,7 @@ NOTE: Grouping groups results in a single group, e.g., (1;2);(3;4;5) evaluates a
|
|
7396
7399
|
slist.push(d.modifiers[m].selector);
|
7397
7400
|
}
|
7398
7401
|
// Sort to present equations in alphabetical order
|
7399
|
-
slist.sort(
|
7402
|
+
slist.sort((a, b) => UI.compareFullNames(a, b));
|
7400
7403
|
for(let i = 0; i < slist.length; i++) {
|
7401
7404
|
options.push(`<option value="${slist[i]}">${slist[i]}</option>`);
|
7402
7405
|
}
|
@@ -9898,13 +9901,7 @@ class GUIDatasetManager extends DatasetManager {
|
|
9898
9901
|
dl = [],
|
9899
9902
|
dnl = [],
|
9900
9903
|
sd = this.selected_dataset,
|
9901
|
-
ioclass = ['', 'import', 'export']
|
9902
|
-
ciPrefixCompare = (a, b) => {
|
9903
|
-
const
|
9904
|
-
pa = a.split(':_').join(' '),
|
9905
|
-
pb = b.split(':_').join(' ');
|
9906
|
-
return ciCompare(pa, pb);
|
9907
|
-
};
|
9904
|
+
ioclass = ['', 'import', 'export'];
|
9908
9905
|
for(let d in MODEL.datasets) if(MODEL.datasets.hasOwnProperty(d) &&
|
9909
9906
|
// NOTE: do not list "black-boxed" entities
|
9910
9907
|
!d.startsWith(UI.BLACK_BOX) &&
|
@@ -9915,7 +9912,7 @@ class GUIDatasetManager extends DatasetManager {
|
|
9915
9912
|
dnl.push(d);
|
9916
9913
|
}
|
9917
9914
|
}
|
9918
|
-
dnl.sort(
|
9915
|
+
dnl.sort((a, b) => UI.compareFullNames(a, b, true));
|
9919
9916
|
// First determine indentation levels, prefixes and names
|
9920
9917
|
const
|
9921
9918
|
indent = [],
|
@@ -10676,7 +10673,7 @@ class GUIDatasetManager extends DatasetManager {
|
|
10676
10673
|
const
|
10677
10674
|
ln = document.getElementById('series-line-number'),
|
10678
10675
|
lc = document.getElementById('series-line-count');
|
10679
|
-
ln.innerHTML = this.series_data.value.
|
10676
|
+
ln.innerHTML = this.series_data.value.substring(0,
|
10680
10677
|
this.series_data.selectionStart).split('\n').length;
|
10681
10678
|
lc.innerHTML = this.series_data.value.split('\n').length;
|
10682
10679
|
}
|
@@ -12188,7 +12185,7 @@ class GUISensitivityAnalysis extends SensitivityAnalysis {
|
|
12188
12185
|
const
|
12189
12186
|
ds_dict = MODEL.listOfAllSelectors,
|
12190
12187
|
html = [],
|
12191
|
-
sl = Object.keys(ds_dict).sort(
|
12188
|
+
sl = Object.keys(ds_dict).sort((a, b) => UI.compareFullNames(a, b, true));
|
12192
12189
|
for(let i = 0; i < sl.length; i++) {
|
12193
12190
|
const
|
12194
12191
|
s = sl[i],
|
@@ -13173,7 +13170,7 @@ class GUIExperimentManager extends ExperimentManager {
|
|
13173
13170
|
for(let i = 0; i < x.variables.length; i++) {
|
13174
13171
|
addDistinct(x.variables[i].displayName, vl);
|
13175
13172
|
}
|
13176
|
-
vl.sort(
|
13173
|
+
vl.sort((a, b) => UI.compareFullNames(a, b));
|
13177
13174
|
for(let i = 0; i < vl.length; i++) {
|
13178
13175
|
ol.push(['<option value="', vl[i], '"',
|
13179
13176
|
(vl[i] == x.selected_variable ? ' selected="selected"' : ''),
|
@@ -15522,7 +15519,7 @@ class Finder {
|
|
15522
15519
|
}
|
15523
15520
|
}
|
15524
15521
|
}
|
15525
|
-
enl.sort(
|
15522
|
+
enl.sort((a, b) => UI.compareFullNames(a, b, true));
|
15526
15523
|
}
|
15527
15524
|
document.getElementById('finder-entity-imgs').innerHTML = imgs;
|
15528
15525
|
let seid = 'etr';
|
@@ -15919,7 +15916,7 @@ class Finder {
|
|
15919
15916
|
// No cost price calculation => trim associated attributes from header
|
15920
15917
|
let p = ah.indexOf('\tCost price');
|
15921
15918
|
if(p > 0) {
|
15922
|
-
ah = ah.
|
15919
|
+
ah = ah.substring(0, p);
|
15923
15920
|
} else {
|
15924
15921
|
// SOC is exogenous, and hence comes before F in header => replace
|
15925
15922
|
ah = ah.replace('\tShare of cost', '');
|
@@ -15991,8 +15988,8 @@ class GUIReceiver {
|
|
15991
15988
|
}
|
15992
15989
|
|
15993
15990
|
log(msg) {
|
15994
|
-
// Logs a message displayed on the status line while solving
|
15995
|
-
if(this.active) {
|
15991
|
+
// Logs a message displayed on the status line while solving.
|
15992
|
+
if(this.active || MODEL.report_results) {
|
15996
15993
|
if(!msg.startsWith('[')) {
|
15997
15994
|
const
|
15998
15995
|
d = new Date(),
|
@@ -16135,21 +16132,34 @@ class GUIReceiver {
|
|
16135
16132
|
report() {
|
16136
16133
|
// Posts the run results to the local server, or signals an error
|
16137
16134
|
let form,
|
16138
|
-
run = ''
|
16135
|
+
run = '',
|
16136
|
+
path = this.channel,
|
16137
|
+
file = this.file_name;
|
16139
16138
|
// NOTE: Always set `solving` to FALSE
|
16140
16139
|
this.solving = false;
|
16141
|
-
|
16140
|
+
// NOTE: When reporting receiver while is not active, report the
|
16141
|
+
// results of the running experiment.
|
16142
|
+
if(this.experiment || !this.active) {
|
16142
16143
|
if(MODEL.running_experiment) {
|
16143
16144
|
run = MODEL.running_experiment.active_combination_index;
|
16144
16145
|
this.log(`Reporting: ${this.file_name} (run #${run})`);
|
16145
16146
|
}
|
16146
16147
|
}
|
16148
|
+
// NOTE: If receiver is not active, path and file must be set.
|
16149
|
+
if(!this.active) {
|
16150
|
+
path = 'user/reports';
|
16151
|
+
// NOTE: The @ will be replaced by the run number, so that that
|
16152
|
+
// number precedes the clock time. The @ will be unique because
|
16153
|
+
// `asFileName()` replaces special characters by underscores.
|
16154
|
+
file = REPOSITORY_BROWSER.asFileName(MODEL.name || 'model') +
|
16155
|
+
'@-' + compactClockTime();
|
16156
|
+
}
|
16147
16157
|
if(MODEL.solved && !VM.halted) {
|
16148
16158
|
// Normal execution termination => report results
|
16149
16159
|
const od = MODEL.outputData;
|
16150
16160
|
form = {
|
16151
|
-
path:
|
16152
|
-
file:
|
16161
|
+
path: path,
|
16162
|
+
file: file,
|
16153
16163
|
action: 'report',
|
16154
16164
|
run: run,
|
16155
16165
|
data: od[0],
|
@@ -16176,12 +16186,13 @@ class GUIReceiver {
|
|
16176
16186
|
return response.text();
|
16177
16187
|
})
|
16178
16188
|
.then((data) => {
|
16179
|
-
// For experiments, only display server response if warning or error
|
16189
|
+
// For experiments, only display server response if warning or error.
|
16180
16190
|
UI.postResponseOK(data, !RECEIVER.experiment);
|
16181
|
-
// If execution completed, perform the call-back action
|
16191
|
+
// If execution completed, perform the call-back action if the
|
16192
|
+
// receiver is active (so not when auto-reporting a run).
|
16182
16193
|
// NOTE: for experiments, call-back is performed upon completion by
|
16183
|
-
// the Experiment Manager
|
16184
|
-
if(!RECEIVER.experiment) RECEIVER.callBack();
|
16194
|
+
// the Experiment Manager.
|
16195
|
+
if(RECEIVER.active && !RECEIVER.experiment) RECEIVER.callBack();
|
16185
16196
|
})
|
16186
16197
|
.catch(() => UI.warn(UI.WARNING.NO_CONNECTION, err));
|
16187
16198
|
}
|