smart-home-engine 0.16.0 → 0.16.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.
- package/README.md +4 -0
- package/dist/web/assets/{index-Cqfuxa_i.js → index-CwiZOgEn.js} +76 -71
- package/dist/web/assets/{index-DcqBg4oJ.css → index-Dn5qOvtB.css} +1 -1
- package/dist/web/assets/jsonMode-CXWNuhmR.js +15 -0
- package/dist/web/assets/{monaco-langs-Decdf6BV.js → monaco-langs-BW2J83t5.js} +250 -269
- package/dist/web/assets/{tsMode-B7q_C6Fy.js → tsMode-DOSh-ywG.js} +1 -1
- package/dist/web/index.html +4 -3
- package/dist/web/monacoeditorwork/json.worker.bundle.js +21320 -0
- package/package.json +1 -1
- package/src/index.js +27 -6
- package/src/sandbox/matter-sandbox.js +6 -3
- package/src/sandbox/shedb-sandbox.js +5 -2
- package/src/sandbox/stdlib.js +3 -3
- package/src/web/scripts-api.js +7 -2
- package/src/web/server.js +53 -2
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -189,8 +189,8 @@ function sunScheduleEvent(obj, shift) {
|
|
|
189
189
|
// call the callback immediately!
|
|
190
190
|
obj.domain.bind(obj.callback)();
|
|
191
191
|
} else {
|
|
192
|
-
// Schedule the event
|
|
193
|
-
scheduler.scheduleJob(event, obj.domain.bind(obj.callback));
|
|
192
|
+
// Schedule the event and track the job so it can be cancelled on script unload
|
|
193
|
+
obj._job = scheduler.scheduleJob(event, obj.domain.bind(obj.callback));
|
|
194
194
|
}
|
|
195
195
|
}
|
|
196
196
|
}
|
|
@@ -659,7 +659,10 @@ function runScript(script, name, origin) {
|
|
|
659
659
|
if (options.random) {
|
|
660
660
|
_myJobs.push(
|
|
661
661
|
scheduler.scheduleJob(pattern, () => {
|
|
662
|
-
|
|
662
|
+
// Track the random-delay timer so it is cancelled on unload
|
|
663
|
+
// if the job fires in the same tick as the script is reloaded.
|
|
664
|
+
const id = setTimeout(scriptDomain.bind(callback), (parseFloat(options.random) || 0) * 1000 * Math.random());
|
|
665
|
+
_myTimers.add(id);
|
|
663
666
|
}),
|
|
664
667
|
);
|
|
665
668
|
} else {
|
|
@@ -935,8 +938,19 @@ function runScript(script, name, origin) {
|
|
|
935
938
|
};
|
|
936
939
|
|
|
937
940
|
const scriptName = path.basename(name, path.extname(name));
|
|
941
|
+
// she.setTimeout / she.clearTimeout — tracked versions for use by stdlib and
|
|
942
|
+
// sandbox modules that don't have direct access to the Sandbox context.
|
|
943
|
+
she.setTimeout = (fn, delay, ...args) => {
|
|
944
|
+
const id = setTimeout(fn, delay, ...args);
|
|
945
|
+
_myTimers.add(id);
|
|
946
|
+
return id;
|
|
947
|
+
};
|
|
948
|
+
she.clearTimeout = (id) => {
|
|
949
|
+
_myTimers.delete(id);
|
|
950
|
+
clearTimeout(id);
|
|
951
|
+
};
|
|
938
952
|
sandboxModules.forEach((md) => {
|
|
939
|
-
md(she, { scriptDomain, scriptName });
|
|
953
|
+
md(she, { scriptDomain, scriptName, scriptFile: name });
|
|
940
954
|
});
|
|
941
955
|
|
|
942
956
|
log.debug(name, 'contextifying sandbox');
|
|
@@ -1010,6 +1024,10 @@ function unloadScript(file) {
|
|
|
1010
1024
|
if (mqttEventCallbacks[i]._script === file) mqttEventCallbacks.splice(i, 1);
|
|
1011
1025
|
}
|
|
1012
1026
|
|
|
1027
|
+
// Remove HTTP routes registered by this script via she.api
|
|
1028
|
+
const scriptName = path.basename(file, path.extname(file));
|
|
1029
|
+
require('./web/server').unregisterRoutesByScript(scriptName);
|
|
1030
|
+
|
|
1013
1031
|
// Cancel all node-schedule jobs for this script
|
|
1014
1032
|
const jobs = scriptJobs.get(file);
|
|
1015
1033
|
if (jobs) {
|
|
@@ -1017,9 +1035,12 @@ function unloadScript(file) {
|
|
|
1017
1035
|
scriptJobs.delete(file);
|
|
1018
1036
|
}
|
|
1019
1037
|
|
|
1020
|
-
// Remove sun events belonging to this script
|
|
1038
|
+
// Remove sun events belonging to this script and cancel any pending scheduled job
|
|
1021
1039
|
for (let i = sunEvents.length - 1; i >= 0; i--) {
|
|
1022
|
-
if (sunEvents[i]._script === file)
|
|
1040
|
+
if (sunEvents[i]._script === file) {
|
|
1041
|
+
if (sunEvents[i]._job) sunEvents[i]._job.cancel();
|
|
1042
|
+
sunEvents.splice(i, 1);
|
|
1043
|
+
}
|
|
1023
1044
|
}
|
|
1024
1045
|
|
|
1025
1046
|
// Clear all tracked timers for this script
|
|
@@ -26,7 +26,10 @@
|
|
|
26
26
|
|
|
27
27
|
const controller = require('../matter/controller');
|
|
28
28
|
|
|
29
|
-
module.exports = function (she, { scriptDomain, scriptName }) {
|
|
29
|
+
module.exports = function (she, { scriptDomain, scriptName, scriptFile }) {
|
|
30
|
+
// Use full file path as the tracking key so cleanup() called from
|
|
31
|
+
// unloadScript() (which passes the full path) matches what was registered.
|
|
32
|
+
const trackingKey = scriptFile || scriptName;
|
|
30
33
|
she.matter = {
|
|
31
34
|
/**
|
|
32
35
|
* Subscribe to an attribute change on a paired Matter device.
|
|
@@ -40,7 +43,7 @@ module.exports = function (she, { scriptDomain, scriptName }) {
|
|
|
40
43
|
*/
|
|
41
44
|
sub(nodeId, endpointId, clusterName, attrName, callback) {
|
|
42
45
|
try {
|
|
43
|
-
return controller.subscribeAttribute(
|
|
46
|
+
return controller.subscribeAttribute(trackingKey, nodeId, endpointId, clusterName, attrName, callback);
|
|
44
47
|
} catch (err) {
|
|
45
48
|
scriptDomain.emit('error', err);
|
|
46
49
|
}
|
|
@@ -52,7 +55,7 @@ module.exports = function (she, { scriptDomain, scriptName }) {
|
|
|
52
55
|
* @param {number} listenerId Returned by she.matter.sub()
|
|
53
56
|
*/
|
|
54
57
|
unsub(listenerId) {
|
|
55
|
-
controller.unsubscribe(
|
|
58
|
+
controller.unsubscribe(trackingKey, listenerId);
|
|
56
59
|
},
|
|
57
60
|
|
|
58
61
|
/**
|
|
@@ -17,8 +17,11 @@
|
|
|
17
17
|
|
|
18
18
|
const { getCore, addListener, removeListenersByScript } = require('../web/shedb');
|
|
19
19
|
|
|
20
|
-
module.exports = function (she, { scriptDomain, scriptName }) {
|
|
20
|
+
module.exports = function (she, { scriptDomain, scriptName, scriptFile }) {
|
|
21
21
|
const core = getCore();
|
|
22
|
+
// Use the full file path as the tracking key so cleanup() in index.js
|
|
23
|
+
// (which passes the full path) matches what was registered here.
|
|
24
|
+
const trackingKey = scriptFile || scriptName;
|
|
22
25
|
|
|
23
26
|
// sheDB may not be initialised (--db-path not given or startup still in progress).
|
|
24
27
|
// We expose stubs that are no-ops / return undefined so user scripts don't crash.
|
|
@@ -62,7 +65,7 @@ module.exports = function (she, { scriptDomain, scriptName }) {
|
|
|
62
65
|
if (!core) return;
|
|
63
66
|
// Wrap in script domain so errors don't crash the process
|
|
64
67
|
const wrapped = scriptDomain.bind(callback);
|
|
65
|
-
addListener(pattern, wrapped,
|
|
68
|
+
addListener(pattern, wrapped, trackingKey);
|
|
66
69
|
},
|
|
67
70
|
|
|
68
71
|
/**
|
package/src/sandbox/stdlib.js
CHANGED
|
@@ -88,11 +88,11 @@ module.exports = function (she) {
|
|
|
88
88
|
she.timer = function (src, target, time) {
|
|
89
89
|
she.mqttsub(src, { retain: false }, (topic, val) => {
|
|
90
90
|
if (val) {
|
|
91
|
-
clearTimeout(timeouts[target]);
|
|
91
|
+
she.clearTimeout(timeouts[target]);
|
|
92
92
|
if (!she.getValue(target)) {
|
|
93
93
|
she.setValue(target, 1);
|
|
94
94
|
}
|
|
95
|
-
timeouts[target] = setTimeout(() => {
|
|
95
|
+
timeouts[target] = she.setTimeout(() => {
|
|
96
96
|
if (she.getValue(target)) {
|
|
97
97
|
she.setValue(target, 0);
|
|
98
98
|
}
|
|
@@ -100,7 +100,7 @@ module.exports = function (she) {
|
|
|
100
100
|
}
|
|
101
101
|
});
|
|
102
102
|
|
|
103
|
-
timeouts[target] = setTimeout(() => {
|
|
103
|
+
timeouts[target] = she.setTimeout(() => {
|
|
104
104
|
if (she.getValue(target)) {
|
|
105
105
|
she.setValue(target, 0);
|
|
106
106
|
}
|
package/src/web/scripts-api.js
CHANGED
|
@@ -132,12 +132,17 @@ router.use((req, res) => {
|
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
// DELETE /she/scripts/<path> — delete file
|
|
135
|
+
// DELETE /she/scripts/<path> — delete file or directory (recursive)
|
|
136
136
|
if (method === 'DELETE') {
|
|
137
137
|
const abs = safePath(root, filePath);
|
|
138
138
|
if (!abs) return res.status(400).json({ error: 'Invalid path' });
|
|
139
139
|
try {
|
|
140
|
-
fs.
|
|
140
|
+
const stat = fs.statSync(abs);
|
|
141
|
+
if (stat.isDirectory()) {
|
|
142
|
+
fs.rmSync(abs, { recursive: true });
|
|
143
|
+
} else {
|
|
144
|
+
fs.unlinkSync(abs);
|
|
145
|
+
}
|
|
141
146
|
return res.json({ ok: true });
|
|
142
147
|
} catch (err) {
|
|
143
148
|
if (err.code === 'ENOENT') return res.status(404).json({ error: 'Not found' });
|
package/src/web/server.js
CHANGED
|
@@ -82,8 +82,15 @@ app.use((req, res, next) => {
|
|
|
82
82
|
// Central route registry — key: 'METHOD /api/scriptname/path'
|
|
83
83
|
const registry = new Map();
|
|
84
84
|
|
|
85
|
+
// Per-script Express sub-routers for she.api routes.
|
|
86
|
+
// Each script gets one Router mounted at /api/<scriptName>; the layer reference
|
|
87
|
+
// is kept so it can be spliced out of the app's middleware stack on unload.
|
|
88
|
+
const scriptRouters = new Map(); // scriptName → { router, layer }
|
|
89
|
+
|
|
85
90
|
/**
|
|
86
91
|
* Register an HTTP route. Throws if the same method+path pair is already registered.
|
|
92
|
+
* Routes under /api/<scriptName>/... are grouped into a per-script sub-router so
|
|
93
|
+
* they can all be removed at once when the script is unloaded.
|
|
87
94
|
* @param {'get'|'post'|'put'|'delete'} method
|
|
88
95
|
* @param {string} fullPath - absolute Express path, e.g. '/api/myscript/foo'
|
|
89
96
|
* @param {Function} handler - Express route handler (req, res)
|
|
@@ -94,7 +101,51 @@ function registerRoute(method, fullPath, handler) {
|
|
|
94
101
|
throw new Error(`Route already registered: ${key}`);
|
|
95
102
|
}
|
|
96
103
|
registry.set(key, true);
|
|
97
|
-
|
|
104
|
+
|
|
105
|
+
// Route belongs to a user script — use a per-script sub-router.
|
|
106
|
+
const m = fullPath.match(/^\/api\/([^/]+)(\/.*)?$/);
|
|
107
|
+
if (m) {
|
|
108
|
+
const scriptName = m[1];
|
|
109
|
+
const routePath = m[2] || '/';
|
|
110
|
+
let entry = scriptRouters.get(scriptName);
|
|
111
|
+
if (!entry) {
|
|
112
|
+
const router = express.Router();
|
|
113
|
+
app.use('/api/' + scriptName, router);
|
|
114
|
+
// Capture the layer Express just pushed onto its stack.
|
|
115
|
+
// Express 5 exposes the router via the public `app.router` getter.
|
|
116
|
+
const stack = app.router.stack;
|
|
117
|
+
const layer = stack[stack.length - 1];
|
|
118
|
+
entry = { router, layer };
|
|
119
|
+
scriptRouters.set(scriptName, entry);
|
|
120
|
+
}
|
|
121
|
+
entry.router[method](routePath, handler);
|
|
122
|
+
} else {
|
|
123
|
+
// Fallback for any non-/api/ paths (shouldn't occur in normal usage).
|
|
124
|
+
app[method](fullPath, handler);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Remove all HTTP routes registered by a script and allow re-registration.
|
|
130
|
+
* Called by unloadScript() in index.js on hot-reload.
|
|
131
|
+
* @param {string} scriptName - basename without extension, e.g. 'myscript'
|
|
132
|
+
*/
|
|
133
|
+
function unregisterRoutesByScript(scriptName) {
|
|
134
|
+
const entry = scriptRouters.get(scriptName);
|
|
135
|
+
if (entry) {
|
|
136
|
+
const stack = app.router?.stack;
|
|
137
|
+
if (stack) {
|
|
138
|
+
const idx = stack.indexOf(entry.layer);
|
|
139
|
+
if (idx !== -1) stack.splice(idx, 1);
|
|
140
|
+
}
|
|
141
|
+
scriptRouters.delete(scriptName);
|
|
142
|
+
}
|
|
143
|
+
// Clear registry entries so the routes can be re-registered on reload.
|
|
144
|
+
for (const key of [...registry.keys()]) {
|
|
145
|
+
if (key.includes('/api/' + scriptName + '/') || key.endsWith('/api/' + scriptName)) {
|
|
146
|
+
registry.delete(key);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
98
149
|
}
|
|
99
150
|
|
|
100
151
|
let httpServer = null;
|
|
@@ -145,4 +196,4 @@ function stopServer() {
|
|
|
145
196
|
});
|
|
146
197
|
}
|
|
147
198
|
|
|
148
|
-
module.exports = { app, registerRoute, setStatsProvider, startServer, stopServer };
|
|
199
|
+
module.exports = { app, registerRoute, unregisterRoutesByScript, setStatsProvider, startServer, stopServer };
|