smart-home-engine 0.18.0 → 0.19.0
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/dist/web/assets/index-CtuJWQQl.css +1 -0
- package/dist/web/assets/{index-CygE8hzz.js → index-xPpby8cF.js} +69 -69
- package/dist/web/assets/{tsMode-DJvlYckS.js → tsMode-DJcKcLpg.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/src/index.js +82 -20
- package/src/lib/shedb-core.js +54 -26
- package/src/lib/shedb-worker.js +51 -26
- package/src/web/prompts/she-api-ref.md +0 -1
- package/src/web/scripts-api.js +14 -3
- package/dist/web/assets/index-ZA04d_Jz.css +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{m as O}from"./monaco-langs-BW2J83t5.js";import{t as I}from"./index-
|
|
1
|
+
import{m as O}from"./monaco-langs-BW2J83t5.js";import{t as I}from"./index-xPpby8cF.js";/*!-----------------------------------------------------------------------------
|
|
2
2
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
3
|
* Version: 0.52.2(404545bded1df6ffa41ea0af4e8ddb219018c6c1)
|
|
4
4
|
* Released under the MIT license
|
package/dist/web/index.html
CHANGED
|
@@ -155,10 +155,10 @@
|
|
|
155
155
|
}
|
|
156
156
|
})();
|
|
157
157
|
</script>
|
|
158
|
-
<script type="module" crossorigin src="/assets/index-
|
|
158
|
+
<script type="module" crossorigin src="/assets/index-xPpby8cF.js"></script>
|
|
159
159
|
<link rel="modulepreload" crossorigin href="/assets/monaco-langs-BW2J83t5.js">
|
|
160
160
|
<link rel="stylesheet" crossorigin href="/assets/monaco-langs-DyX1CsEw.css">
|
|
161
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
161
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CtuJWQQl.css">
|
|
162
162
|
</head>
|
|
163
163
|
<body>
|
|
164
164
|
<div id="app"></div>
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -392,7 +392,7 @@ function stateChange(topic, state, oldState, msg) {
|
|
|
392
392
|
const match = mqttWildcards(topic, subs.topic);
|
|
393
393
|
|
|
394
394
|
if (match && typeof options.condition === 'function') {
|
|
395
|
-
if (!options.condition(topic
|
|
395
|
+
if (!options.condition(topic, state.val, state, oldState, msg)) {
|
|
396
396
|
return;
|
|
397
397
|
}
|
|
398
398
|
}
|
|
@@ -418,13 +418,13 @@ function stateChange(topic, state, oldState, msg) {
|
|
|
418
418
|
setTimeout(() => {
|
|
419
419
|
/**
|
|
420
420
|
* @callback subscribeCallback
|
|
421
|
-
* @param {string} topic - the topic that triggered this callback
|
|
421
|
+
* @param {string} topic - the topic that triggered this callback
|
|
422
422
|
* @param {mixed} val - the val property of the new state
|
|
423
423
|
* @param {object} obj - new state - the whole state object (e.g. {"val": true, "ts": 12346345, "lc": 12346345} )
|
|
424
424
|
* @param {object} objPrev - previous state - the whole state object
|
|
425
425
|
* @param {object} msg - the mqtt message as received from MQTT.js
|
|
426
426
|
*/
|
|
427
|
-
subs.callback(topic
|
|
427
|
+
subs.callback(topic, state.val, state, oldState, msg);
|
|
428
428
|
}, delay);
|
|
429
429
|
}
|
|
430
430
|
});
|
|
@@ -564,8 +564,6 @@ function runScript(script, name, origin) {
|
|
|
564
564
|
}
|
|
565
565
|
|
|
566
566
|
if (typeof topic === 'string') {
|
|
567
|
-
topic = topic.replace(/^([^/]+)\/\//, '$1/status/');
|
|
568
|
-
|
|
569
567
|
if (typeof options.condition === 'string') {
|
|
570
568
|
if (options.condition.indexOf('\n') !== -1) {
|
|
571
569
|
throw new Error('options.condition string must be one-line javascript');
|
|
@@ -581,11 +579,11 @@ function runScript(script, name, origin) {
|
|
|
581
579
|
subscriptions.push({ topic, options, callback: typeof callback === 'function' && scriptDomain.bind(callback), _script: name });
|
|
582
580
|
|
|
583
581
|
if (options.retain && store.has('mqtt::' + topic) && typeof callback === 'function') {
|
|
584
|
-
callback(topic
|
|
582
|
+
callback(topic, store.get('mqtt::' + topic), store.getObject('mqtt::' + topic));
|
|
585
583
|
} else if (options.retain && (/\/\+\//.test(topic) || /\+$/.test(topic) || /\+/.test(topic) || topic.endsWith('#')) && typeof callback === 'function') {
|
|
586
584
|
for (const [t, obj] of store.mqttEntries()) {
|
|
587
585
|
if (mqttWildcards(t, topic)) {
|
|
588
|
-
callback(t
|
|
586
|
+
callback(t, obj.val, obj);
|
|
589
587
|
}
|
|
590
588
|
}
|
|
591
589
|
}
|
|
@@ -728,7 +726,6 @@ function runScript(script, name, origin) {
|
|
|
728
726
|
she.mqttpub(topic, val, { retain: false });
|
|
729
727
|
}
|
|
730
728
|
} else {
|
|
731
|
-
topic = topic.replace(/^([^/]+)\/\/(.+)$/, '$1/set/$2');
|
|
732
729
|
she.mqttpub(topic, val, { retain: false });
|
|
733
730
|
}
|
|
734
731
|
},
|
|
@@ -739,7 +736,6 @@ function runScript(script, name, origin) {
|
|
|
739
736
|
* @returns {mixed} the topics value
|
|
740
737
|
*/
|
|
741
738
|
getValue: function Sandbox_getValue(topic) {
|
|
742
|
-
topic = topic.replace(/^([^/]+)\/\/(.+)$/, '$1/status/$2');
|
|
743
739
|
return store.get('mqtt::' + topic);
|
|
744
740
|
},
|
|
745
741
|
|
|
@@ -753,7 +749,6 @@ function runScript(script, name, origin) {
|
|
|
753
749
|
* she.getProp('hm//Bewegungsmelder Keller/MOTION', 'ts');
|
|
754
750
|
*/
|
|
755
751
|
getProp: function Sandbox_getProp(topic, ...props) {
|
|
756
|
-
topic = topic.replace(/^([^/]+)\/\/(.+)$/, '$1/status/$2');
|
|
757
752
|
if (props.length > 0) {
|
|
758
753
|
let tmp = store.getObject('mqtt::' + topic);
|
|
759
754
|
if (typeof tmp === 'undefined') {
|
|
@@ -1143,9 +1138,35 @@ function isLibFile(absFile, scriptRoot) {
|
|
|
1143
1138
|
return false;
|
|
1144
1139
|
}
|
|
1145
1140
|
|
|
1141
|
+
/**
|
|
1142
|
+
* Returns true if a .shedisable-<name> sibling marker exists for the given path.
|
|
1143
|
+
* Works for both files and directories.
|
|
1144
|
+
*/
|
|
1145
|
+
function isDisabledPath(absPath) {
|
|
1146
|
+
const dir = path.dirname(path.resolve(absPath));
|
|
1147
|
+
const name = path.basename(absPath);
|
|
1148
|
+
return fs.existsSync(path.join(dir, `.shedisable-${name}`));
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
/**
|
|
1152
|
+
* Returns true if any ancestor directory of absFile (between it and scriptRoot)
|
|
1153
|
+
* has a .shedisable-<dirname> sibling in its parent.
|
|
1154
|
+
*/
|
|
1155
|
+
function isInDisabledDir(absFile, scriptRoot) {
|
|
1156
|
+
const root = path.resolve(scriptRoot);
|
|
1157
|
+
let dir = path.dirname(path.resolve(absFile));
|
|
1158
|
+
while (dir.length > root.length && dir.startsWith(root)) {
|
|
1159
|
+
if (isDisabledPath(dir)) return true;
|
|
1160
|
+
const parent = path.dirname(dir);
|
|
1161
|
+
if (parent === dir) break;
|
|
1162
|
+
dir = parent;
|
|
1163
|
+
}
|
|
1164
|
+
return false;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1146
1167
|
/**
|
|
1147
1168
|
* Recursively load all user .js scripts from a directory tree.
|
|
1148
|
-
* Files
|
|
1169
|
+
* Files/dirs with a .shedisable-<name> sibling, and files inside .shelib dirs, are skipped.
|
|
1149
1170
|
*/
|
|
1150
1171
|
function loadDirRecursive(dir, scriptRoot) {
|
|
1151
1172
|
let entries;
|
|
@@ -1158,10 +1179,11 @@ function loadDirRecursive(dir, scriptRoot) {
|
|
|
1158
1179
|
entries
|
|
1159
1180
|
.sort((a, b) => a.name.localeCompare(b.name))
|
|
1160
1181
|
.forEach((entry) => {
|
|
1182
|
+
if (entry.name.startsWith('.shedisable-')) return; // skip marker files
|
|
1161
1183
|
const abs = path.join(dir, entry.name);
|
|
1162
1184
|
if (entry.isDirectory()) {
|
|
1163
|
-
loadDirRecursive(abs, scriptRoot);
|
|
1164
|
-
} else if (entry.name.endsWith('.js') && !isLibFile(abs, scriptRoot)) {
|
|
1185
|
+
if (!isDisabledPath(abs)) loadDirRecursive(abs, scriptRoot);
|
|
1186
|
+
} else if (entry.name.endsWith('.js') && !isLibFile(abs, scriptRoot) && !isDisabledPath(abs)) {
|
|
1165
1187
|
loadScript(abs.replace(/\\/g, '/'));
|
|
1166
1188
|
}
|
|
1167
1189
|
});
|
|
@@ -1175,6 +1197,7 @@ function loadDir(dir) {
|
|
|
1175
1197
|
const dirWatcher = chokidar.watch(dir, {
|
|
1176
1198
|
ignored: (p, stats) => {
|
|
1177
1199
|
const name = path.basename(p);
|
|
1200
|
+
if (name.startsWith('.shedisable-')) return false; // always watch disable markers
|
|
1178
1201
|
return stats?.isFile() && !name.endsWith('.js') && name !== '.shelib';
|
|
1179
1202
|
},
|
|
1180
1203
|
persistent: true,
|
|
@@ -1186,17 +1209,49 @@ function loadDir(dir) {
|
|
|
1186
1209
|
filePath = filePath.replace(/\\/g, '/');
|
|
1187
1210
|
const basename = path.basename(filePath);
|
|
1188
1211
|
|
|
1189
|
-
// .
|
|
1212
|
+
// .shedisable-<name> marker changes - hot-unload or hot-reload the target
|
|
1213
|
+
if (basename.startsWith('.shedisable-')) {
|
|
1214
|
+
const targetName = basename.slice('.shedisable-'.length);
|
|
1215
|
+
const targetPath = path.join(path.dirname(filePath), targetName).replace(/\\/g, '/');
|
|
1216
|
+
if (event === 'add') {
|
|
1217
|
+
if (targetName.endsWith('.js')) {
|
|
1218
|
+
if (scripts[targetPath]) {
|
|
1219
|
+
log.info(targetPath, 'disabled. unloading.');
|
|
1220
|
+
unloadScript(targetPath);
|
|
1221
|
+
}
|
|
1222
|
+
} else {
|
|
1223
|
+
const absTarget = path.resolve(targetPath);
|
|
1224
|
+
Object.keys(scripts).forEach((scriptFile) => {
|
|
1225
|
+
if (path.resolve(scriptFile).startsWith(absTarget + path.sep)) {
|
|
1226
|
+
log.info(scriptFile, 'directory disabled. unloading.');
|
|
1227
|
+
unloadScript(scriptFile);
|
|
1228
|
+
}
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
} else if (event === 'unlink') {
|
|
1232
|
+
if (targetName.endsWith('.js')) {
|
|
1233
|
+
if (fs.existsSync(targetPath) && !isLibFile(targetPath, dir) && !scripts[targetPath]) {
|
|
1234
|
+
log.info(targetPath, 're-enabled. loading.');
|
|
1235
|
+
loadScript(targetPath);
|
|
1236
|
+
}
|
|
1237
|
+
} else {
|
|
1238
|
+
if (fs.existsSync(targetPath)) loadDirRecursive(targetPath, path.resolve(dir));
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
// .shelib marker changes - warn only, manual restart required
|
|
1190
1245
|
if (basename === '.shelib') {
|
|
1191
1246
|
if (event === 'add') {
|
|
1192
|
-
log.warn(filePath, 'library marker added
|
|
1247
|
+
log.warn(filePath, 'library marker added - .js files in this directory will no longer load as scripts after daemon restart');
|
|
1193
1248
|
} else if (event === 'unlink') {
|
|
1194
|
-
log.warn(filePath, 'library marker removed
|
|
1249
|
+
log.warn(filePath, 'library marker removed - .js files in this directory will load as scripts after daemon restart');
|
|
1195
1250
|
}
|
|
1196
1251
|
return;
|
|
1197
1252
|
}
|
|
1198
1253
|
|
|
1199
|
-
// Directory events
|
|
1254
|
+
// Directory events - handle gracefully (no process.exit)
|
|
1200
1255
|
if (event === 'addDir') return;
|
|
1201
1256
|
|
|
1202
1257
|
if (event === 'unlinkDir') {
|
|
@@ -1213,7 +1268,11 @@ function loadDir(dir) {
|
|
|
1213
1268
|
|
|
1214
1269
|
if (event === 'change' && filePath.endsWith('.js')) {
|
|
1215
1270
|
if (isLibFile(filePath, dir)) {
|
|
1216
|
-
log.warn(filePath, 'is a library file
|
|
1271
|
+
log.warn(filePath, 'is a library file - scripts that require() it will see the old version until they or the daemon are restarted');
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
if (isDisabledPath(filePath) || isInDisabledDir(filePath, dir)) {
|
|
1275
|
+
log.debug(filePath, 'is disabled - ignoring change');
|
|
1217
1276
|
return;
|
|
1218
1277
|
}
|
|
1219
1278
|
log.info(filePath, 'change detected. hot-reloading.');
|
|
@@ -1221,7 +1280,11 @@ function loadDir(dir) {
|
|
|
1221
1280
|
loadScript(filePath);
|
|
1222
1281
|
} else if (event === 'add' && filePath.endsWith('.js')) {
|
|
1223
1282
|
if (isLibFile(filePath, dir)) {
|
|
1224
|
-
log.debug(filePath, 'is a library file
|
|
1283
|
+
log.debug(filePath, 'is a library file - not loading as script');
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
if (isDisabledPath(filePath) || isInDisabledDir(filePath, dir)) {
|
|
1287
|
+
log.debug(filePath, 'is disabled - not loading as script');
|
|
1225
1288
|
return;
|
|
1226
1289
|
}
|
|
1227
1290
|
log.info(filePath, 'added. loading.');
|
|
@@ -1235,7 +1298,6 @@ function loadDir(dir) {
|
|
|
1235
1298
|
});
|
|
1236
1299
|
}
|
|
1237
1300
|
}
|
|
1238
|
-
|
|
1239
1301
|
function start() {
|
|
1240
1302
|
if (config.file) {
|
|
1241
1303
|
if (typeof config.file === 'string') {
|
package/src/lib/shedb-core.js
CHANGED
|
@@ -91,7 +91,7 @@ function deepExtend(target, source) {
|
|
|
91
91
|
class SheDBCore extends EventEmitter {
|
|
92
92
|
/**
|
|
93
93
|
* @param {object} opts
|
|
94
|
-
* @param {string} opts.dbPath -
|
|
94
|
+
* @param {string} opts.dbPath - path to the directory that holds docs.json and views.json
|
|
95
95
|
* @param {object} opts.log - pino-compatible logger
|
|
96
96
|
* @param {number} [opts.scriptTimeout=5000] - vm script timeout in ms
|
|
97
97
|
*/
|
|
@@ -112,7 +112,9 @@ class SheDBCore extends EventEmitter {
|
|
|
112
112
|
|
|
113
113
|
this._viewEnvs = {}; // id → { compileError? } — only syntax-check metadata
|
|
114
114
|
this._saveTimer = null;
|
|
115
|
-
this.
|
|
115
|
+
this._saveViewsTimer = null;
|
|
116
|
+
this._docsPath = path.join(dbPath, 'docs.json');
|
|
117
|
+
this._viewsPath = path.join(dbPath, 'views.json');
|
|
116
118
|
this._worker = null;
|
|
117
119
|
|
|
118
120
|
this._spawnWorker();
|
|
@@ -125,15 +127,29 @@ class SheDBCore extends EventEmitter {
|
|
|
125
127
|
|
|
126
128
|
_load() {
|
|
127
129
|
try {
|
|
128
|
-
|
|
130
|
+
fs.mkdirSync(this.dbPath, { recursive: true });
|
|
131
|
+
} catch (err) {
|
|
132
|
+
this.log.error('shedb: could not create directory ' + this.dbPath + ': ' + err.message);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const data = JSON.parse(fs.readFileSync(this._docsPath, 'utf8'));
|
|
129
137
|
this.rev = data.rev || 0;
|
|
130
138
|
this.docs = data.docs || {};
|
|
131
139
|
this.queries = data.queries || {};
|
|
132
|
-
this.views = data.views || {};
|
|
133
140
|
this.log.info('shedb loaded ' + Object.keys(this.docs).length + ' docs, ' + Object.keys(this.queries).length + ' views from ' + this.dbPath);
|
|
134
141
|
} catch (err) {
|
|
135
142
|
if (err.code !== 'ENOENT') {
|
|
136
|
-
this.log.warn('shedb: could not load ' + this.
|
|
143
|
+
this.log.warn('shedb: could not load ' + this._docsPath + ': ' + err.message);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const vdata = JSON.parse(fs.readFileSync(this._viewsPath, 'utf8'));
|
|
149
|
+
this.views = vdata.views || {};
|
|
150
|
+
} catch (err) {
|
|
151
|
+
if (err.code !== 'ENOENT') {
|
|
152
|
+
this.log.warn('shedb: could not load views from ' + this._viewsPath + ': ' + err.message);
|
|
137
153
|
}
|
|
138
154
|
}
|
|
139
155
|
|
|
@@ -149,16 +165,29 @@ class SheDBCore extends EventEmitter {
|
|
|
149
165
|
_save() {
|
|
150
166
|
clearTimeout(this._saveTimer);
|
|
151
167
|
this._saveTimer = setTimeout(() => {
|
|
152
|
-
const tmp = this.
|
|
168
|
+
const tmp = this._docsPath + '.tmp';
|
|
153
169
|
try {
|
|
154
|
-
fs.writeFileSync(tmp, JSON.stringify({ rev: this.rev, docs: this.docs, queries: this.queries
|
|
155
|
-
fs.renameSync(tmp, this.
|
|
170
|
+
fs.writeFileSync(tmp, JSON.stringify({ rev: this.rev, docs: this.docs, queries: this.queries }), 'utf8');
|
|
171
|
+
fs.renameSync(tmp, this._docsPath); // atomic on Linux (same FS)
|
|
156
172
|
} catch (err) {
|
|
157
173
|
this.log.error('shedb: save failed: ' + err.message);
|
|
158
174
|
}
|
|
159
175
|
}, 250);
|
|
160
176
|
}
|
|
161
177
|
|
|
178
|
+
_saveViews() {
|
|
179
|
+
clearTimeout(this._saveViewsTimer);
|
|
180
|
+
this._saveViewsTimer = setTimeout(() => {
|
|
181
|
+
const tmp = this._viewsPath + '.tmp';
|
|
182
|
+
try {
|
|
183
|
+
fs.writeFileSync(tmp, JSON.stringify({ views: this.views }), 'utf8');
|
|
184
|
+
fs.renameSync(tmp, this._viewsPath);
|
|
185
|
+
} catch (err) {
|
|
186
|
+
this.log.error('shedb: save views failed: ' + err.message);
|
|
187
|
+
}
|
|
188
|
+
}, 250);
|
|
189
|
+
}
|
|
190
|
+
|
|
162
191
|
// -------------------------------------------------------------------------
|
|
163
192
|
// Rev tracking — mirrors mqttDB pattern exactly
|
|
164
193
|
// -------------------------------------------------------------------------
|
|
@@ -215,7 +244,7 @@ class SheDBCore extends EventEmitter {
|
|
|
215
244
|
this.rev++;
|
|
216
245
|
this._save();
|
|
217
246
|
this.emit('update', id, this.docs[id]);
|
|
218
|
-
this.
|
|
247
|
+
this._sendPatch(id, this.docs[id]);
|
|
219
248
|
return true;
|
|
220
249
|
}
|
|
221
250
|
this._setRev(id, rev);
|
|
@@ -244,7 +273,7 @@ class SheDBCore extends EventEmitter {
|
|
|
244
273
|
this.rev++;
|
|
245
274
|
this._save();
|
|
246
275
|
this.emit('update', id, this.docs[id]);
|
|
247
|
-
this.
|
|
276
|
+
this._sendPatch(id, this.docs[id]);
|
|
248
277
|
return true;
|
|
249
278
|
}
|
|
250
279
|
this._setRev(id, rev);
|
|
@@ -256,7 +285,7 @@ class SheDBCore extends EventEmitter {
|
|
|
256
285
|
this.rev++;
|
|
257
286
|
this._save();
|
|
258
287
|
this.emit('update', id, '');
|
|
259
|
-
this.
|
|
288
|
+
this._sendPatch(id, null);
|
|
260
289
|
}
|
|
261
290
|
|
|
262
291
|
/**
|
|
@@ -287,7 +316,7 @@ class SheDBCore extends EventEmitter {
|
|
|
287
316
|
this.rev++;
|
|
288
317
|
this._save();
|
|
289
318
|
this.emit('update', id, this.docs[id]);
|
|
290
|
-
this.
|
|
319
|
+
this._sendPatch(id, this.docs[id]);
|
|
291
320
|
return true;
|
|
292
321
|
}
|
|
293
322
|
this._setRev(id, rev);
|
|
@@ -309,6 +338,7 @@ class SheDBCore extends EventEmitter {
|
|
|
309
338
|
delete this._viewEnvs[id];
|
|
310
339
|
delete this.views[id];
|
|
311
340
|
this._save();
|
|
341
|
+
this._saveViews();
|
|
312
342
|
if (this._worker) this._worker.postMessage({ type: 'delQuery', id });
|
|
313
343
|
this.emit('view', id, '');
|
|
314
344
|
return;
|
|
@@ -362,7 +392,7 @@ class SheDBCore extends EventEmitter {
|
|
|
362
392
|
delete this.views[id].result;
|
|
363
393
|
this.log.error('shedb view ' + id + ': ' + msg.error);
|
|
364
394
|
this.emit('view', id, this.views[id]);
|
|
365
|
-
this.
|
|
395
|
+
this._saveViews();
|
|
366
396
|
return;
|
|
367
397
|
}
|
|
368
398
|
|
|
@@ -370,7 +400,7 @@ class SheDBCore extends EventEmitter {
|
|
|
370
400
|
this.views[id] = { _id: id, _rev: (prev._rev ?? -1) + 1, result: msg.result, length: msg.result.length };
|
|
371
401
|
delete this.views[id].error;
|
|
372
402
|
this.emit('view', id, this.views[id]);
|
|
373
|
-
this.
|
|
403
|
+
this._saveViews();
|
|
374
404
|
}
|
|
375
405
|
});
|
|
376
406
|
|
|
@@ -390,22 +420,20 @@ class SheDBCore extends EventEmitter {
|
|
|
390
420
|
});
|
|
391
421
|
}
|
|
392
422
|
|
|
393
|
-
/** Send
|
|
394
|
-
|
|
395
|
-
if (this.
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
}
|
|
402
|
-
});
|
|
423
|
+
/** Send a single-document patch (or deletion) to the worker — O(1). */
|
|
424
|
+
_sendPatch(id, doc) {
|
|
425
|
+
if (!this._worker) return;
|
|
426
|
+
if (doc) {
|
|
427
|
+
this._worker.postMessage({ type: 'patch', id, doc: structuredClone(doc) });
|
|
428
|
+
} else {
|
|
429
|
+
this._worker.postMessage({ type: 'del', id });
|
|
430
|
+
}
|
|
403
431
|
}
|
|
404
432
|
|
|
405
|
-
/** Send full state (docs + all queries) to the worker — used on init and respawn. */
|
|
433
|
+
/** Send full state (docs + all queries) to the worker — used on init and respawn only. */
|
|
406
434
|
_sendInitialState() {
|
|
407
435
|
if (!this._worker) return;
|
|
408
|
-
this._worker.postMessage({ type: '
|
|
436
|
+
this._worker.postMessage({ type: 'init', docs: structuredClone(this.docs) });
|
|
409
437
|
for (const id of Object.keys(this.queries)) {
|
|
410
438
|
this._worker.postMessage({ type: 'query', id, payload: this.queries[id] });
|
|
411
439
|
}
|
package/src/lib/shedb-worker.js
CHANGED
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Message protocol:
|
|
9
9
|
* Main → Worker:
|
|
10
|
-
* { type: '
|
|
11
|
-
* { type: '
|
|
10
|
+
* { type: 'init', docs: {...} } full snapshot (startup/respawn only)
|
|
11
|
+
* { type: 'patch', id, doc } single document created/updated
|
|
12
|
+
* { type: 'del', id } single document deleted
|
|
13
|
+
* { type: 'query', id, payload: {filter?,map,reduce?} }
|
|
12
14
|
* { type: 'delQuery', id }
|
|
13
15
|
*
|
|
14
16
|
* Worker → Main:
|
|
@@ -25,7 +27,7 @@ const TIMEOUT = (workerData && workerData.scriptTimeout) || 5000;
|
|
|
25
27
|
|
|
26
28
|
let docs = {};
|
|
27
29
|
const queries = {};
|
|
28
|
-
let queue =
|
|
30
|
+
let queue = new Set();
|
|
29
31
|
let running = false;
|
|
30
32
|
|
|
31
33
|
// ---------------------------------------------------------------------------
|
|
@@ -37,22 +39,31 @@ function getProp(obj, propPath) {
|
|
|
37
39
|
return propPath.split('.').reduce((cur, k) => (cur != null ? cur[k] : undefined), obj);
|
|
38
40
|
}
|
|
39
41
|
|
|
42
|
+
// Enqueue only the views whose filter matches the changed doc id.
|
|
43
|
+
// Views with no filter must always re-run (they iterate all docs).
|
|
44
|
+
function enqueueForDoc(docId) {
|
|
45
|
+
for (const [id, q] of Object.entries(queries)) {
|
|
46
|
+
if (!q.filter || mqttWildcard(docId, q.filter)) enqueue(id);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
40
50
|
// ---------------------------------------------------------------------------
|
|
41
51
|
// View queue
|
|
42
52
|
// ---------------------------------------------------------------------------
|
|
43
53
|
|
|
44
54
|
function enqueue(id) {
|
|
45
|
-
|
|
55
|
+
queue.add(id);
|
|
46
56
|
if (!running) scheduleNext();
|
|
47
57
|
}
|
|
48
58
|
|
|
49
59
|
function scheduleNext() {
|
|
50
|
-
if (queue.
|
|
60
|
+
if (queue.size === 0) {
|
|
51
61
|
running = false;
|
|
52
62
|
return;
|
|
53
63
|
}
|
|
54
64
|
running = true;
|
|
55
|
-
const id = queue.
|
|
65
|
+
const id = queue.values().next().value;
|
|
66
|
+
queue.delete(id);
|
|
56
67
|
setImmediate(() => buildAndRun(id));
|
|
57
68
|
}
|
|
58
69
|
|
|
@@ -63,20 +74,7 @@ function buildAndRun(id) {
|
|
|
63
74
|
return;
|
|
64
75
|
}
|
|
65
76
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
// Build script source — same structure as the original in-process approach
|
|
69
|
-
let src = `api.map = function() {\n${map}\n};\napi._result = [];\n`;
|
|
70
|
-
if (filter) {
|
|
71
|
-
src += `api.forEachDocument(docId => { if (api.mqttWildcard(docId, ${JSON.stringify(filter)})) api.map.apply(api.getDocument(docId)); });\n`;
|
|
72
|
-
} else {
|
|
73
|
-
src += `api.forEachDocument(docId => { api.map.apply(api.getDocument(docId)); });\n`;
|
|
74
|
-
}
|
|
75
|
-
if (reduce) {
|
|
76
|
-
src += `api.reduce = function(result) {\n${reduce}\n};\napi._result = api.reduce(api._result);\n`;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Sandbox — uses the local docs snapshot
|
|
77
|
+
// Sandbox — uses the live docs object (updated by patch/del messages)
|
|
80
78
|
const sandbox = {
|
|
81
79
|
api: {
|
|
82
80
|
forEachDocument: (cb) => Object.keys(docs).forEach(cb),
|
|
@@ -89,9 +87,8 @@ function buildAndRun(id) {
|
|
|
89
87
|
sandbox.emit = (item) => sandbox.api._result.push(item);
|
|
90
88
|
|
|
91
89
|
try {
|
|
92
|
-
const script = new vm.Script(src, { filename: 'shedb-view-' + id });
|
|
93
90
|
const ctx = vm.createContext(sandbox);
|
|
94
|
-
script.runInContext(ctx, { timeout: TIMEOUT });
|
|
91
|
+
q.script.runInContext(ctx, { timeout: TIMEOUT });
|
|
95
92
|
parentPort.postMessage({ type: 'view', id, result: Array.from(ctx.api._result) });
|
|
96
93
|
} catch (err) {
|
|
97
94
|
parentPort.postMessage({ type: 'view', id, error: 'runtime: ' + err.message });
|
|
@@ -106,16 +103,44 @@ function buildAndRun(id) {
|
|
|
106
103
|
|
|
107
104
|
parentPort.on('message', (msg) => {
|
|
108
105
|
switch (msg.type) {
|
|
109
|
-
case '
|
|
106
|
+
case 'init':
|
|
110
107
|
docs = msg.docs;
|
|
111
|
-
// Re-run all registered views with the
|
|
108
|
+
// Re-run all registered views with the fresh snapshot
|
|
112
109
|
for (const id of Object.keys(queries)) enqueue(id);
|
|
113
110
|
break;
|
|
114
111
|
|
|
115
|
-
case '
|
|
116
|
-
|
|
112
|
+
case 'patch':
|
|
113
|
+
docs[msg.id] = msg.doc;
|
|
114
|
+
enqueueForDoc(msg.id);
|
|
115
|
+
break;
|
|
116
|
+
|
|
117
|
+
case 'del':
|
|
118
|
+
delete docs[msg.id];
|
|
119
|
+
enqueueForDoc(msg.id);
|
|
120
|
+
break;
|
|
121
|
+
|
|
122
|
+
case 'query': {
|
|
123
|
+
const { filter, map, reduce } = msg.payload;
|
|
124
|
+
let src = `api.map = function() {\n${map}\n};\napi._result = [];\n`;
|
|
125
|
+
if (filter) {
|
|
126
|
+
src += `api.forEachDocument(docId => { if (api.mqttWildcard(docId, ${JSON.stringify(filter)})) api.map.apply(api.getDocument(docId)); });\n`;
|
|
127
|
+
} else {
|
|
128
|
+
src += `api.forEachDocument(docId => { api.map.apply(api.getDocument(docId)); });\n`;
|
|
129
|
+
}
|
|
130
|
+
if (reduce) {
|
|
131
|
+
src += `api.reduce = function(result) {\n${reduce}\n};\napi._result = api.reduce(api._result);\n`;
|
|
132
|
+
}
|
|
133
|
+
let script;
|
|
134
|
+
try {
|
|
135
|
+
script = new vm.Script(src, { filename: 'shedb-view-' + msg.id });
|
|
136
|
+
} catch (err) {
|
|
137
|
+
parentPort.postMessage({ type: 'view', id: msg.id, error: 'compile: ' + err.message });
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
queries[msg.id] = { ...msg.payload, script };
|
|
117
141
|
enqueue(msg.id);
|
|
118
142
|
break;
|
|
143
|
+
}
|
|
119
144
|
|
|
120
145
|
case 'delQuery':
|
|
121
146
|
delete queries[msg.id];
|
|
@@ -10,7 +10,6 @@ Scripts run in a sandboxed VM. The `she` object is injected automatically.
|
|
|
10
10
|
### MQTT
|
|
11
11
|
```
|
|
12
12
|
she.mqtt.sub(topic, [opts], cb) Subscribe; wildcards: + (1 level) # (multi)
|
|
13
|
-
+//sensor → +/status/sensor shorthand
|
|
14
13
|
opts.change: true = only fire when value changes
|
|
15
14
|
she.mqtt.pub(topic, payload, [opts]) Publish; opts: { qos, retain }
|
|
16
15
|
she.mqtt.get(topic) Current retained value (sync)
|
package/src/web/scripts-api.js
CHANGED
|
@@ -28,6 +28,11 @@ function hasShelibMarker(absDir) {
|
|
|
28
28
|
return fs.existsSync(path.join(absDir, '.shelib'));
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/** True if a .shedisable-<name> sibling exists for the given file or directory. */
|
|
32
|
+
function hasShedisableMarker(abs) {
|
|
33
|
+
return fs.existsSync(path.join(path.dirname(abs), `.shedisable-${path.basename(abs)}`));
|
|
34
|
+
}
|
|
35
|
+
|
|
31
36
|
/** Flat list of all files with metadata and lib flag. */
|
|
32
37
|
function walk(dir, base, parentIsLib) {
|
|
33
38
|
let entries;
|
|
@@ -40,11 +45,13 @@ function walk(dir, base, parentIsLib) {
|
|
|
40
45
|
const results = [];
|
|
41
46
|
for (const entry of entries) {
|
|
42
47
|
if (entry.name === '.shelib') continue;
|
|
48
|
+
if (entry.name.startsWith('.shedisable-')) continue;
|
|
43
49
|
const rel = base ? `${base}/${entry.name}` : entry.name;
|
|
44
50
|
if (entry.isDirectory()) {
|
|
45
51
|
results.push(...walk(path.join(dir, entry.name), rel, lib));
|
|
46
52
|
} else {
|
|
47
|
-
const
|
|
53
|
+
const abs = path.join(dir, entry.name);
|
|
54
|
+
const stat = fs.statSync(abs);
|
|
48
55
|
results.push({ path: rel, size: stat.size, mtime: stat.mtimeMs, lib });
|
|
49
56
|
}
|
|
50
57
|
}
|
|
@@ -67,15 +74,19 @@ function buildTree(dir, base, parentIsLib) {
|
|
|
67
74
|
const result = [];
|
|
68
75
|
for (const entry of entries) {
|
|
69
76
|
if (entry.name === '.shelib') continue;
|
|
77
|
+
if (entry.name.startsWith('.shedisable-')) continue;
|
|
70
78
|
const rel = base ? `${base}/${entry.name}` : entry.name;
|
|
71
79
|
const abs = path.join(dir, entry.name);
|
|
72
80
|
if (entry.isDirectory()) {
|
|
73
81
|
const childIsLib = lib || hasShelibMarker(abs);
|
|
82
|
+
const disabled = hasShedisableMarker(abs);
|
|
74
83
|
const children = buildTree(abs, rel, childIsLib);
|
|
75
|
-
result.push({ type: 'dir', name: entry.name, path: rel, lib: childIsLib, children });
|
|
84
|
+
result.push({ type: 'dir', name: entry.name, path: rel, lib: childIsLib, disabled, children });
|
|
76
85
|
} else {
|
|
77
86
|
const stat = fs.statSync(abs);
|
|
78
|
-
|
|
87
|
+
const isJs = entry.name.endsWith('.js');
|
|
88
|
+
const disabled = isJs ? hasShedisableMarker(abs) : false;
|
|
89
|
+
result.push({ type: 'file', name: entry.name, path: rel, lib, size: stat.size, mtime: stat.mtimeMs, ...(isJs ? { disabled } : {}) });
|
|
79
90
|
}
|
|
80
91
|
}
|
|
81
92
|
result.sort((a, b) => {
|