smart-home-engine 0.18.0 → 0.18.1
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-CygE8hzz.js → index-B8uWPBaX.js} +69 -69
- package/dist/web/assets/index-CtuJWQQl.css +1 -0
- package/dist/web/assets/{tsMode-DJvlYckS.js → tsMode-LUwPAJjU.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/src/index.js +77 -10
- package/src/lib/shedb-core.js +61 -21
- package/src/lib/shedb-worker.js +51 -26
- package/src/web/scripts-api.js +14 -3
- package/dist/web/assets/index-ZA04d_Jz.css +0 -1
package/src/lib/shedb-core.js
CHANGED
|
@@ -112,7 +112,10 @@ 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._viewsPath = dbPath.endsWith('.json')
|
|
117
|
+
? dbPath.slice(0, -5) + '-views.json'
|
|
118
|
+
: dbPath + '-views.json';
|
|
116
119
|
this._worker = null;
|
|
117
120
|
|
|
118
121
|
this._spawnWorker();
|
|
@@ -124,12 +127,13 @@ class SheDBCore extends EventEmitter {
|
|
|
124
127
|
// -------------------------------------------------------------------------
|
|
125
128
|
|
|
126
129
|
_load() {
|
|
130
|
+
let legacyViews = null;
|
|
127
131
|
try {
|
|
128
132
|
const data = JSON.parse(fs.readFileSync(this.dbPath, 'utf8'));
|
|
129
133
|
this.rev = data.rev || 0;
|
|
130
134
|
this.docs = data.docs || {};
|
|
131
135
|
this.queries = data.queries || {};
|
|
132
|
-
|
|
136
|
+
if (data.views && Object.keys(data.views).length > 0) legacyViews = data.views;
|
|
133
137
|
this.log.info('shedb loaded ' + Object.keys(this.docs).length + ' docs, ' + Object.keys(this.queries).length + ' views from ' + this.dbPath);
|
|
134
138
|
} catch (err) {
|
|
135
139
|
if (err.code !== 'ENOENT') {
|
|
@@ -137,6 +141,30 @@ class SheDBCore extends EventEmitter {
|
|
|
137
141
|
}
|
|
138
142
|
}
|
|
139
143
|
|
|
144
|
+
try {
|
|
145
|
+
const vdata = JSON.parse(fs.readFileSync(this._viewsPath, 'utf8'));
|
|
146
|
+
this.views = vdata.views || {};
|
|
147
|
+
} catch (err) {
|
|
148
|
+
if (err.code !== 'ENOENT') {
|
|
149
|
+
this.log.warn('shedb: could not load views from ' + this._viewsPath + ': ' + err.message);
|
|
150
|
+
}
|
|
151
|
+
if (legacyViews) {
|
|
152
|
+
// Migrate: split legacy single-file format into two files
|
|
153
|
+
this.views = legacyViews;
|
|
154
|
+
this.log.info('shedb: migrating views to separate file ' + this._viewsPath);
|
|
155
|
+
try {
|
|
156
|
+
const vtmp = this._viewsPath + '.tmp';
|
|
157
|
+
fs.writeFileSync(vtmp, JSON.stringify({ views: this.views }), 'utf8');
|
|
158
|
+
fs.renameSync(vtmp, this._viewsPath);
|
|
159
|
+
const tmp = this.dbPath + '.tmp';
|
|
160
|
+
fs.writeFileSync(tmp, JSON.stringify({ rev: this.rev, docs: this.docs, queries: this.queries }), 'utf8');
|
|
161
|
+
fs.renameSync(tmp, this.dbPath);
|
|
162
|
+
} catch (e) {
|
|
163
|
+
this.log.error('shedb: migration failed: ' + e.message);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
140
168
|
// Syntax-check persisted views and send state to worker
|
|
141
169
|
for (const id of Object.keys(this.queries)) {
|
|
142
170
|
this._syntaxCheck(id, this.queries[id]);
|
|
@@ -151,7 +179,7 @@ class SheDBCore extends EventEmitter {
|
|
|
151
179
|
this._saveTimer = setTimeout(() => {
|
|
152
180
|
const tmp = this.dbPath + '.tmp';
|
|
153
181
|
try {
|
|
154
|
-
fs.writeFileSync(tmp, JSON.stringify({ rev: this.rev, docs: this.docs, queries: this.queries
|
|
182
|
+
fs.writeFileSync(tmp, JSON.stringify({ rev: this.rev, docs: this.docs, queries: this.queries }), 'utf8');
|
|
155
183
|
fs.renameSync(tmp, this.dbPath); // atomic on Linux (same FS)
|
|
156
184
|
} catch (err) {
|
|
157
185
|
this.log.error('shedb: save failed: ' + err.message);
|
|
@@ -159,6 +187,19 @@ class SheDBCore extends EventEmitter {
|
|
|
159
187
|
}, 250);
|
|
160
188
|
}
|
|
161
189
|
|
|
190
|
+
_saveViews() {
|
|
191
|
+
clearTimeout(this._saveViewsTimer);
|
|
192
|
+
this._saveViewsTimer = setTimeout(() => {
|
|
193
|
+
const tmp = this._viewsPath + '.tmp';
|
|
194
|
+
try {
|
|
195
|
+
fs.writeFileSync(tmp, JSON.stringify({ views: this.views }), 'utf8');
|
|
196
|
+
fs.renameSync(tmp, this._viewsPath);
|
|
197
|
+
} catch (err) {
|
|
198
|
+
this.log.error('shedb: save views failed: ' + err.message);
|
|
199
|
+
}
|
|
200
|
+
}, 250);
|
|
201
|
+
}
|
|
202
|
+
|
|
162
203
|
// -------------------------------------------------------------------------
|
|
163
204
|
// Rev tracking — mirrors mqttDB pattern exactly
|
|
164
205
|
// -------------------------------------------------------------------------
|
|
@@ -215,7 +256,7 @@ class SheDBCore extends EventEmitter {
|
|
|
215
256
|
this.rev++;
|
|
216
257
|
this._save();
|
|
217
258
|
this.emit('update', id, this.docs[id]);
|
|
218
|
-
this.
|
|
259
|
+
this._sendPatch(id, this.docs[id]);
|
|
219
260
|
return true;
|
|
220
261
|
}
|
|
221
262
|
this._setRev(id, rev);
|
|
@@ -244,7 +285,7 @@ class SheDBCore extends EventEmitter {
|
|
|
244
285
|
this.rev++;
|
|
245
286
|
this._save();
|
|
246
287
|
this.emit('update', id, this.docs[id]);
|
|
247
|
-
this.
|
|
288
|
+
this._sendPatch(id, this.docs[id]);
|
|
248
289
|
return true;
|
|
249
290
|
}
|
|
250
291
|
this._setRev(id, rev);
|
|
@@ -256,7 +297,7 @@ class SheDBCore extends EventEmitter {
|
|
|
256
297
|
this.rev++;
|
|
257
298
|
this._save();
|
|
258
299
|
this.emit('update', id, '');
|
|
259
|
-
this.
|
|
300
|
+
this._sendPatch(id, null);
|
|
260
301
|
}
|
|
261
302
|
|
|
262
303
|
/**
|
|
@@ -287,7 +328,7 @@ class SheDBCore extends EventEmitter {
|
|
|
287
328
|
this.rev++;
|
|
288
329
|
this._save();
|
|
289
330
|
this.emit('update', id, this.docs[id]);
|
|
290
|
-
this.
|
|
331
|
+
this._sendPatch(id, this.docs[id]);
|
|
291
332
|
return true;
|
|
292
333
|
}
|
|
293
334
|
this._setRev(id, rev);
|
|
@@ -309,6 +350,7 @@ class SheDBCore extends EventEmitter {
|
|
|
309
350
|
delete this._viewEnvs[id];
|
|
310
351
|
delete this.views[id];
|
|
311
352
|
this._save();
|
|
353
|
+
this._saveViews();
|
|
312
354
|
if (this._worker) this._worker.postMessage({ type: 'delQuery', id });
|
|
313
355
|
this.emit('view', id, '');
|
|
314
356
|
return;
|
|
@@ -362,7 +404,7 @@ class SheDBCore extends EventEmitter {
|
|
|
362
404
|
delete this.views[id].result;
|
|
363
405
|
this.log.error('shedb view ' + id + ': ' + msg.error);
|
|
364
406
|
this.emit('view', id, this.views[id]);
|
|
365
|
-
this.
|
|
407
|
+
this._saveViews();
|
|
366
408
|
return;
|
|
367
409
|
}
|
|
368
410
|
|
|
@@ -370,7 +412,7 @@ class SheDBCore extends EventEmitter {
|
|
|
370
412
|
this.views[id] = { _id: id, _rev: (prev._rev ?? -1) + 1, result: msg.result, length: msg.result.length };
|
|
371
413
|
delete this.views[id].error;
|
|
372
414
|
this.emit('view', id, this.views[id]);
|
|
373
|
-
this.
|
|
415
|
+
this._saveViews();
|
|
374
416
|
}
|
|
375
417
|
});
|
|
376
418
|
|
|
@@ -390,22 +432,20 @@ class SheDBCore extends EventEmitter {
|
|
|
390
432
|
});
|
|
391
433
|
}
|
|
392
434
|
|
|
393
|
-
/** Send
|
|
394
|
-
|
|
395
|
-
if (this.
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
}
|
|
402
|
-
});
|
|
435
|
+
/** Send a single-document patch (or deletion) to the worker — O(1). */
|
|
436
|
+
_sendPatch(id, doc) {
|
|
437
|
+
if (!this._worker) return;
|
|
438
|
+
if (doc) {
|
|
439
|
+
this._worker.postMessage({ type: 'patch', id, doc: structuredClone(doc) });
|
|
440
|
+
} else {
|
|
441
|
+
this._worker.postMessage({ type: 'del', id });
|
|
442
|
+
}
|
|
403
443
|
}
|
|
404
444
|
|
|
405
|
-
/** Send full state (docs + all queries) to the worker — used on init and respawn. */
|
|
445
|
+
/** Send full state (docs + all queries) to the worker — used on init and respawn only. */
|
|
406
446
|
_sendInitialState() {
|
|
407
447
|
if (!this._worker) return;
|
|
408
|
-
this._worker.postMessage({ type: '
|
|
448
|
+
this._worker.postMessage({ type: 'init', docs: structuredClone(this.docs) });
|
|
409
449
|
for (const id of Object.keys(this.queries)) {
|
|
410
450
|
this._worker.postMessage({ type: 'query', id, payload: this.queries[id] });
|
|
411
451
|
}
|
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];
|
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) => {
|