smart-home-engine 0.0.1 → 0.11.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.
Files changed (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +76 -0
  3. package/dist/web/assets/codicon-DCmgc-ay.ttf +0 -0
  4. package/dist/web/assets/index-BbwiXmS-.css +1 -0
  5. package/dist/web/assets/index-DD-XScWV.js +140 -0
  6. package/dist/web/assets/monaco-langs-DZ6hB11b.js +1423 -0
  7. package/dist/web/assets/monaco-langs-DyX1CsEw.css +1 -0
  8. package/dist/web/assets/tsMode-DxTbjAE2.js +16 -0
  9. package/dist/web/index.html +164 -0
  10. package/dist/web/monacoeditorwork/editor.worker.bundle.js +13519 -0
  11. package/dist/web/monacoeditorwork/ts.worker.bundle.js +256353 -0
  12. package/package.json +85 -10
  13. package/src/config.js +56 -0
  14. package/src/elastic.js +19 -0
  15. package/src/index.js +1192 -0
  16. package/src/influx.js +25 -0
  17. package/src/lib/mqtt-wildcards.js +34 -0
  18. package/src/lib/parse-payload.js +29 -0
  19. package/src/lib/redis.js +74 -0
  20. package/src/lib/shedb-core.js +447 -0
  21. package/src/lib/shedb-worker.js +126 -0
  22. package/src/lib/state-store.js +97 -0
  23. package/src/lib/storage.js +74 -0
  24. package/src/matter/controller.js +307 -0
  25. package/src/sandbox/api.js +57 -0
  26. package/src/sandbox/elastic-sandbox.js +88 -0
  27. package/src/sandbox/influx-sandbox.js +107 -0
  28. package/src/sandbox/matter-sandbox.js +92 -0
  29. package/src/sandbox/shedb-sandbox.js +89 -0
  30. package/src/sandbox/stdlib.js +132 -0
  31. package/src/scripts/hello.js +3 -0
  32. package/src/web/ai-api.js +443 -0
  33. package/src/web/auth.js +186 -0
  34. package/src/web/config-api.js +34 -0
  35. package/src/web/deps-api.js +138 -0
  36. package/src/web/git-api.js +188 -0
  37. package/src/web/log-ws.js +78 -0
  38. package/src/web/matter-api.js +102 -0
  39. package/src/web/mqtt-api.js +65 -0
  40. package/src/web/scripts-api.js +192 -0
  41. package/src/web/server.js +139 -0
  42. package/src/web/shedb-api.js +140 -0
  43. package/src/web/shedb.js +168 -0
  44. package/index.js +0 -0
package/src/influx.js ADDED
@@ -0,0 +1,25 @@
1
+ 'use strict';
2
+
3
+ let _client = null;
4
+ let _opts = null;
5
+
6
+ /**
7
+ * Initialise the InfluxDB client. Called from index.js when config.influx is set.
8
+ * @param {{ url: string, token: string, org: string, bucket: string }} opts
9
+ */
10
+ function init(opts) {
11
+ if (!opts || !opts.url || !opts.token) return;
12
+ const { InfluxDB } = require('@influxdata/influxdb-client');
13
+ _opts = opts;
14
+ _client = new InfluxDB({ url: opts.url, token: opts.token });
15
+ }
16
+
17
+ function getClient() {
18
+ return _client;
19
+ }
20
+
21
+ function getOpts() {
22
+ return _opts;
23
+ }
24
+
25
+ module.exports = { init, getClient, getOpts };
@@ -0,0 +1,34 @@
1
+ // Source: https://github.com/hobbyquaker/mqtt-wildcard (MIT)
2
+ 'use strict';
3
+
4
+ function mqttWildcard(topic, wildcard) {
5
+ if (topic === wildcard) {
6
+ return [];
7
+ } else if (wildcard === '#') {
8
+ return [topic];
9
+ }
10
+
11
+ var res = [];
12
+ var t = String(topic).split('/');
13
+ var w = String(wildcard).split('/');
14
+
15
+ var i = 0;
16
+ for (var lt = t.length; i < lt; i++) {
17
+ if (w[i] === '+') {
18
+ res.push(t[i]);
19
+ } else if (w[i] === '#') {
20
+ res.push(t.slice(i).join('/'));
21
+ return res;
22
+ } else if (w[i] !== t[i]) {
23
+ return null;
24
+ }
25
+ }
26
+
27
+ if (w[i] === '#') {
28
+ i += 1;
29
+ }
30
+
31
+ return i === w.length ? res : null;
32
+ }
33
+
34
+ module.exports = mqttWildcard;
@@ -0,0 +1,29 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Parse an MQTT payload (string or Buffer) into a typed state object.
5
+ * The returned object always has a `val` property; JSON payloads that
6
+ * already carry a `val` key are returned as-is.
7
+ *
8
+ * @param {string|Buffer} payload
9
+ * @returns {{ val: *, ts?: number, lc?: number }}
10
+ */
11
+ function parsePayload(payload) {
12
+ const str = payload.toString();
13
+
14
+ if (str === 'true') return { val: true };
15
+ if (str === 'false') return { val: false };
16
+
17
+ if (!isNaN(str)) return { val: parseFloat(str) };
18
+
19
+ try {
20
+ const parsed = JSON.parse(str);
21
+ if (Array.isArray(parsed)) return { val: parsed };
22
+ if (!parsed || typeof parsed.val === 'undefined') return { val: parsed };
23
+ return parsed;
24
+ } catch (_) {
25
+ return { val: str };
26
+ }
27
+ }
28
+
29
+ module.exports = parsePayload;
@@ -0,0 +1,74 @@
1
+ 'use strict';
2
+
3
+ let client = null;
4
+
5
+ /**
6
+ * Initialize the Redis write-through cache.
7
+ * Seeds StateStore from Redis on startup, then writes every store change back.
8
+ *
9
+ * Config: config.json → { "redis": { "url": "redis://localhost:6379" } }
10
+ *
11
+ * @param {object} opts
12
+ * @param {string} opts.url - Redis URL
13
+ * @param {object} opts.store - StateStore instance
14
+ * @param {object} opts.log - Logger
15
+ * @returns {Promise<void>}
16
+ */
17
+ async function init({ url, store, log }) {
18
+ let Redis;
19
+ try {
20
+ Redis = require('ioredis');
21
+ } catch {
22
+ log.error('redis: ioredis not installed — run: npm install ioredis');
23
+ return;
24
+ }
25
+
26
+ client = new Redis(url, { lazyConnect: true });
27
+
28
+ client.on('error', (err) => {
29
+ log.error('redis error:', err.message);
30
+ });
31
+
32
+ try {
33
+ await client.connect();
34
+ } catch (err) {
35
+ log.error('redis: connect failed:', err.message);
36
+ return;
37
+ }
38
+
39
+ // Seed StateStore from Redis hash on startup
40
+ try {
41
+ const hash = await client.hgetall('she:state');
42
+ if (hash) {
43
+ let count = 0;
44
+ for (const [key, json] of Object.entries(hash)) {
45
+ try {
46
+ const obj = JSON.parse(json);
47
+ store.setObject(key, obj);
48
+ count++;
49
+ } catch {
50
+ log.warn('redis: skipping invalid JSON for key', key);
51
+ }
52
+ }
53
+ log.info('redis: seeded', count, 'keys from she:state');
54
+ }
55
+ } catch (err) {
56
+ log.error('redis: seed failed:', err.message);
57
+ }
58
+
59
+ // Write-through: every StateStore change → Redis hset
60
+ store.on('change', (key, _val, obj) => {
61
+ client.hset('she:state', key, JSON.stringify(obj)).catch((err) => {
62
+ log.error('redis: hset failed:', err.message);
63
+ });
64
+ });
65
+
66
+ log.info('redis: connected', url);
67
+ }
68
+
69
+ /** @returns {import('ioredis').Redis | null} */
70
+ function getClient() {
71
+ return client;
72
+ }
73
+
74
+ module.exports = { init, getClient };
@@ -0,0 +1,447 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * SheDB Core — vendored and simplified from hobbyquaker/mqttDB.
5
+ *
6
+ * Key differences vs. original:
7
+ * - View execution delegated to a single worker_threads Worker
8
+ * - No obj-ease dependency: prop utilities inlined below
9
+ * - No external persistence library: atomic JSON via tmp+rename
10
+ * - Logging via pino-style `log` object instead of yalm
11
+ *
12
+ * Events emitted:
13
+ * 'ready' — after initial file load + view compilation
14
+ * 'update' (id, doc|'') — document changed or deleted
15
+ * 'view' (id, viewObj|'') — view result changed or view deleted
16
+ * 'query' (id) — query (view definition) added/changed
17
+ */
18
+
19
+ const fs = require('fs');
20
+ const vm = require('vm');
21
+ const path = require('path');
22
+ const { Worker } = require('worker_threads');
23
+ const { EventEmitter } = require('events');
24
+ const mqttWildcard = require('./mqtt-wildcards');
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Inline obj-ease equivalents (dot-notation property helpers)
28
+ // ---------------------------------------------------------------------------
29
+
30
+ function getProp(obj, propPath) {
31
+ if (obj == null || !propPath) return undefined;
32
+ return propPath.split('.').reduce((cur, k) => (cur != null ? cur[k] : undefined), obj);
33
+ }
34
+
35
+ function setProp(obj, propPath, val) {
36
+ if (!obj || !propPath) return false;
37
+ const parts = propPath.split('.');
38
+ let cur = obj;
39
+ for (let i = 0; i < parts.length - 1; i++) {
40
+ if (cur[parts[i]] == null || typeof cur[parts[i]] !== 'object') cur[parts[i]] = {};
41
+ cur = cur[parts[i]];
42
+ }
43
+ const last = parts[parts.length - 1];
44
+ if (deepEqual(cur[last], val)) return false;
45
+ cur[last] = val;
46
+ return true;
47
+ }
48
+
49
+ function delProp(obj, propPath) {
50
+ if (!obj || !propPath) return false;
51
+ const parts = propPath.split('.');
52
+ let cur = obj;
53
+ for (let i = 0; i < parts.length - 1; i++) {
54
+ if (cur[parts[i]] == null || typeof cur[parts[i]] !== 'object') return false;
55
+ cur = cur[parts[i]];
56
+ }
57
+ const last = parts[parts.length - 1];
58
+ if (typeof cur[last] === 'undefined') return false;
59
+ delete cur[last];
60
+ return true;
61
+ }
62
+
63
+ function deepEqual(a, b) {
64
+ if (a === b) return true;
65
+ if (a === null || b === null || typeof a !== 'object' || typeof b !== 'object') return false;
66
+ const ka = Object.keys(a).sort();
67
+ const kb = Object.keys(b).sort();
68
+ if (ka.length !== kb.length) return false;
69
+ return ka.every((k, i) => kb[i] === k && deepEqual(a[k], b[k]));
70
+ }
71
+
72
+ function deepExtend(target, source) {
73
+ let changed = false;
74
+ for (const k of Object.keys(source)) {
75
+ const sv = source[k];
76
+ const tv = target[k];
77
+ if (sv !== null && typeof sv === 'object' && !Array.isArray(sv) && tv !== null && typeof tv === 'object' && !Array.isArray(tv)) {
78
+ if (deepExtend(tv, sv)) changed = true;
79
+ } else if (!deepEqual(tv, sv)) {
80
+ target[k] = sv;
81
+ changed = true;
82
+ }
83
+ }
84
+ return changed;
85
+ }
86
+
87
+ // ---------------------------------------------------------------------------
88
+ // Core
89
+ // ---------------------------------------------------------------------------
90
+
91
+ class SheDBCore extends EventEmitter {
92
+ /**
93
+ * @param {object} opts
94
+ * @param {string} opts.dbPath - absolute path to the JSON data file
95
+ * @param {object} opts.log - pino-compatible logger
96
+ * @param {number} [opts.scriptTimeout=5000] - vm script timeout in ms
97
+ */
98
+ constructor({ dbPath, log, scriptTimeout = 5000 }) {
99
+ super();
100
+ this.dbPath = dbPath;
101
+ this.log = log;
102
+ this.scriptTimeout = scriptTimeout;
103
+
104
+ /** @type {Object<string,object>} document store */
105
+ this.docs = {};
106
+ /** @type {Object<string,{filter?,map,reduce?}>} persistent view definitions */
107
+ this.queries = {};
108
+ /** @type {Object<string,{_id,_rev,result?,length?,error?}>} computed view results */
109
+ this.views = {};
110
+ /** Global revision counter — increments on every document change */
111
+ this.rev = 0;
112
+
113
+ this._viewEnvs = {}; // id → { compileError? } — only syntax-check metadata
114
+ this._saveTimer = null;
115
+ this._sendDbScheduled = false;
116
+ this._worker = null;
117
+
118
+ this._spawnWorker();
119
+ this._load();
120
+ }
121
+
122
+ // -------------------------------------------------------------------------
123
+ // Persistence — atomic JSON via tmp+rename
124
+ // -------------------------------------------------------------------------
125
+
126
+ _load() {
127
+ try {
128
+ const data = JSON.parse(fs.readFileSync(this.dbPath, 'utf8'));
129
+ this.rev = data.rev || 0;
130
+ this.docs = data.docs || {};
131
+ this.queries = data.queries || {};
132
+ this.views = data.views || {};
133
+ this.log.info('shedb loaded ' + Object.keys(this.docs).length + ' docs, ' + Object.keys(this.queries).length + ' views from ' + this.dbPath);
134
+ } catch (err) {
135
+ if (err.code !== 'ENOENT') {
136
+ this.log.warn('shedb: could not load ' + this.dbPath + ': ' + err.message);
137
+ }
138
+ }
139
+
140
+ // Syntax-check persisted views and send state to worker
141
+ for (const id of Object.keys(this.queries)) {
142
+ this._syntaxCheck(id, this.queries[id]);
143
+ }
144
+ this._sendInitialState();
145
+
146
+ setImmediate(() => this.emit('ready'));
147
+ }
148
+
149
+ _save() {
150
+ clearTimeout(this._saveTimer);
151
+ this._saveTimer = setTimeout(() => {
152
+ const tmp = this.dbPath + '.tmp';
153
+ try {
154
+ fs.writeFileSync(tmp, JSON.stringify({ rev: this.rev, docs: this.docs, queries: this.queries, views: this.views }), 'utf8');
155
+ fs.renameSync(tmp, this.dbPath); // atomic on Linux (same FS)
156
+ } catch (err) {
157
+ this.log.error('shedb: save failed: ' + err.message);
158
+ }
159
+ }, 250);
160
+ }
161
+
162
+ // -------------------------------------------------------------------------
163
+ // Rev tracking — mirrors mqttDB pattern exactly
164
+ // -------------------------------------------------------------------------
165
+
166
+ _getRev(id) {
167
+ if (this.docs[id] && typeof this.docs[id]._rev !== 'undefined') {
168
+ const rev = this.docs[id]._rev;
169
+ delete this.docs[id]._rev;
170
+ return rev;
171
+ }
172
+ return -1;
173
+ }
174
+
175
+ _setRev(id, rev) {
176
+ if (this.docs[id]) this.docs[id]._rev = rev;
177
+ }
178
+
179
+ _incRev(id, rev) {
180
+ if (this.docs[id]) this.docs[id]._rev = rev + 1;
181
+ }
182
+
183
+ // -------------------------------------------------------------------------
184
+ // Public document API
185
+ // -------------------------------------------------------------------------
186
+
187
+ get(id) {
188
+ return this.docs[id];
189
+ }
190
+
191
+ /**
192
+ * Set (create or overwrite) a document.
193
+ * @param {string} id
194
+ * @param {object|''} payload - empty string means delete
195
+ * @returns {boolean} true if the document actually changed
196
+ */
197
+ set(id, payload) {
198
+ if (payload === '') return this.del(id) || true;
199
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return false;
200
+
201
+ const incoming = Object.assign({}, payload);
202
+ delete incoming._rev;
203
+ delete incoming._id;
204
+
205
+ const rev = this._getRev(id);
206
+ const cur = this.docs[id] ? Object.assign({}, this.docs[id]) : undefined;
207
+ if (cur) {
208
+ delete cur._rev;
209
+ delete cur._id;
210
+ }
211
+
212
+ if (!deepEqual(cur, incoming)) {
213
+ this.docs[id] = Object.assign({}, incoming, { _id: id });
214
+ this._incRev(id, rev);
215
+ this.rev++;
216
+ this._save();
217
+ this.emit('update', id, this.docs[id]);
218
+ this._sendDb();
219
+ return true;
220
+ }
221
+ this._setRev(id, rev);
222
+ return false;
223
+ }
224
+
225
+ /**
226
+ * Deep-merge a partial object into an existing document.
227
+ */
228
+ extend(id, payload) {
229
+ if (!payload || typeof payload !== 'object') return false;
230
+ const clean = Object.assign({}, payload);
231
+ delete clean._id;
232
+ delete clean._rev;
233
+
234
+ const rev = this._getRev(id);
235
+ if (!this.docs[id]) this.docs[id] = { _id: id };
236
+ const savedId = this.docs[id]._id;
237
+ delete this.docs[id]._id;
238
+
239
+ const changed = deepExtend(this.docs[id], clean);
240
+ this.docs[id]._id = savedId || id;
241
+
242
+ if (changed) {
243
+ this._incRev(id, rev);
244
+ this.rev++;
245
+ this._save();
246
+ this.emit('update', id, this.docs[id]);
247
+ this._sendDb();
248
+ return true;
249
+ }
250
+ this._setRev(id, rev);
251
+ return false;
252
+ }
253
+
254
+ del(id) {
255
+ delete this.docs[id];
256
+ this.rev++;
257
+ this._save();
258
+ this.emit('update', id, '');
259
+ this._sendDb();
260
+ }
261
+
262
+ /**
263
+ * Set/create/del a nested property on a document.
264
+ * @param {string} id
265
+ * @param {{method:'set'|'create'|'del', prop:string, val?:*}} opts
266
+ */
267
+ prop(id, { method, prop, val } = {}) {
268
+ if (!this.docs[id] || !prop) return false;
269
+
270
+ const rev = this._getRev(id);
271
+ let changed = false;
272
+
273
+ if (method === 'set') {
274
+ changed = setProp(this.docs[id], prop, val);
275
+ } else if (method === 'create') {
276
+ if (typeof getProp(this.docs[id], prop) === 'undefined') {
277
+ setProp(this.docs[id], prop, val);
278
+ changed = true;
279
+ }
280
+ } else if (method === 'del') {
281
+ changed = delProp(this.docs[id], prop);
282
+ }
283
+
284
+ if (changed) {
285
+ this.docs[id]._id = id;
286
+ this._incRev(id, rev);
287
+ this.rev++;
288
+ this._save();
289
+ this.emit('update', id, this.docs[id]);
290
+ this._sendDb();
291
+ return true;
292
+ }
293
+ this._setRev(id, rev);
294
+ return false;
295
+ }
296
+
297
+ // -------------------------------------------------------------------------
298
+ // Named views (persistent map/reduce queries)
299
+ // -------------------------------------------------------------------------
300
+
301
+ /**
302
+ * Create/update or delete a named view.
303
+ * @param {string} id - view name
304
+ * @param {''|{filter?:string, map:string, reduce?:string}} payload - '' to delete
305
+ */
306
+ query(id, payload) {
307
+ if (payload === '') {
308
+ delete this.queries[id];
309
+ delete this._viewEnvs[id];
310
+ delete this.views[id];
311
+ this._save();
312
+ if (this._worker) this._worker.postMessage({ type: 'delQuery', id });
313
+ this.emit('view', id, '');
314
+ return;
315
+ }
316
+ this.queries[id] = payload;
317
+ // Fast in-process syntax check — report compile errors immediately
318
+ if (!this._syntaxCheck(id, payload)) return;
319
+ if (this._worker) this._worker.postMessage({ type: 'query', id, payload });
320
+ this._save();
321
+ this.emit('query', id);
322
+ }
323
+
324
+ // -------------------------------------------------------------------------
325
+ // Ad-hoc (non-persistent) query — synchronous, no vm
326
+ // Map fn signature: (doc, emit) where emit(item) pushes to result
327
+ // -------------------------------------------------------------------------
328
+
329
+ adhocQuery(filter, mapFn, reduceFn) {
330
+ const result = [];
331
+ const emit = (item) => result.push(item);
332
+ for (const id of Object.keys(this.docs)) {
333
+ if (filter && !mqttWildcard(id, filter)) continue;
334
+ try {
335
+ mapFn(this.docs[id], emit);
336
+ } catch {
337
+ /* ignore per-doc map errors */
338
+ }
339
+ }
340
+ return typeof reduceFn === 'function' ? reduceFn(result) : result;
341
+ }
342
+
343
+ // -------------------------------------------------------------------------
344
+ // Worker thread management
345
+ // -------------------------------------------------------------------------
346
+
347
+ _spawnWorker() {
348
+ this._worker = new Worker(path.join(__dirname, 'shedb-worker.js'), {
349
+ workerData: { scriptTimeout: this.scriptTimeout },
350
+ });
351
+
352
+ this._worker.on('message', (msg) => {
353
+ if (msg.type !== 'view') return;
354
+ const { id } = msg;
355
+
356
+ if (msg.deleted) return; // deletion already handled in query()
357
+
358
+ const prev = this.views[id] || { _id: id, _rev: -1 };
359
+
360
+ if (msg.error) {
361
+ this.views[id] = { _id: id, _rev: prev._rev ?? -1, error: msg.error };
362
+ delete this.views[id].result;
363
+ this.log.error('shedb view ' + id + ': ' + msg.error);
364
+ this.emit('view', id, this.views[id]);
365
+ this._save();
366
+ return;
367
+ }
368
+
369
+ if (!deepEqual(msg.result, prev.result)) {
370
+ this.views[id] = { _id: id, _rev: (prev._rev ?? -1) + 1, result: msg.result, length: msg.result.length };
371
+ delete this.views[id].error;
372
+ this.emit('view', id, this.views[id]);
373
+ this._save();
374
+ }
375
+ });
376
+
377
+ this._worker.on('error', (err) => {
378
+ this.log.error('shedb worker error: ' + err.message);
379
+ });
380
+
381
+ this._worker.on('exit', (code) => {
382
+ this._worker = null;
383
+ if (code !== 0) {
384
+ this.log.error('shedb worker exited with code ' + code + ', restarting in 1s');
385
+ setTimeout(() => {
386
+ this._spawnWorker();
387
+ this._sendInitialState();
388
+ }, 1000);
389
+ }
390
+ });
391
+ }
392
+
393
+ /** Send full docs snapshot to the worker (debounced via setImmediate). */
394
+ _sendDb() {
395
+ if (this._sendDbScheduled) return;
396
+ this._sendDbScheduled = true;
397
+ setImmediate(() => {
398
+ this._sendDbScheduled = false;
399
+ if (this._worker) {
400
+ this._worker.postMessage({ type: 'db', docs: structuredClone(this.docs) });
401
+ }
402
+ });
403
+ }
404
+
405
+ /** Send full state (docs + all queries) to the worker — used on init and respawn. */
406
+ _sendInitialState() {
407
+ if (!this._worker) return;
408
+ this._worker.postMessage({ type: 'db', docs: structuredClone(this.docs) });
409
+ for (const id of Object.keys(this.queries)) {
410
+ this._worker.postMessage({ type: 'query', id, payload: this.queries[id] });
411
+ }
412
+ }
413
+
414
+ // -------------------------------------------------------------------------
415
+ // Syntax check — fast in-process compile, no execution
416
+ // -------------------------------------------------------------------------
417
+
418
+ _syntaxCheck(id, { filter, map, reduce }) {
419
+ if (!this._viewEnvs[id]) this._viewEnvs[id] = {};
420
+ const env = this._viewEnvs[id];
421
+
422
+ let src = `api.map = function() {\n${map}\n};\napi._result = [];\n`;
423
+ if (filter) {
424
+ src += `api.forEachDocument(docId => { if (api.mqttWildcard(docId, ${JSON.stringify(filter)})) api.map.apply(api.getDocument(docId)); });\n`;
425
+ } else {
426
+ src += `api.forEachDocument(docId => { api.map.apply(api.getDocument(docId)); });\n`;
427
+ }
428
+ if (reduce) {
429
+ src += `api.reduce = function(result) {\n${reduce}\n};\napi._result = api.reduce(api._result);\n`;
430
+ }
431
+
432
+ try {
433
+ new vm.Script(src, { filename: 'shedb-view-' + id });
434
+ delete env.compileError;
435
+ return true;
436
+ } catch (err) {
437
+ env.compileError = 'compile: ' + err.message;
438
+ this.log.error('shedb view ' + id + ': ' + env.compileError);
439
+ const rev = (this.views[id]?._rev ?? -1) + 1;
440
+ this.views[id] = { _id: id, _rev: rev, error: env.compileError };
441
+ this.emit('view', id, this.views[id]);
442
+ return false;
443
+ }
444
+ }
445
+ }
446
+
447
+ module.exports = SheDBCore;