smart-home-engine 1.20.6 → 1.20.8

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.
@@ -1,4 +1,4 @@
1
- import{m as O}from"./monaco-langs-Dt8a9eeo.js";import{t as I}from"./index-D-DESdgj.js";/*!-----------------------------------------------------------------------------
1
+ import{m as O}from"./monaco-langs-Dt8a9eeo.js";import{t as I}from"./index-t_EkMUG4.js";/*!-----------------------------------------------------------------------------
2
2
  * Copyright (c) Microsoft Corporation. All rights reserved.
3
3
  * Version: 0.52.2(404545bded1df6ffa41ea0af4e8ddb219018c6c1)
4
4
  * Released under the MIT license
@@ -172,10 +172,10 @@
172
172
  }
173
173
  })();
174
174
  </script>
175
- <script type="module" crossorigin src="/assets/index-D-DESdgj.js"></script>
175
+ <script type="module" crossorigin src="/assets/index-t_EkMUG4.js"></script>
176
176
  <link rel="modulepreload" crossorigin href="/assets/monaco-langs-Dt8a9eeo.js">
177
177
  <link rel="stylesheet" crossorigin href="/assets/monaco-langs-DyX1CsEw.css">
178
- <link rel="stylesheet" crossorigin href="/assets/index-B4M4MeEk.css">
178
+ <link rel="stylesheet" crossorigin href="/assets/index-C3njyEHY.css">
179
179
  </head>
180
180
  <body>
181
181
  <div id="app"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smart-home-engine",
3
- "version": "1.20.6",
3
+ "version": "1.20.8",
4
4
  "description": "Node.js based script runner for use in MQTT based Smart Home environments",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
package/src/index.js CHANGED
@@ -303,9 +303,32 @@ let _sentinelValue = null; // unique value for this boot's sentinel
303
303
  const _STARTUP_TIMEOUT_MS = 10000; // ms to wait for broker before starting anyway
304
304
  const _SENTINEL_TIMEOUT_MS = config.sentinelTimeout; // ms to wait for sentinel after connecting
305
305
 
306
- function startOnce(reason) {
307
- if (_started) return;
306
+ // Script start waits on two gates: MQTT retained state (sentinel) and — when
307
+ // the Matter controller is enabled — controller startup, so scripts using
308
+ // she.matter.* at top level don't fail with "Matter controller not started".
309
+ let _mqttGate = false;
310
+ let _matterGate = !config.matterStorage; // pre-open when matter is disabled
311
+
312
+ function _maybeStart() {
313
+ if (_started || !_mqttGate || !_matterGate) {
314
+ if (!_started && _mqttGate && !_matterGate) {
315
+ log.info('waiting for matter controller before starting scripts');
316
+ }
317
+ return;
318
+ }
308
319
  _started = true;
320
+ start();
321
+ }
322
+
323
+ function matterGateOpen() {
324
+ if (_matterGate) return;
325
+ _matterGate = true;
326
+ _maybeStart();
327
+ }
328
+
329
+ function startOnce(reason) {
330
+ if (_started || _mqttGate) return;
331
+ _mqttGate = true;
309
332
  if (_startupTimeout) {
310
333
  clearTimeout(_startupTimeout);
311
334
  _startupTimeout = null;
@@ -316,7 +339,7 @@ function startOnce(reason) {
316
339
  }
317
340
  if (reason) log.info(reason);
318
341
  broadcast({ type: 'mqtt:status', ready: true, connected });
319
- start();
342
+ _maybeStart();
320
343
  }
321
344
 
322
345
  // Wire up the MQTT API: pass the state store and a getter for the live MQTT client.
@@ -605,9 +628,21 @@ if (config.matterStorage) {
605
628
  matterStoragePath = ensureStorageDir('matter');
606
629
  }
607
630
  log.info('matter controller starting, storage:', matterStoragePath);
608
- matterController.init(matterStoragePath, log, broadcast).catch((err) => {
609
- log.error('matter controller init failed:', err.message, err.stack);
610
- });
631
+ // Scripts wait for the controller (see _maybeStart) — but never forever:
632
+ // a hanging start must not block the whole script engine.
633
+ const _matterStartTimeout = setTimeout(() => {
634
+ if (!_started) log.warn('matter controller not ready after 30s — starting scripts anyway');
635
+ matterGateOpen();
636
+ }, 30000);
637
+ matterController
638
+ .init(matterStoragePath, log, broadcast)
639
+ .catch((err) => {
640
+ log.error('matter controller init failed:', err.message, err.stack);
641
+ })
642
+ .finally(() => {
643
+ clearTimeout(_matterStartTimeout);
644
+ matterGateOpen();
645
+ });
611
646
  } else {
612
647
  log.warn('matter controller disabled — set matterStorage in config.json to enable');
613
648
  }
@@ -626,7 +661,10 @@ if (config.url) {
626
661
  startOnce();
627
662
  }, _STARTUP_TIMEOUT_MS);
628
663
  } else {
629
- start();
664
+ // No broker configured — open the MQTT gate directly (scripts may still
665
+ // wait for the Matter controller, see _maybeStart).
666
+ _mqttGate = true;
667
+ _maybeStart();
630
668
  }
631
669
 
632
670
  function stateChange(topic, state, oldState, msg) {
@@ -1084,42 +1122,33 @@ function runScript(script, name, _origin) {
1084
1122
  if (modules[md]) {
1085
1123
  return modules[md];
1086
1124
  }
1087
- try {
1088
- let result;
1089
- if (md.match(/^\.\//) || md.match(/^\.\.\//)) {
1090
- // Relative import — resolve from the script's own directory
1091
- const tmp = './' + path.relative(__dirname, path.join(scriptDir, md));
1092
- she.debug('require', tmp);
1093
- result = require(tmp);
1094
- } else {
1095
- // Absolute import — try ~/.she/node_modules/ first (user-installed),
1096
- // then fall back to engine's own require (builtins + engine deps).
1097
- try {
1098
- result = _userRequire(md);
1099
- she.debug('require (user)', md);
1100
- } catch {
1101
- const localMod = path.join(scriptDir, 'node_modules', md, 'package.json');
1102
- if (fs.existsSync(localMod)) {
1103
- result = require(path.join(scriptDir, 'node_modules', md));
1104
- she.debug('require (local)', md);
1105
- } else {
1106
- result = require(md);
1107
- she.debug('require', md);
1108
- }
1125
+ // Failures throw (Cannot find module, syntax errors in the required
1126
+ // file, …) and thereby stop the requiring script — a script must not
1127
+ // silently continue with an undefined module.
1128
+ let result;
1129
+ if (md.match(/^\.\//) || md.match(/^\.\.\//)) {
1130
+ // Relative import — resolve from the script's own directory
1131
+ she.debug('require', md);
1132
+ result = require(path.resolve(scriptDir, md));
1133
+ } else {
1134
+ // Absolute import — try ~/.she/node_modules/ first (user-installed),
1135
+ // then fall back to engine's own require (builtins + engine deps).
1136
+ try {
1137
+ result = _userRequire(md);
1138
+ she.debug('require (user)', md);
1139
+ } catch {
1140
+ const localMod = path.join(scriptDir, 'node_modules', md, 'package.json');
1141
+ if (fs.existsSync(localMod)) {
1142
+ result = require(path.join(scriptDir, 'node_modules', md));
1143
+ she.debug('require (local)', md);
1144
+ } else {
1145
+ result = require(md);
1146
+ she.debug('require', md);
1109
1147
  }
1110
1148
  }
1111
- modules[md] = result;
1112
- return result;
1113
- } catch (err) {
1114
- const lines = err.stack.split('\n');
1115
- const stack = [];
1116
- lines.forEach((line) => {
1117
- if (!line.match(/module\.js:/)) {
1118
- stack.push(line);
1119
- }
1120
- });
1121
- log.error(name + ': ' + stack);
1122
1149
  }
1150
+ modules[md] = result;
1151
+ return result;
1123
1152
  },
1124
1153
 
1125
1154
  console: {
@@ -1174,7 +1203,15 @@ function runScript(script, name, _origin) {
1174
1203
 
1175
1204
  scriptDomain.run(() => {
1176
1205
  log.debug(logLabel, 'running');
1177
- script.runInContext(context);
1206
+ try {
1207
+ script.runInContext(context);
1208
+ } catch (e) {
1209
+ // Contain top-level script errors: without this, the exception
1210
+ // unwinds through the caller (e.g. the watcher batch loading many
1211
+ // scripts in one tick) and aborts loading of subsequent scripts.
1212
+ // Route through the domain handler for the usual attributed log.
1213
+ scriptDomain.emit('error', e);
1214
+ }
1178
1215
  });
1179
1216
 
1180
1217
  // Log a summary of what was registered — symmetric with the unload summary
@@ -18,6 +18,13 @@ const COMMISSION_TIMEOUT_MS = 120000;
18
18
  /** @type {import('@matter/main').ServerNode | null} */
19
19
  let _server = null;
20
20
  let _log = null;
21
+ // Rate-limit state for repeating matter.js error lines (B6)
22
+ let _lastMatterErr = { line: '', ts: 0, suppressed: 0 };
23
+
24
+ /** Depth-limited, cycle-safe rendering of arbitrary throwables/objects. */
25
+ function _inspect(v) {
26
+ return require('util').inspect(v, { depth: 6, breakLength: 160 });
27
+ }
21
28
  /** @type {((msg: object) => void) | null} */
22
29
  let _broadcast = null;
23
30
 
@@ -115,9 +122,70 @@ async function init(storagePath, log, broadcastFn) {
115
122
  Logger.format = LogFormat.PLAIN;
116
123
  Logger.level = LogLevel.INFO;
117
124
  Logger.destinations.default.write = (text, message) => {
118
- if (message.level >= LogLevel.ERROR) _log.error('matter.js:', text);
119
- else if (message.level === LogLevel.WARN) _log.warn('matter.js:', text);
120
- else _log.debug('matter.js:', text);
125
+ // she's log adds its own timestamp — drop matter.js's embedded one
126
+ let line = text.replace(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}\s*/, '');
127
+ if (message.level >= LogLevel.ERROR) {
128
+ // PLAIN formatting renders some throwables as "{}" — recover the
129
+ // detail from the structured message values (B6).
130
+ if (/(\{\}|\[object \w+\])\s*$/.test(line)) {
131
+ const details = (message.values ?? [])
132
+ .filter((v) => v !== null && typeof v === 'object')
133
+ .map((v) => _inspect(v))
134
+ .filter((d) => d && d !== '{}');
135
+ if (details.length) line += ' — ' + details.join(' — ');
136
+ }
137
+ // Rate-limit identical repeating error lines (e.g. a failing
138
+ // reconnect loop): suppress repeats within 30 s, keep a heartbeat.
139
+ const now = Date.now();
140
+ if (line === _lastMatterErr.line && now - _lastMatterErr.ts < 30000) {
141
+ _lastMatterErr.ts = now;
142
+ _lastMatterErr.suppressed++;
143
+ if (_lastMatterErr.suppressed % 20 !== 0) return;
144
+ _log.error(`matter.js: (error repeated ${_lastMatterErr.suppressed} times) ${line}`);
145
+ return;
146
+ }
147
+ if (_lastMatterErr.suppressed % 20 !== 0) {
148
+ _log.error(`matter.js: (previous error repeated ${_lastMatterErr.suppressed} more times)`);
149
+ }
150
+ _lastMatterErr = { line, ts: now, suppressed: 0 };
151
+ _log.error('matter.js:', line);
152
+ } else if (message.level === LogLevel.WARN) {
153
+ _log.warn('matter.js:', line);
154
+ } else {
155
+ _log.debug('matter.js:', line);
156
+ }
157
+ };
158
+
159
+ // matter.js's default unhandled-error reporter logs the bare value, which
160
+ // renders as "{}" for non-Error throwables and drops cause / AggregateError
161
+ // details. The hook is explicitly replaceable — report full detail (B6).
162
+ //
163
+ // matter.js wires this to process.uncaughtExceptionMonitor, which fires for
164
+ // EVERY uncaught exception in the process — matter internals, sandbox
165
+ // script errors, and she's own code alike. Attribute by stack:
166
+ // - sandbox script errors are skipped entirely (she's per-script domain
167
+ // handles and logs them with correct script attribution);
168
+ // - only stacks that actually contain matter.js frames get the
169
+ // "matter.js:" label — everything else is reported neutrally, since it
170
+ // may just as well be a bug in she itself.
171
+ Logger.reportUnhandledError = (error) => {
172
+ try {
173
+ const stack = typeof error?.stack === 'string' ? error.stack : '';
174
+ if (stack) {
175
+ if (stack.includes('Script.runInContext') || stack.includes('node:vm')) return;
176
+ try {
177
+ const config = require('../config');
178
+ const dirs = config.dir ? (Array.isArray(config.dir) ? config.dir : [config.dir]) : [];
179
+ if (dirs.some((d) => d && stack.includes(d))) return;
180
+ } catch {
181
+ /* config unavailable — fall through to logging */
182
+ }
183
+ }
184
+ const fromMatter = stack.includes('/@matter/') || stack.includes('\\@matter\\') || stack.includes('@project-chip');
185
+ _log.error(fromMatter ? 'matter.js: unhandled error:' : 'unhandled error:', _inspect(error));
186
+ } catch {
187
+ /* never throw from the error reporter */
188
+ }
121
189
  };
122
190
 
123
191
  // Configure storage before anything else touches StorageService