iobroker.javascript 9.2.2 → 9.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.md +10 -25
  2. package/admin/assets/{AiChatPanel-Bbux7LP7.js → AiChatPanel-B4vPXgLz.js} +1 -1
  3. package/admin/assets/{ScriptEditor-DKljeYQN.js → ScriptEditor-CmEeMfC2.js} +1 -1
  4. package/admin/assets/ScriptEditorVanillaMonaco-C22Gpet7.js +3 -0
  5. package/admin/assets/cronHoverProvider-Bg0Vz-1E.js +2 -0
  6. package/admin/assets/index-45IwpC7_.js +1 -0
  7. package/admin/assets/{index-DgE6XhUM.js → index-7mrTpzQy.js} +1 -1
  8. package/admin/assets/{index-DSvAO584.js → index-BTQOdA7v.js} +83 -83
  9. package/admin/assets/{index-_BUuZlI0.js → index-Dggem6AX.js} +1 -1
  10. package/admin/assets/{index-CexrwGGC.js → index-qoSy77Bg.js} +41 -37
  11. package/admin/assets/{localSharedImportMap-BCwqnmn4.js → localSharedImportMap-CVlAGiIu.js} +1 -1
  12. package/admin/assets/stateHoverProvider-DxTNxyYN.js +9 -0
  13. package/admin/assets/{virtual_mf-REMOTE_ENTRY_ID_iobroker_javascript__remoteEntry_js-DNrEsMrg.js → virtual_mf-REMOTE_ENTRY_ID_iobroker_javascript__remoteEntry_js-CeYV4czE.js} +2 -2
  14. package/admin/custom/assets/Components-CGiJE5Zh.js +1 -0
  15. package/admin/custom/assets/ConfigCustomJavascriptSet__loadShare___mf_0_emotion_mf_1_react__loadShare__.mjs-sg07E3QA.js +1 -0
  16. package/admin/custom/assets/ConfigCustomJavascriptSet__loadShare___mf_0_emotion_mf_1_styled__loadShare__.mjs-CwjI4A-J.js +1 -0
  17. package/admin/custom/assets/{ConfigCustomJavascriptSet__loadShare__react__loadShare__.mjs-0SjJESw9.js → ConfigCustomJavascriptSet__loadShare__react__loadShare__.mjs-BW9Qbxx5.js} +1 -1
  18. package/admin/custom/assets/{ConfigCustomJavascriptSet__loadShare__react__loadShare__.mjs_commonjs-proxy-CInLgMWW.js → ConfigCustomJavascriptSet__loadShare__react__loadShare__.mjs_commonjs-proxy-2W2BYT2K.js} +1 -1
  19. package/admin/custom/assets/{ConfigCustomJavascriptSet__loadShare__react_mf_2_dom__loadShare__.mjs_commonjs-proxy-CorOEig8.js → ConfigCustomJavascriptSet__loadShare__react_mf_2_dom__loadShare__.mjs_commonjs-proxy-D8zcZsJo.js} +1 -1
  20. package/admin/custom/assets/DefaultPropsProvider-CfKHPa-G.js +1 -0
  21. package/admin/custom/assets/{TextField-DNzklBSl.js → TextField-CxPfV5Sr.js} +12 -12
  22. package/admin/custom/assets/{ThemeProvider-B4G3jidj.js → ThemeProvider-DSTCckLJ.js} +4 -4
  23. package/admin/custom/assets/{createSvgIcon-ZdrQ8zSU.js → createSvgIcon-b2B2NI3g.js} +1 -1
  24. package/admin/custom/assets/createTheme-MsdB8zt7.js +8 -0
  25. package/admin/custom/assets/defaultTheme-CmVP-QNF.js +1 -0
  26. package/admin/custom/assets/emotion-cache.browser.esm-BgJfNZJN.js +1 -0
  27. package/admin/custom/assets/emotion-react.browser.esm-chZAeh_-.js +1 -0
  28. package/admin/custom/assets/emotion-serialize.esm-BUa21YfQ.js +1 -0
  29. package/admin/custom/assets/emotion-styled.browser.esm-Yi13plpH.js +1 -0
  30. package/admin/custom/assets/emotion-use-insertion-effect-with-fallbacks.browser.esm-CEPeBbh9.js +1 -0
  31. package/admin/custom/assets/extends-CF3RwP-h.js +1 -0
  32. package/admin/custom/assets/index-17vbGv00.js +1 -0
  33. package/admin/custom/assets/{index-iRHaQfi-.js → index-B5DqCD_a.js} +1 -1
  34. package/admin/custom/assets/{index-DNKzDki_.js → index-B6NrrORQ.js} +159 -159
  35. package/admin/custom/assets/index-CT5PwRfY.js +964 -0
  36. package/admin/custom/assets/{index-YBvFYuP6.js → index-CzRngQHl.js} +1 -1
  37. package/admin/custom/assets/index-DLNtxshy.js +1 -0
  38. package/admin/custom/assets/{index-NItS-TWp.js → index-DTbSiEk2.js} +1 -1
  39. package/admin/custom/assets/{index-Dp7A3XHd.js → index-DV76KW3-.js} +4 -4
  40. package/admin/custom/assets/{index-D0iTiVVG.js → index-DlQbqHom.js} +1 -1
  41. package/admin/custom/assets/{index-BptSiKBK.js → index-FTmk0JJx.js} +5 -5
  42. package/admin/custom/assets/{jsx-runtime-DfVU3AZG.js → jsx-runtime-BplTyRFk.js} +1 -1
  43. package/admin/custom/assets/localSharedImportMap-ChjFDKsq.js +1 -0
  44. package/admin/custom/assets/useTheme-BV4xOYIL.js +1 -0
  45. package/admin/custom/assets/useTheme-ldICnlmf.js +1 -0
  46. package/admin/custom/assets/{virtualExposes-jSvr-kK3.js → virtualExposes-D402kVGY.js} +1 -1
  47. package/admin/custom/assets/{virtual_mf-REMOTE_ENTRY_ID_ConfigCustomJavascriptSet__customComponents_js-Br3IlnqO.js → virtual_mf-REMOTE_ENTRY_ID_ConfigCustomJavascriptSet__customComponents_js-ClQrb2n1.js} +2 -2
  48. package/admin/custom/customComponents.js +1 -1
  49. package/admin/google-blockly/own/field_oid.js +151 -2
  50. package/admin/i18n/es.json +77 -77
  51. package/admin/mf-manifest.json +1 -1
  52. package/admin/remoteEntry.js +1 -1
  53. package/admin/tab.html +10 -8
  54. package/build/lib/debugger.js +1 -1
  55. package/build/lib/debugger.js.map +1 -1
  56. package/build/lib/eventObj.js +31 -14
  57. package/build/lib/eventObj.js.map +1 -1
  58. package/build/lib/inspect.js +3 -3
  59. package/build/lib/inspect.js.map +1 -1
  60. package/build/lib/sandbox.js +336 -119
  61. package/build/lib/sandbox.js.map +1 -1
  62. package/build/lib/scheduler.js +18 -13
  63. package/build/lib/scheduler.js.map +1 -1
  64. package/build/main.js +282 -149
  65. package/build/main.js.map +1 -1
  66. package/build/types.d.ts +18 -6
  67. package/io-package.json +27 -14
  68. package/package.json +11 -11
  69. package/admin/assets/ScriptEditorVanillaMonaco-CaFyKuJ2.js +0 -3
  70. package/admin/assets/index-SHs7_wDh.js +0 -1
  71. package/admin/assets/stateHoverProvider-CGChzV5W.js +0 -3
  72. package/admin/custom/assets/Components-B9ZyjeRI.js +0 -1
  73. package/admin/custom/assets/DefaultPropsProvider-CGb21NGY.js +0 -1
  74. package/admin/custom/assets/createTheme-Wd4QxQE0.js +0 -8
  75. package/admin/custom/assets/defaultTheme-F2iJF2G2.js +0 -1
  76. package/admin/custom/assets/index-AZB1OHDH.js +0 -1
  77. package/admin/custom/assets/index-C0RmLaAj.js +0 -1
  78. package/admin/custom/assets/index-sozs10Ij.js +0 -960
  79. package/admin/custom/assets/localSharedImportMap-BxT0VwTO.js +0 -1
  80. package/admin/custom/assets/useTheme-BfhDdtAr.js +0 -1
  81. package/admin/custom/assets/useTheme-DWZ6jNyq.js +0 -1
  82. /package/admin/custom/assets/{index-huvZQhZw.js → index-D_5kZmfw.js} +0 -0
  83. /package/admin/custom/assets/{index-CmJ8HzSc.js → index-DpJ18x6R.js} +0 -0
  84. /package/admin/custom/assets/{leaflet-src-k2rCV-Ix.js → leaflet-src-B472P7Y1.js} +0 -0
@@ -33,6 +33,8 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.isExactId = isExactId;
37
+ exports.removeFromDispatchIndex = removeFromDispatchIndex;
36
38
  exports.sandBox = sandBox;
37
39
  const jsonataMod = __importStar(require("jsonata"));
38
40
  const adapter_core_1 = require("@iobroker/adapter-core");
@@ -43,6 +45,61 @@ const eventObjMod = __importStar(require("./eventObj"));
43
45
  const patternCompareFunctions_1 = require("./patternCompareFunctions");
44
46
  const SCRIPT_CODE_MARKER = 'script.js.';
45
47
  const pattern2RegEx = adapter_core_1.commonTools.pattern2RegEx;
48
+ // Pre-compiled RegExp constants for formatTimeDiff – avoids recompiling on every call
49
+ const FTD_TEST_D = /(?<!\\)[DTД]/;
50
+ const FTD_REPL_DD = /(?<!\\)(?:DD|TT|ДД)/g;
51
+ const FTD_REPL_D = /(?<!\\)[DTД]/g;
52
+ const FTD_TEST_H = /(?<!\\)[hSч]/;
53
+ const FTD_REPL_HH = /(?<!\\)(?:hh|SS|чч)/g;
54
+ const FTD_REPL_H = /(?<!\\)[hSч]/g;
55
+ const FTD_TEST_M = /(?<!\\)[mм]/;
56
+ const FTD_REPL_MM = /(?<!\\)(?:mm|мм)/g;
57
+ const FTD_REPL_M = /(?<!\\)[mм]/g;
58
+ const FTD_TEST_S = /(?<!\\)[sс]/;
59
+ const FTD_REPL_SS = /(?<!\\)(?:ss|сс)/g;
60
+ const FTD_REPL_S = /(?<!\\)[sс]/g;
61
+ const FTD_UNESCAPE = [
62
+ [/\\([DTД])/g, '$1'],
63
+ [/\\([hSч])/g, '$1'],
64
+ [/\\([mм])/g, '$1'],
65
+ [/\\([sс])/g, '$1'],
66
+ ];
67
+ /** Maximum number of cached selector RegExps before FIFO eviction kicks in */
68
+ const _SELECTOR_REGEXP_CACHE_MAX = 1000;
69
+ /** Cache for selector-string → RegExp to avoid recompiling identical patterns (FIFO-bounded) */
70
+ const _selectorRegExpCache = new Map();
71
+ /** Monotonically increasing handler-ID counter – avoids Date.now()+random collisions */
72
+ let _handlerIdCounter = 1;
73
+ /** Returns true when patId is a plain exact state-ID (no wildcards, no RegExp notation). */
74
+ function isExactId(patId) {
75
+ return (!!patId && typeof patId === 'string' && !patId.includes('*') && !patId.includes('?') && !patId.startsWith('/'));
76
+ }
77
+ /**
78
+ * Removes a subscription from the O(1) dispatch index (subscriptionsMap / subscriptionsWildcard).
79
+ * Must use the same exact-id classification as the subscribe side (isExactId),
80
+ * otherwise entries leak in the structure that was not searched.
81
+ */
82
+ function removeFromDispatchIndex(ctx, sub) {
83
+ const patId = sub.pattern?.id;
84
+ if (isExactId(patId)) {
85
+ const bucket = ctx.subscriptionsMap.get(patId);
86
+ if (bucket) {
87
+ const pos = bucket.indexOf(sub);
88
+ if (pos !== -1) {
89
+ bucket.splice(pos, 1);
90
+ }
91
+ if (bucket.length === 0) {
92
+ ctx.subscriptionsMap.delete(patId);
93
+ }
94
+ }
95
+ }
96
+ else {
97
+ const wPos = ctx.subscriptionsWildcard.indexOf(sub);
98
+ if (wPos !== -1) {
99
+ ctx.subscriptionsWildcard.splice(wPos, 1);
100
+ }
101
+ }
102
+ }
46
103
  function sandBox(script, name, verbose, debug, context) {
47
104
  const consts = constsMod;
48
105
  const words = wordsMod;
@@ -90,7 +147,8 @@ function sandBox(script, name, verbose, debug, context) {
90
147
  });
91
148
  }
92
149
  else {
93
- adapter.getForeignStates(pattern, (_err, _states) => _states && Object.keys(_states).forEach(id => (states[id] = _states[id])));
150
+ // IO-3: Object.assign instead of Object.keys().forEach() no temporary keys array
151
+ adapter.getForeignStates(pattern, (_err, _states) => _states && Object.assign(states, _states));
94
152
  }
95
153
  }
96
154
  else {
@@ -210,14 +268,24 @@ function sandBox(script, name, verbose, debug, context) {
210
268
  * @param str The selector string to transform into a regular expression
211
269
  */
212
270
  function selectorStringToRegExp(str) {
271
+ const cached = _selectorRegExpCache.get(str);
272
+ if (cached) {
273
+ return cached;
274
+ }
213
275
  const startsWithWildcard = str[0] === '*';
214
276
  const endsWithWildcard = str[str.length - 1] === '*';
215
- // Sanitize the selector, so it is safe to use in a RegEx
216
- // Taken from https://stackoverflow.com/a/3561711/10179833 but modified
217
- // since * has a special meaning in our selector and should not be escaped
218
277
  // eslint-disable-next-line no-useless-escape
219
- str = str.replace(/[-\/\\^$+?.()|[\]{}]/g, '\\$&').replace(/\*/g, '.*');
220
- return new RegExp((startsWithWildcard ? '' : '^') + str + (endsWithWildcard ? '' : '$'));
278
+ const escaped = str.replace(/[-\/\\^$+?.()|[\]{}]/g, '\\$&').replace(/\*/g, '.*');
279
+ const re = new RegExp((startsWithWildcard ? '' : '^') + escaped + (endsWithWildcard ? '' : '$'));
280
+ // FIFO eviction: drop the oldest entry once the cache exceeds its limit
281
+ if (_selectorRegExpCache.size >= _SELECTOR_REGEXP_CACHE_MAX) {
282
+ const oldestKey = _selectorRegExpCache.keys().next().value;
283
+ if (oldestKey !== undefined) {
284
+ _selectorRegExpCache.delete(oldestKey);
285
+ }
286
+ }
287
+ _selectorRegExpCache.set(str, re);
288
+ return re;
221
289
  }
222
290
  /**
223
291
  * Adds a regular expression for selectors targeting the state ID
@@ -482,9 +550,21 @@ function sandBox(script, name, verbose, debug, context) {
482
550
  if (!adapter.config.subscribe && context.interimStateValues[id]) {
483
551
  // if the state is changed, we will compare it with interimStateValues
484
552
  const oldState = context.interimStateValues[id];
485
- const attrs = Object.keys(stateAsObject).filter(attr => attr !== 'ts' && stateAsObject[attr] !== undefined);
486
- if (!attrs.every(attr => stateAsObject[attr] ===
487
- oldState[attr])) {
553
+ // IO-1: for…in instead of Object.keys().filter().every() no temporary array per call
554
+ let stateHasChanged = false;
555
+ for (const attr in stateAsObject) {
556
+ if (attr === 'ts') {
557
+ continue;
558
+ }
559
+ if (stateAsObject[attr] === undefined) {
560
+ continue;
561
+ }
562
+ if (stateAsObject[attr] !== oldState[attr]) {
563
+ stateHasChanged = true;
564
+ break;
565
+ }
566
+ }
567
+ if (stateHasChanged) {
488
568
  // state is changed for sure, and we will call setForeignState
489
569
  // and store new state to interimStateValues
490
570
  context.interimStateValues[id] = stateAsObject;
@@ -728,17 +808,17 @@ function sandBox(script, name, verbose, debug, context) {
728
808
  if (!context.channels || !context.devices) {
729
809
  context.channels = {};
730
810
  context.devices = {};
731
- for (const _id in objects) {
732
- if (Object.prototype.hasOwnProperty.call(objects, _id) && objects[_id].type === 'state') {
811
+ for (const _id of Object.keys(objects)) {
812
+ if (objects[_id].type === 'state') {
733
813
  const parts = _id.split('.');
734
814
  parts.pop();
735
815
  const chn = parts.join('.');
736
816
  parts.pop();
737
817
  const dev = parts.join('.');
738
- context.devices[dev] = context.devices[dev] || [];
739
- context.devices[dev].push(_id);
740
- context.channels[chn] = context.channels[chn] || [];
741
- context.channels[chn].push(_id);
818
+ context.devices[dev] = context.devices[dev] || new Set();
819
+ context.devices[dev].add(_id);
820
+ context.channels[chn] = context.channels[chn] || new Set();
821
+ context.channels[chn].add(_id);
742
822
  }
743
823
  }
744
824
  }
@@ -746,8 +826,8 @@ function sandBox(script, name, verbose, debug, context) {
746
826
  if (name === 'schedule') {
747
827
  if (!context.schedules) {
748
828
  context.schedules = [];
749
- for (const _id in objects) {
750
- if (Object.prototype.hasOwnProperty.call(objects, _id) && objects[_id].type === 'schedule') {
829
+ for (const _id of Object.keys(objects)) {
830
+ if (objects[_id].type === 'schedule') {
751
831
  context.schedules.push(_id);
752
832
  }
753
833
  }
@@ -851,7 +931,7 @@ function sandBox(script, name, verbose, debug, context) {
851
931
  }
852
932
  // retrieve the state ID collection for all remaining channels
853
933
  res = res
854
- .map(id => channels[id])
934
+ .map(id => [...(channels[id] || [])])
855
935
  // and flatten the array to get only the state IDs
856
936
  .reduce((acc, next) => acc.concat(next), []);
857
937
  // now filter out those that don't match every ID selector for the state ID
@@ -884,7 +964,7 @@ function sandBox(script, name, verbose, debug, context) {
884
964
  }
885
965
  // retrieve the state ID collection for all remaining devices
886
966
  res = res
887
- .map(id => devices[id])
967
+ .map(id => [...(devices[id] || [])])
888
968
  // and flatten the array to get only the state IDs
889
969
  .reduce((acc, next) => acc.concat(next), []);
890
970
  // now filter out those that don't match every ID selector for the state ID
@@ -921,12 +1001,8 @@ function sandBox(script, name, verbose, debug, context) {
921
1001
  res = res.filter(id => applyEnumSelectors(id));
922
1002
  }
923
1003
  }
924
- const resUnique = [];
925
- for (let i = 0; i < res.length; i++) {
926
- if (!resUnique.includes(res[i])) {
927
- resUnique.push(res[i]);
928
- }
929
- }
1004
+ // IO-2: O(1) deduplication via Set instead of O(n²) resUnique.includes()
1005
+ const resUnique = [...new Set(res)];
930
1006
  for (let i = 0; i < resUnique.length; i++) {
931
1007
  result[i] = resUnique[i];
932
1008
  }
@@ -1078,7 +1154,7 @@ function sandBox(script, name, verbose, debug, context) {
1078
1154
  sandbox.log(`Invalid callback for onLog`, 'warn');
1079
1155
  return 0;
1080
1156
  }
1081
- const handler = { id: Date.now() + Math.floor(Math.random() * 10000), cb: callback, sandbox, severity };
1157
+ const handler = { id: _handlerIdCounter++, cb: callback, sandbox, severity };
1082
1158
  context.logSubscriptions[sandbox.scriptName] = context.logSubscriptions[sandbox.scriptName] || [];
1083
1159
  context.logSubscriptions[sandbox.scriptName].push(handler);
1084
1160
  context.updateLogSubscriptions();
@@ -1355,8 +1431,8 @@ function sandBox(script, name, verbose, debug, context) {
1355
1431
  if (oPattern?.id && Array.isArray(oPattern.id)) {
1356
1432
  const result = [];
1357
1433
  for (let t = 0; t < oPattern.id.length; t++) {
1358
- const pa = JSON.parse(JSON.stringify(oPattern));
1359
- pa.id = oPattern.id[t];
1434
+ // IO-4: Spread instead of JSON.parse(JSON.stringify()) – no deep clone needed (only primitive fields)
1435
+ const pa = { ...oPattern, id: oPattern.id[t] };
1360
1436
  result.push(sandbox.subscribe(pa, callbackOrChangeTypeOrId, value));
1361
1437
  }
1362
1438
  return result;
@@ -1431,10 +1507,11 @@ function sandBox(script, name, verbose, debug, context) {
1431
1507
  const _adapter = `system.adapter.${a}`;
1432
1508
  if (objects[_adapter] && objects[_adapter].common && objects[_adapter].common.subscribable) {
1433
1509
  const alive = `system.adapter.${a}.alive`;
1434
- context.adapterSubs[alive] = context.adapterSubs[alive] || [];
1435
- const subExists = context.adapterSubs[alive].filter(sub => sub === oPattern.id).length > 0;
1510
+ context.adapterSubs[alive] = context.adapterSubs[alive] || new Set();
1511
+ // Set.has() is O(1) and automatically prevents duplicates
1512
+ const subExists = context.adapterSubs[alive].has(oPattern.id);
1436
1513
  if (!subExists) {
1437
- context.adapterSubs[alive].push(oPattern.id);
1514
+ context.adapterSubs[alive].add(oPattern.id);
1438
1515
  adapter.sendTo(a, 'subscribe', oPattern.id);
1439
1516
  }
1440
1517
  }
@@ -1445,6 +1522,16 @@ function sandBox(script, name, verbose, debug, context) {
1445
1522
  subscribePattern(script, oPattern.id);
1446
1523
  subs.patternCompareFunctions = getPatternCompareFunctions(oPattern);
1447
1524
  context.subscriptions.push(subs);
1525
+ // O(1) dispatch index: exact string IDs go into the map, everything else into the wildcard array
1526
+ if (isExactId(oPattern.id)) {
1527
+ if (!context.subscriptionsMap.has(oPattern.id)) {
1528
+ context.subscriptionsMap.set(oPattern.id, []);
1529
+ }
1530
+ context.subscriptionsMap.get(oPattern.id).push(subs);
1531
+ }
1532
+ else {
1533
+ context.subscriptionsWildcard.push(subs);
1534
+ }
1448
1535
  if (oPattern.enumName || oPattern.enumId) {
1449
1536
  context.isEnums = true;
1450
1537
  }
@@ -1452,13 +1539,13 @@ function sandBox(script, name, verbose, debug, context) {
1452
1539
  },
1453
1540
  getSubscriptions: function () {
1454
1541
  const result = {};
1455
- for (let s = 0; s < context.subscriptions.length; s++) {
1456
- result[context.subscriptions[s].pattern.id] =
1457
- result[context.subscriptions[s].pattern.id] || [];
1458
- result[context.subscriptions[s].pattern.id].push({
1459
- name: context.subscriptions[s].name,
1460
- pattern: context.subscriptions[s].pattern,
1461
- });
1542
+ // Iterate over the O(1) dispatch index instead of the flat array
1543
+ for (const [id, bucket] of context.subscriptionsMap) {
1544
+ result[id] = bucket.map(s => ({ name: s.name, pattern: s.pattern }));
1545
+ }
1546
+ for (const s of context.subscriptionsWildcard) {
1547
+ const key = s.pattern.id;
1548
+ (result[key] ??= []).push({ name: s.name, pattern: s.pattern });
1462
1549
  }
1463
1550
  if (sandbox.verbose) {
1464
1551
  sandbox.log(`getSubscriptions() => ${JSON.stringify(result)}`, 'info');
@@ -1491,8 +1578,8 @@ function sandBox(script, name, verbose, debug, context) {
1491
1578
  if (objects[_adapter]?.common?.subscribable) {
1492
1579
  const a = `${parts[0]}.${parts[1]}`;
1493
1580
  const alive = `system.adapter.${a}.alive`;
1494
- context.adapterSubs[alive] = context.adapterSubs[alive] || [];
1495
- context.adapterSubs[alive].push(id);
1581
+ context.adapterSubs[alive] = context.adapterSubs[alive] || new Set();
1582
+ context.adapterSubs[alive].add(id);
1496
1583
  if (sandbox.verbose) {
1497
1584
  sandbox.log(`adapterSubscribe: ${a} - ${id}`, 'info');
1498
1585
  }
@@ -1517,8 +1604,11 @@ function sandBox(script, name, verbose, debug, context) {
1517
1604
  if ((0, tools_1.isObject)(idOrObject)) {
1518
1605
  for (let i = context.subscriptions.length - 1; i >= 0; i--) {
1519
1606
  if (context.subscriptions[i] === idOrObject) {
1520
- unsubscribePattern(script, context.subscriptions[i].pattern.id);
1607
+ const sub = context.subscriptions[i];
1608
+ unsubscribePattern(script, sub.pattern.id);
1521
1609
  context.subscriptions.splice(i, 1);
1610
+ // Remove from O(1) dispatch structures
1611
+ removeFromDispatchIndex(context, sub);
1522
1612
  sandbox.__engine.__subscriptions--;
1523
1613
  return true;
1524
1614
  }
@@ -1529,8 +1619,11 @@ function sandBox(script, name, verbose, debug, context) {
1529
1619
  for (let i = context.subscriptions.length - 1; i >= 0; i--) {
1530
1620
  if (context.subscriptions[i].name === name && context.subscriptions[i].pattern.id === idOrObject) {
1531
1621
  deleted++;
1532
- unsubscribePattern(script, context.subscriptions[i].pattern.id);
1622
+ const sub = context.subscriptions[i];
1623
+ unsubscribePattern(script, sub.pattern.id);
1533
1624
  context.subscriptions.splice(i, 1);
1625
+ // Remove from O(1) dispatch structures
1626
+ removeFromDispatchIndex(context, sub);
1534
1627
  sandbox.__engine.__subscriptions--;
1535
1628
  }
1536
1629
  }
@@ -1542,7 +1635,7 @@ function sandBox(script, name, verbose, debug, context) {
1542
1635
  return sandbox.subscribe(pattern, callbackOrChangeTypeOrId, value);
1543
1636
  },
1544
1637
  onEnumMembers: function (enumId, callback) {
1545
- if (enums.includes(enumId)) {
1638
+ if (enums.has(enumId)) {
1546
1639
  const subscriptions = {};
1547
1640
  const init = () => {
1548
1641
  const obj = objects[enumId];
@@ -1557,7 +1650,8 @@ function sandBox(script, name, verbose, debug, context) {
1557
1650
  }
1558
1651
  // Subscribe to all members of enum
1559
1652
  for (const objId of members) {
1560
- if (!Object.keys(subscriptions).includes(objId)) {
1653
+ // IO-6: `in` operator instead of Object.keys().includes() – O(1) instead of O(n)
1654
+ if (!(objId in subscriptions)) {
1561
1655
  if (objects?.[objId]?.type === 'state') {
1562
1656
  // Just subscribe to states
1563
1657
  subscriptions[objId] = sandbox.subscribe(objId, callback); // TODO: more features
@@ -2059,11 +2153,13 @@ function sandBox(script, name, verbose, debug, context) {
2059
2153
  const schedules = context.scheduler?.getList() || [];
2060
2154
  if (allScripts) {
2061
2155
  Object.keys(context.scripts).forEach(name => context.scripts[name].schedules &&
2062
- context.scripts[name].schedules.forEach(s => schedules.push(JSON.parse(JSON.stringify(s._ioBroker)))));
2156
+ // IO-8: Spread instead of JSON.parse(JSON.stringify()) – _ioBroker has only primitive fields
2157
+ context.scripts[name].schedules.forEach(s => schedules.push({ ...s._ioBroker })));
2063
2158
  }
2064
2159
  else {
2065
2160
  script.schedules &&
2066
- script.schedules.forEach(s => schedules.push(JSON.parse(JSON.stringify(s._ioBroker))));
2161
+ // IO-8: Spread instead of JSON.parse(JSON.stringify())
2162
+ script.schedules.forEach(s => schedules.push({ ...s._ioBroker }));
2067
2163
  }
2068
2164
  return schedules;
2069
2165
  },
@@ -2100,10 +2196,22 @@ function sandBox(script, name, verbose, debug, context) {
2100
2196
  if (timers[id]) {
2101
2197
  sandbox.verbose &&
2102
2198
  sandbox.log(`setStateDelayed: clear ${timers[id].length} running timers`, 'info');
2199
+ // collect affected scriptNames before deleting
2200
+ const affectedScripts = new Set(timers[id].map(e => e.scriptName));
2103
2201
  for (let i = 0; i < timers[id].length; i++) {
2104
2202
  clearTimeout(timers[id][i].t);
2105
2203
  }
2106
2204
  delete timers[id];
2205
+ // update timersByScript reverse-index for all affected scripts
2206
+ for (const scriptName of affectedScripts) {
2207
+ const scriptSet = context.timersByScript.get(scriptName);
2208
+ if (scriptSet) {
2209
+ scriptSet.delete(id);
2210
+ if (!scriptSet.size) {
2211
+ context.timersByScript.delete(scriptName);
2212
+ }
2213
+ }
2214
+ }
2107
2215
  }
2108
2216
  else {
2109
2217
  if (sandbox.verbose) {
@@ -2130,18 +2238,36 @@ function sandBox(script, name, verbose, debug, context) {
2130
2238
  if (timers[_id]) {
2131
2239
  // optimisation
2132
2240
  if (timers[_id].length === 1) {
2241
+ const scriptName = timers[_id][0].scriptName;
2133
2242
  delete timers[_id];
2243
+ // update timersByScript reverse-index
2244
+ const scriptSet = context.timersByScript.get(scriptName);
2245
+ if (scriptSet) {
2246
+ scriptSet.delete(_id);
2247
+ if (!scriptSet.size) {
2248
+ context.timersByScript.delete(scriptName);
2249
+ }
2250
+ }
2134
2251
  }
2135
2252
  else {
2136
2253
  for (let t = 0; t < timers[_id].length; t++) {
2137
2254
  if (timers[_id][t].id === _timerId) {
2255
+ const scriptName = timers[_id][t].scriptName;
2138
2256
  timers[_id].splice(t, 1);
2257
+ if (!timers[_id].length) {
2258
+ delete timers[_id];
2259
+ // update timersByScript reverse-index
2260
+ const scriptSet = context.timersByScript.get(scriptName);
2261
+ if (scriptSet) {
2262
+ scriptSet.delete(_id);
2263
+ if (!scriptSet.size) {
2264
+ context.timersByScript.delete(scriptName);
2265
+ }
2266
+ }
2267
+ }
2139
2268
  break;
2140
2269
  }
2141
2270
  }
2142
- if (!timers[_id].length) {
2143
- delete timers[_id];
2144
- }
2145
2271
  }
2146
2272
  }
2147
2273
  }, delay, context.timerId, id, state, isAck);
@@ -2161,6 +2287,11 @@ function sandBox(script, name, verbose, debug, context) {
2161
2287
  : isAck,
2162
2288
  scriptName: name,
2163
2289
  });
2290
+ // Keep reverse-index in sync for O(1) cleanup in stopScript
2291
+ if (!context.timersByScript.has(name)) {
2292
+ context.timersByScript.set(name, new Set());
2293
+ }
2294
+ context.timersByScript.get(name).add(id);
2164
2295
  return context.timerId;
2165
2296
  },
2166
2297
  clearStateDelayed: function (id, timerId) {
@@ -2172,14 +2303,18 @@ function sandBox(script, name, verbose, debug, context) {
2172
2303
  sandbox.log(`clearStateDelayed(id=${id}, timerId=${timerId})`, 'info');
2173
2304
  }
2174
2305
  if (timers[id]) {
2306
+ // collect scriptNames of entries that will be removed
2307
+ const removedScripts = new Set();
2175
2308
  for (let i = timers[id].length - 1; i >= 0; i--) {
2176
2309
  if (timerId === undefined || timers[id][i].id === timerId) {
2310
+ const clearedTimerId = timers[id][i].id;
2311
+ removedScripts.add(timers[id][i].scriptName);
2177
2312
  clearTimeout(timers[id][i].t);
2178
2313
  if (timerId !== undefined) {
2179
2314
  timers[id].splice(i, 1);
2180
2315
  }
2181
2316
  if (sandbox.verbose) {
2182
- sandbox.log(`clearStateDelayed: clear timer ${timers[id][i].id}`, 'info');
2317
+ sandbox.log(`clearStateDelayed: clear timer ${clearedTimerId}`, 'info');
2183
2318
  }
2184
2319
  }
2185
2320
  }
@@ -2191,6 +2326,25 @@ function sandBox(script, name, verbose, debug, context) {
2191
2326
  delete timers[id];
2192
2327
  }
2193
2328
  }
2329
+ // IO-7: keep the timersByScript reverse-index in sync. For every script whose
2330
+ // timer(s) we just removed, drop `id` from its set – unless that script still has
2331
+ // another timer for this state (other scripts' timers may keep timers[id] alive).
2332
+ if (removedScripts.size) {
2333
+ const remaining = timers[id]; // undefined if the whole entry was deleted
2334
+ for (const scriptName of removedScripts) {
2335
+ const stateIds = context.timersByScript.get(scriptName);
2336
+ if (!stateIds) {
2337
+ continue;
2338
+ }
2339
+ if (remaining?.some(e => e.scriptName === scriptName)) {
2340
+ continue;
2341
+ }
2342
+ stateIds.delete(id);
2343
+ if (!stateIds.size) {
2344
+ context.timersByScript.delete(scriptName);
2345
+ }
2346
+ }
2347
+ }
2194
2348
  return true;
2195
2349
  }
2196
2350
  return false;
@@ -2388,7 +2542,7 @@ function sandBox(script, name, verbose, debug, context) {
2388
2542
  }
2389
2543
  let result;
2390
2544
  try {
2391
- result = JSON.parse(JSON.stringify(objects[id]));
2545
+ result = objects[id] ? structuredClone(objects[id]) : undefined;
2392
2546
  }
2393
2547
  catch (err) {
2394
2548
  void adapter.setState(`scriptProblem.${name.substring(SCRIPT_CODE_MARKER.length)}`, {
@@ -2413,9 +2567,9 @@ function sandBox(script, name, verbose, debug, context) {
2413
2567
  }
2414
2568
  if (enumName) {
2415
2569
  const e = eventObj.getObjectEnumsSync(context, id);
2416
- const obj = JSON.parse(JSON.stringify(objects[id]));
2417
- obj.enumIds = JSON.parse(JSON.stringify(e.enumIds));
2418
- obj.enumNames = JSON.parse(JSON.stringify(e.enumNames));
2570
+ const obj = structuredClone(objects[id]);
2571
+ obj.enumIds = structuredClone(e.enumIds);
2572
+ obj.enumNames = structuredClone(e.enumNames);
2419
2573
  if (typeof enumName === 'string') {
2420
2574
  const r = new RegExp(`^enum\\.${enumName}\\.`);
2421
2575
  for (let i = obj.enumIds.length - 1; i >= 0; i--) {
@@ -2431,7 +2585,7 @@ function sandBox(script, name, verbose, debug, context) {
2431
2585
  }
2432
2586
  let result;
2433
2587
  try {
2434
- result = JSON.parse(JSON.stringify(objects[id]));
2588
+ result = structuredClone(objects[id]);
2435
2589
  }
2436
2590
  catch (err) {
2437
2591
  void adapter.setState(`scriptProblem.${name.substring(SCRIPT_CODE_MARKER.length)}`, {
@@ -2489,11 +2643,11 @@ function sandBox(script, name, verbose, debug, context) {
2489
2643
  getEnums: function (enumName) {
2490
2644
  const result = [];
2491
2645
  const r = enumName ? new RegExp(`^enum\\.${enumName}\\.`) : false;
2492
- for (let i = 0; i < enums.length; i++) {
2493
- if (!r || r.test(enums[i])) {
2494
- const common = objects[enums[i]].common || {};
2646
+ for (const enumId of enums) {
2647
+ if (!r || r.test(enumId)) {
2648
+ const common = objects[enumId].common || {};
2495
2649
  result.push({
2496
- id: enums[i],
2650
+ id: enumId,
2497
2651
  members: common.members || [],
2498
2652
  name: common.name || '',
2499
2653
  });
@@ -2502,7 +2656,7 @@ function sandBox(script, name, verbose, debug, context) {
2502
2656
  if (sandbox.verbose) {
2503
2657
  sandbox.log(`getEnums(enumName=${enumName}) => ${JSON.stringify(result)}`, 'info');
2504
2658
  }
2505
- return JSON.parse(JSON.stringify(result));
2659
+ return structuredClone(result);
2506
2660
  },
2507
2661
  createAlias: function (name, alias, forceCreation, common, native, callback) {
2508
2662
  if (typeof native === 'function') {
@@ -3154,19 +3308,39 @@ function sandBox(script, name, verbose, debug, context) {
3154
3308
  adapter.sendTo(_adapter, cmd, msg, cbFunc, options);
3155
3309
  }
3156
3310
  else {
3157
- // Send it to all instances
3158
- context.adapter.getObjectView('system', 'instance', { startkey: `system.adapter.${_adapter}.`, endkey: `system.adapter.${_adapter}.\u9999` }, options, (err, res) => {
3159
- if (err || !res) {
3160
- sandbox.log(`sendTo failed: ${err?.message}`, 'error');
3161
- return;
3162
- }
3163
- const instances = res.rows.map(item => item.id.substring('system.adapter.'.length));
3164
- instances.forEach(instance => {
3311
+ // IO-9: Cache sendTo broadcast instance list – avoid repeated getObjectView DB calls
3312
+ const cached = context.sendToInstanceCache.get(_adapter);
3313
+ if (cached) {
3314
+ cached.forEach(instance => {
3165
3315
  sandbox.verbose &&
3166
- sandbox.log(`sendTo(instance=${instance}, cmd=${cmd}, msg=${JSON.stringify(msg)}, hasCallback=${typeof callback === 'function'})`, 'info');
3316
+ sandbox.log(`sendTo(instance=${instance}, cmd=${cmd}, msg=${JSON.stringify(msg)}, hasCallback=${typeof callback === 'function'}) [cached]`, 'info');
3167
3317
  adapter.sendTo(instance, cmd, msg, cbFunc, options);
3168
3318
  });
3169
- });
3319
+ }
3320
+ else {
3321
+ // Send it to all instances
3322
+ context.adapter.getObjectView('system', 'instance', { startkey: `system.adapter.${_adapter}.`, endkey: `system.adapter.${_adapter}.\u9999` }, options, (err, res) => {
3323
+ if (err || !res) {
3324
+ sandbox.log(`sendTo failed: ${err?.message}`, 'error');
3325
+ return;
3326
+ }
3327
+ const instances = res.rows.map(item => item.id.substring('system.adapter.'.length));
3328
+ // Store in cache for subsequent calls (invalidated on system.adapter.* object change).
3329
+ // LRU eviction: keep the map bounded to avoid unbounded memory growth.
3330
+ if (context.sendToInstanceCache.size >= 200) {
3331
+ const firstKey = context.sendToInstanceCache.keys().next().value;
3332
+ if (firstKey !== undefined) {
3333
+ context.sendToInstanceCache.delete(firstKey);
3334
+ }
3335
+ }
3336
+ context.sendToInstanceCache.set(_adapter, instances);
3337
+ instances.forEach(instance => {
3338
+ sandbox.verbose &&
3339
+ sandbox.log(`sendTo(instance=${instance}, cmd=${cmd}, msg=${JSON.stringify(msg)}, hasCallback=${typeof callback === 'function'})`, 'info');
3340
+ adapter.sendTo(instance, cmd, msg, cbFunc, options);
3341
+ });
3342
+ });
3343
+ }
3170
3344
  }
3171
3345
  },
3172
3346
  sendto: function (_adapter, cmd, msg, callback) {
@@ -3230,7 +3404,8 @@ function sandBox(script, name, verbose, debug, context) {
3230
3404
  errorInCallback(err);
3231
3405
  }
3232
3406
  }, ms);
3233
- script.intervals.push(int);
3407
+ // IO-10: Set.add() – O(1) instead of Array.push()
3408
+ script.intervals.add(int);
3234
3409
  if (sandbox.verbose) {
3235
3410
  sandbox.log(`setInterval(ms=${ms})`, 'info');
3236
3411
  }
@@ -3240,13 +3415,13 @@ function sandBox(script, name, verbose, debug, context) {
3240
3415
  return null;
3241
3416
  },
3242
3417
  clearInterval: function (id) {
3243
- const pos = script.intervals.indexOf(id);
3244
- if (pos !== -1) {
3418
+ // IO-10: Set.has/delete – O(1) instead of Array.indexOf+splice O(n)
3419
+ if (script.intervals.has(id)) {
3245
3420
  if (sandbox.verbose) {
3246
3421
  sandbox.log('clearInterval() => cleared', 'info');
3247
3422
  }
3248
3423
  clearInterval(id);
3249
- script.intervals.splice(pos, 1);
3424
+ script.intervals.delete(id);
3250
3425
  }
3251
3426
  else {
3252
3427
  if (sandbox.verbose) {
@@ -3257,11 +3432,8 @@ function sandBox(script, name, verbose, debug, context) {
3257
3432
  setTimeout: function (callback, ms, ...args) {
3258
3433
  if (typeof callback === 'function') {
3259
3434
  const to = setTimeout(() => {
3260
- // Remove timeout from the list
3261
- const pos = script.timeouts.indexOf(to);
3262
- if (pos !== -1) {
3263
- script.timeouts.splice(pos, 1);
3264
- }
3435
+ // IO-10: Set.delete O(1) instead of Array.indexOf+splice O(n)
3436
+ script.timeouts.delete(to);
3265
3437
  try {
3266
3438
  callback.call(sandbox, ...args);
3267
3439
  }
@@ -3272,20 +3444,21 @@ function sandBox(script, name, verbose, debug, context) {
3272
3444
  if (sandbox.verbose) {
3273
3445
  sandbox.log(`setTimeout(ms=${ms})`, 'info');
3274
3446
  }
3275
- script.timeouts.push(to);
3447
+ // IO-10: Set.add – O(1) instead of Array.push
3448
+ script.timeouts.add(to);
3276
3449
  return to;
3277
3450
  }
3278
3451
  sandbox.log(`Invalid callback for setTimeout! - ${typeof callback}`, 'error');
3279
3452
  return null;
3280
3453
  },
3281
3454
  clearTimeout: function (id) {
3282
- const pos = script.timeouts.indexOf(id);
3283
- if (pos !== -1) {
3455
+ // IO-10: Set.has/delete – O(1) instead of Array.indexOf+splice O(n)
3456
+ if (script.timeouts.has(id)) {
3284
3457
  if (sandbox.verbose) {
3285
3458
  sandbox.log('clearTimeout() => cleared', 'info');
3286
3459
  }
3287
3460
  clearTimeout(id);
3288
- script.timeouts.splice(pos, 1);
3461
+ script.timeouts.delete(id);
3289
3462
  }
3290
3463
  else {
3291
3464
  if (sandbox.verbose) {
@@ -3632,50 +3805,45 @@ function sandBox(script, name, verbose, debug, context) {
3632
3805
  const day = 24 * hour;
3633
3806
  const neg = diff < 0;
3634
3807
  diff = Math.abs(diff);
3635
- if (/(?<!\\)(D|T|Д)/.test(text)) {
3808
+ if (FTD_TEST_D.test(text)) {
3636
3809
  const days = Math.floor(diff / day);
3637
- text = text
3638
- .replace(/(?<!\\)(DD|TT|ДД)/g, days.toString().padStart(2, '0'))
3639
- .replace(/(?<!\\)(D|T|Д)/g, days.toString());
3810
+ text = text.replace(FTD_REPL_DD, days.toString().padStart(2, '0')).replace(FTD_REPL_D, days.toString());
3640
3811
  if (sandbox.verbose) {
3641
3812
  sandbox.log(`formatTimeDiff(format=${format}, text=${text}, days=${days})`, 'debug');
3642
3813
  }
3643
3814
  diff -= days * day;
3644
3815
  }
3645
- if (/(?<!\\)(h|S|ч)/.test(text)) {
3816
+ if (FTD_TEST_H.test(text)) {
3646
3817
  const hours = Math.floor(diff / hour);
3647
3818
  text = text
3648
- .replace(/(?<!\\)(hh|SS|чч)/g, hours.toString().padStart(2, '0'))
3649
- .replace(/(?<!\\)(h|S|ч)/g, hours.toString());
3819
+ .replace(FTD_REPL_HH, hours.toString().padStart(2, '0'))
3820
+ .replace(FTD_REPL_H, hours.toString());
3650
3821
  if (sandbox.verbose) {
3651
3822
  sandbox.log(`formatTimeDiff(format=${format}, text=${text}, hours=${hours})`, 'debug');
3652
3823
  }
3653
3824
  diff -= hours * hour;
3654
3825
  }
3655
- if (/(?<!\\)(m|м)/.test(text)) {
3826
+ if (FTD_TEST_M.test(text)) {
3656
3827
  const minutes = Math.floor(diff / minute);
3657
3828
  text = text
3658
- .replace(/(?<!\\)(mm|мм)/g, minutes.toString().padStart(2, '0'))
3659
- .replace(/(?<!\\)(m|м)/g, minutes.toString());
3829
+ .replace(FTD_REPL_MM, minutes.toString().padStart(2, '0'))
3830
+ .replace(FTD_REPL_M, minutes.toString());
3660
3831
  if (sandbox.verbose) {
3661
3832
  sandbox.log(`formatTimeDiff(format=${format}, text=${text}, minutes=${minutes})`, 'debug');
3662
3833
  }
3663
3834
  diff -= minutes * minute;
3664
3835
  }
3665
- if (/(?<!\\)(s|с)/.test(text)) {
3836
+ if (FTD_TEST_S.test(text)) {
3666
3837
  const seconds = Math.floor(diff / second);
3667
3838
  text = text
3668
- .replace(/(?<!\\)(ss|сс)/g, seconds.toString().padStart(2, '0'))
3669
- .replace(/(?<!\\)(s|с)/g, seconds.toString());
3839
+ .replace(FTD_REPL_SS, seconds.toString().padStart(2, '0'))
3840
+ .replace(FTD_REPL_S, seconds.toString());
3670
3841
  sandbox.verbose &&
3671
3842
  sandbox.log(`formatTimeDiff(format=${format}, text=${text}, seconds=${seconds})`, 'debug');
3672
- // diff -= seconds * second; // no milliseconds
3673
3843
  }
3674
- text = text
3675
- .replace(/\\(D|T|Д)/g, '$1')
3676
- .replace(/\\(h|S|ч)/g, '$1')
3677
- .replace(/\\(m|м)/g, '$1')
3678
- .replace(/\\(s|с)/g, '$1');
3844
+ for (const [pattern, replacement] of FTD_UNESCAPE) {
3845
+ text = text.replace(pattern, replacement);
3846
+ }
3679
3847
  if (sandbox.verbose) {
3680
3848
  sandbox.log(`formatTimeDiff(format=${format}, text=${text})`, 'debug');
3681
3849
  }
@@ -4299,7 +4467,7 @@ function sandBox(script, name, verbose, debug, context) {
4299
4467
  context.messageBusHandlers[sandbox.scriptName] = context.messageBusHandlers[sandbox.scriptName] || {};
4300
4468
  context.messageBusHandlers[sandbox.scriptName][messageName] =
4301
4469
  context.messageBusHandlers[sandbox.scriptName][messageName] || [];
4302
- const handler = { id: Date.now() + Math.floor(Math.random() * 10000), cb: callback, sandbox };
4470
+ const handler = { id: _handlerIdCounter++, cb: callback, sandbox };
4303
4471
  context.messageBusHandlers[sandbox.scriptName][messageName].push(handler);
4304
4472
  sandbox.__engine.__subscriptionsMessage += 1;
4305
4473
  if (sandbox.__engine.__subscriptionsMessage %
@@ -4314,23 +4482,18 @@ function sandBox(script, name, verbose, debug, context) {
4314
4482
  let found = false;
4315
4483
  if (ctx) {
4316
4484
  if (typeof idOrName === 'number') {
4317
- for (const messageName in ctx) {
4318
- if (Object.prototype.hasOwnProperty.call(ctx, messageName)) {
4319
- for (let i = 0; i < ctx[messageName].length; i++) {
4320
- if (ctx[messageName][i].id === idOrName) {
4321
- ctx[messageName].splice(i, 1);
4322
- if (!ctx[messageName].length) {
4323
- delete ctx[messageName];
4324
- sandbox.__engine.__subscriptionsMessage--;
4325
- }
4326
- found = true;
4327
- break;
4485
+ outer: for (const messageName of Object.keys(ctx)) {
4486
+ for (let i = 0; i < ctx[messageName].length; i++) {
4487
+ if (ctx[messageName][i].id === idOrName) {
4488
+ ctx[messageName].splice(i, 1);
4489
+ if (!ctx[messageName].length) {
4490
+ delete ctx[messageName];
4491
+ sandbox.__engine.__subscriptionsMessage--;
4328
4492
  }
4493
+ found = true;
4494
+ break outer;
4329
4495
  }
4330
4496
  }
4331
- if (found) {
4332
- break;
4333
- }
4334
4497
  }
4335
4498
  }
4336
4499
  else if (idOrName && ctx[idOrName]) {
@@ -4401,6 +4564,11 @@ function sandBox(script, name, verbose, debug, context) {
4401
4564
  }
4402
4565
  adapter.subscribeForeignObjects(pattern);
4403
4566
  context.subscriptionsObject.push(subs);
4567
+ // Keep O(1) dispatch map in sync
4568
+ if (!context.subscriptionsObjectMap.has(pattern)) {
4569
+ context.subscriptionsObjectMap.set(pattern, []);
4570
+ }
4571
+ context.subscriptionsObjectMap.get(pattern).push(subs);
4404
4572
  return subs;
4405
4573
  },
4406
4574
  unsubscribeObject: function (subObject) {
@@ -4418,6 +4586,17 @@ function sandBox(script, name, verbose, debug, context) {
4418
4586
  if (context.subscriptionsObject[i] === subObject) {
4419
4587
  adapter.unsubscribeForeignObjects(subObject.pattern);
4420
4588
  context.subscriptionsObject.splice(i, 1);
4589
+ // Keep O(1) dispatch map in sync
4590
+ const mapSubs = context.subscriptionsObjectMap.get(subObject.pattern);
4591
+ if (mapSubs) {
4592
+ const pos = mapSubs.indexOf(subObject);
4593
+ if (pos !== -1) {
4594
+ mapSubs.splice(pos, 1);
4595
+ }
4596
+ if (!mapSubs.length) {
4597
+ context.subscriptionsObjectMap.delete(subObject.pattern);
4598
+ }
4599
+ }
4421
4600
  sandbox.__engine.__subscriptionsObject--;
4422
4601
  return true;
4423
4602
  }
@@ -4428,6 +4607,17 @@ function sandBox(script, name, verbose, debug, context) {
4428
4607
  context.subscriptionsObject[i].pattern === subObject.pattern) {
4429
4608
  deleted++;
4430
4609
  adapter.unsubscribeForeignObjects(subObject.pattern);
4610
+ // Keep O(1) dispatch map in sync
4611
+ const mapSubsP = context.subscriptionsObjectMap.get(subObject.pattern);
4612
+ if (mapSubsP) {
4613
+ const pos = mapSubsP.indexOf(context.subscriptionsObject[i]);
4614
+ if (pos !== -1) {
4615
+ mapSubsP.splice(pos, 1);
4616
+ }
4617
+ if (!mapSubsP.length) {
4618
+ context.subscriptionsObjectMap.delete(subObject.pattern);
4619
+ }
4620
+ }
4431
4621
  context.subscriptionsObject.splice(i, 1);
4432
4622
  sandbox.__engine.__subscriptionsObject--;
4433
4623
  }
@@ -4552,11 +4742,33 @@ function sandBox(script, name, verbose, debug, context) {
4552
4742
  if (sandbox.verbose) {
4553
4743
  sandbox.log(`extendObject(id=${id}, obj=${JSON.stringify(obj)})`, 'info');
4554
4744
  }
4745
+ let objClone;
4746
+ try {
4747
+ objClone = structuredClone(obj);
4748
+ }
4749
+ catch (err) {
4750
+ // e.g. DataCloneError when the object contains functions
4751
+ void adapter.setState(`scriptProblem.${name.substring(SCRIPT_CODE_MARKER.length)}`, {
4752
+ val: true,
4753
+ ack: true,
4754
+ c: 'extendObject',
4755
+ });
4756
+ sandbox.log(`Object "${id}" can't be copied: ${JSON.stringify(err)}`, 'error');
4757
+ if (typeof callback === 'function') {
4758
+ try {
4759
+ callback.call(sandbox, new Error(`Object "${id}" can't be copied`));
4760
+ }
4761
+ catch (err) {
4762
+ errorInCallback(err);
4763
+ }
4764
+ }
4765
+ return;
4766
+ }
4555
4767
  if (callback) {
4556
- adapter.extendForeignObject(id, JSON.parse(JSON.stringify(obj)), callback);
4768
+ adapter.extendForeignObject(id, objClone, callback);
4557
4769
  }
4558
4770
  else {
4559
- void adapter.extendForeignObject(id, JSON.parse(JSON.stringify(obj)));
4771
+ void adapter.extendForeignObject(id, objClone);
4560
4772
  }
4561
4773
  }
4562
4774
  };
@@ -4612,6 +4824,11 @@ function sandBox(script, name, verbose, debug, context) {
4612
4824
  writable: false,
4613
4825
  });
4614
4826
  }
4827
+ // Shallow-freeze the console object exposed to scripts to prevent its methods from being overwritten
4828
+ // Note: __engine must NOT be frozen because its counter properties are mutated at runtime
4829
+ if (sandbox.console && typeof sandbox.console === 'object') {
4830
+ Object.freeze(sandbox.console);
4831
+ }
4615
4832
  return sandbox;
4616
4833
  }
4617
4834
  //# sourceMappingURL=sandbox.js.map