@2112-lab/central-plant 0.3.58 → 0.3.59

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 (27) hide show
  1. package/dist/bundle/index.js +1635 -32
  2. package/dist/cjs/src/core/centralPlant.js +80 -11
  3. package/dist/cjs/src/core/centralPlantInternals.js +4 -0
  4. package/dist/cjs/src/core/centralPlantValidator.js +10 -6
  5. package/dist/cjs/src/core/stateEngine.js +733 -0
  6. package/dist/cjs/src/core/stateEngineScene.js +97 -0
  7. package/dist/cjs/src/index.js +10 -0
  8. package/dist/cjs/src/managers/pathfinding/PathFlowManager.js +94 -13
  9. package/dist/cjs/src/managers/pathfinding/PathRenderingManager.js +15 -0
  10. package/dist/cjs/src/managers/scene/componentTooltipManager.js +7 -1
  11. package/dist/cjs/src/managers/scene/controlPanelManager.js +377 -0
  12. package/dist/cjs/src/managers/scene/sceneExportManager.js +10 -1
  13. package/dist/cjs/src/managers/state/EffectiveVisualManager.js +270 -0
  14. package/dist/esm/src/core/centralPlant.js +80 -11
  15. package/dist/esm/src/core/centralPlantInternals.js +4 -0
  16. package/dist/esm/src/core/centralPlantValidator.js +10 -6
  17. package/dist/esm/src/core/stateEngine.js +725 -0
  18. package/dist/esm/src/core/stateEngineScene.js +90 -0
  19. package/dist/esm/src/index.js +2 -0
  20. package/dist/esm/src/managers/pathfinding/PathFlowManager.js +94 -13
  21. package/dist/esm/src/managers/pathfinding/PathRenderingManager.js +15 -0
  22. package/dist/esm/src/managers/scene/componentTooltipManager.js +7 -1
  23. package/dist/esm/src/managers/scene/controlPanelManager.js +352 -0
  24. package/dist/esm/src/managers/scene/sceneExportManager.js +10 -1
  25. package/dist/esm/src/managers/state/EffectiveVisualManager.js +266 -0
  26. package/dist/index.d.ts +85 -0
  27. package/package.json +5 -2
@@ -0,0 +1,725 @@
1
+ import { createClass as _createClass, toConsumableArray as _toConsumableArray, slicedToArray as _slicedToArray, createForOfIteratorHelper as _createForOfIteratorHelper, classCallCheck as _classCallCheck } from '../../_virtual/_rollupPluginBabelHelpers.js';
2
+
3
+ /**
4
+ * @file stateEngine.js
5
+ * @description Portable State Engine core — registry, desired/effective resolution,
6
+ * plant (and later group) gates, change notifications. No Vue/Three.js.
7
+ *
8
+ * Phase 1 scaffold per STATE_ENGINE_FRAMEWORK_0.3.md / milestones doc.
9
+ */
10
+
11
+ /**
12
+ * @typedef {'desired' | 'effective'} StateLayer
13
+ */
14
+
15
+ /**
16
+ * Build a scoped registry address.
17
+ * @param {string} scope - e.g. "plant", "group:chiller-loop-a", "pump-001"
18
+ * @param {string} attribute
19
+ * @returns {string}
20
+ */
21
+ function makeAddress(scope, attribute) {
22
+ return "".concat(scope, "::").concat(attribute);
23
+ }
24
+
25
+ /**
26
+ * Shallow equality for registry values (boolean, number, string, null).
27
+ * @param {*} a
28
+ * @param {*} b
29
+ * @returns {boolean}
30
+ */
31
+ function valuesEqual(a, b) {
32
+ return Object.is(a, b);
33
+ }
34
+
35
+ /**
36
+ * Safe-off value for a gated operational attribute: 0 for numbers, false for
37
+ * booleans, otherwise the value unchanged.
38
+ * @param {*} desired
39
+ * @returns {*}
40
+ */
41
+ function offValue(desired) {
42
+ if (typeof desired === 'number') return 0;
43
+ if (typeof desired === 'boolean') return false;
44
+ return desired;
45
+ }
46
+
47
+ /**
48
+ * Central Plant State Engine (logic only).
49
+ *
50
+ * Desired values are stored in the registry. Effective values are computed
51
+ * from gates (plant / groups / object) without erasing desired.
52
+ */
53
+ var StateEngine = /*#__PURE__*/function () {
54
+ /**
55
+ * @param {object} [options]
56
+ * @param {number} [options.maxChainDepth=8] - Ripple hop limit (Phase 3)
57
+ */
58
+ function StateEngine() {
59
+ var _options$maxChainDept, _options$gatedOperati;
60
+ var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
61
+ _classCallCheck(this, StateEngine);
62
+ /** @type {Map<string, *>} address → desired value */
63
+ this._desired = new Map();
64
+
65
+ /** @type {Map<string, Set<string>>} groupId → member object ids */
66
+ this._groupMembers = new Map();
67
+
68
+ /** @type {Map<string, *>} groupId → group desired power (and later other attrs) */
69
+ this._groupDesired = new Map();
70
+
71
+ /** @type {boolean} */
72
+ this._plantPowerDesired = true;
73
+
74
+ /** @type {Set<(change: { address: string, desired: *, effective: * }) => void>} */
75
+ this._listeners = new Set();
76
+
77
+ /** @type {number} */
78
+ this.maxChainDepth = (_options$maxChainDept = options.maxChainDepth) !== null && _options$maxChainDept !== void 0 ? _options$maxChainDept : 8;
79
+
80
+ /**
81
+ * Operational attributes that read a safe-off value when their object's
82
+ * power is not effectively on (Framework §6/§10: fan level 5 + plant off → 0).
83
+ * Passive/meta attributes stay ungated.
84
+ * @type {Set<string>}
85
+ */
86
+ this._gatedOperationalAttributes = new Set((_options$gatedOperati = options.gatedOperationalAttributes) !== null && _options$gatedOperati !== void 0 ? _options$gatedOperati : ['fan-level', 'rpm', 'level', 'speed']);
87
+
88
+ /** @type {Array<object>} declared chains (Phase 3) */
89
+ this._chains = [];
90
+
91
+ /** @type {boolean} */
92
+ this._batching = false;
93
+
94
+ /** @type {Map<string, { address: string, desired: *, effective: * }>} */
95
+ this._pendingNotifications = new Map();
96
+ }
97
+
98
+ // ─── Load / reset ─────────────────────────────────────────────────────────
99
+
100
+ /**
101
+ * Load plant, groups, and initial desired values from a scene-like declaration.
102
+ * @param {object} declaration
103
+ * @param {object} [declaration.plant]
104
+ * @param {object} [declaration.groups]
105
+ * @param {object} [declaration.stateRegistry]
106
+ * @param {Array<object>} [declaration.chains]
107
+ */
108
+ return _createClass(StateEngine, [{
109
+ key: "load",
110
+ value: function load() {
111
+ var _declaration$plant;
112
+ var declaration = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
113
+ this._desired.clear();
114
+ this._groupMembers.clear();
115
+ this._groupDesired.clear();
116
+ this._chains = Array.isArray(declaration.chains) ? _toConsumableArray(declaration.chains) : [];
117
+ var plantDefault = ((_declaration$plant = declaration.plant) === null || _declaration$plant === void 0 || (_declaration$plant = _declaration$plant.states) === null || _declaration$plant === void 0 || (_declaration$plant = _declaration$plant.power) === null || _declaration$plant === void 0 ? void 0 : _declaration$plant.default) !== undefined ? declaration.plant.states.power.default : true;
118
+ this._plantPowerDesired = Boolean(plantDefault);
119
+ this._desired.set(makeAddress('plant', 'power'), this._plantPowerDesired);
120
+ var groups = declaration.groups || {};
121
+ for (var _i = 0, _Object$entries = Object.entries(groups); _i < _Object$entries.length; _i++) {
122
+ var _group$states;
123
+ var _Object$entries$_i = _slicedToArray(_Object$entries[_i], 2),
124
+ groupId = _Object$entries$_i[0],
125
+ group = _Object$entries$_i[1];
126
+ var members = Array.isArray(group.members) ? group.members : [];
127
+ this._groupMembers.set(groupId, new Set(members));
128
+ var groupPower = ((_group$states = group.states) === null || _group$states === void 0 || (_group$states = _group$states.power) === null || _group$states === void 0 ? void 0 : _group$states.default) !== undefined ? group.states.power.default : true;
129
+ this._groupDesired.set(groupId, Boolean(groupPower));
130
+ this._desired.set(makeAddress("group:".concat(groupId), 'power'), Boolean(groupPower));
131
+ }
132
+ var registry = declaration.stateRegistry || {};
133
+ for (var _i2 = 0, _Object$entries2 = Object.entries(registry); _i2 < _Object$entries2.length; _i2++) {
134
+ var _Object$entries2$_i = _slicedToArray(_Object$entries2[_i2], 2),
135
+ address = _Object$entries2$_i[0],
136
+ value = _Object$entries2$_i[1];
137
+ this._desired.set(address, value);
138
+ if (address === makeAddress('plant', 'power')) {
139
+ this._plantPowerDesired = Boolean(value);
140
+ }
141
+ var groupMatch = /^group:([^:]+)::power$/.exec(address);
142
+ if (groupMatch) {
143
+ this._groupDesired.set(groupMatch[1], Boolean(value));
144
+ }
145
+ }
146
+ }
147
+ }, {
148
+ key: "reset",
149
+ value: function reset() {
150
+ this._desired.clear();
151
+ this._groupMembers.clear();
152
+ this._groupDesired.clear();
153
+ this._chains = [];
154
+ this._plantPowerDesired = true;
155
+ this._pendingNotifications.clear();
156
+ this._batching = false;
157
+ }
158
+
159
+ // ─── Desired / effective ──────────────────────────────────────────────────
160
+
161
+ /**
162
+ * @param {string} address
163
+ * @returns {*}
164
+ */
165
+ }, {
166
+ key: "getDesired",
167
+ value: function getDesired(address) {
168
+ return this._desired.has(address) ? this._desired.get(address) : undefined;
169
+ }
170
+
171
+ /**
172
+ * Compute effective value for an address.
173
+ * Power-style gates: plant AND all containing groups AND object desired.
174
+ * Non-power attributes: return desired when power gates allow, else a safe off value.
175
+ *
176
+ * @param {string} address
177
+ * @returns {*}
178
+ */
179
+ }, {
180
+ key: "getEffective",
181
+ value: function getEffective(address) {
182
+ var desired = this.getDesired(address);
183
+ if (desired === undefined) return undefined;
184
+ if (address === makeAddress('plant', 'power')) {
185
+ return Boolean(desired);
186
+ }
187
+ var groupPowerMatch = /^group:([^:]+)::power$/.exec(address);
188
+ if (groupPowerMatch) {
189
+ return this._plantPowerDesired && Boolean(desired);
190
+ }
191
+ var objectPowerMatch = /^([^:]+)::power$/.exec(address);
192
+ if (objectPowerMatch && !address.startsWith('group:') && !address.startsWith('plant::')) {
193
+ var objectId = objectPowerMatch[1];
194
+ // Skip nested I/O addresses like pump-001::start-switch::mode
195
+ if (address.split('::').length !== 2) {
196
+ return desired;
197
+ }
198
+ return this._isPowerAllowedForObject(objectId) && Boolean(desired);
199
+ }
200
+
201
+ // Flow and other attrs: when tied to an object id prefix, gate on that object's power path
202
+ var parts = address.split('::');
203
+ if (parts.length === 2) {
204
+ var _parts = _slicedToArray(parts, 2),
205
+ _objectId = _parts[0],
206
+ attribute = _parts[1];
207
+ if (attribute === 'flow') {
208
+ var powerAllowed = this._isPowerAllowedForObject(_objectId);
209
+ if (!powerAllowed) return false;
210
+ return Boolean(desired);
211
+ }
212
+ // Operational values (fan level, rpm, …) collapse to a safe-off value when
213
+ // the object's power is not effectively on; restored when power returns (§6, §10).
214
+ if (this._gatedOperationalAttributes.has(attribute)) {
215
+ if (!this._isPowerAllowedForObject(_objectId)) return offValue(desired);
216
+ return desired;
217
+ }
218
+ }
219
+ return desired;
220
+ }
221
+
222
+ /**
223
+ * @param {string} objectId
224
+ * @returns {boolean}
225
+ */
226
+ }, {
227
+ key: "_isPowerAllowedForObject",
228
+ value: function _isPowerAllowedForObject(objectId) {
229
+ if (!this._plantPowerDesired) return false;
230
+ var _iterator = _createForOfIteratorHelper(this._groupMembers),
231
+ _step;
232
+ try {
233
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
234
+ var _step$value = _slicedToArray(_step.value, 2),
235
+ groupId = _step$value[0],
236
+ members = _step$value[1];
237
+ if (members.has(objectId)) {
238
+ var groupOn = this._groupDesired.get(groupId);
239
+ if (!groupOn) return false;
240
+ }
241
+ }
242
+ } catch (err) {
243
+ _iterator.e(err);
244
+ } finally {
245
+ _iterator.f();
246
+ }
247
+ return true;
248
+ }
249
+
250
+ /**
251
+ * Groups that contain an object.
252
+ * @param {string} objectId
253
+ * @returns {string[]}
254
+ */
255
+ }, {
256
+ key: "getGroupsForObject",
257
+ value: function getGroupsForObject(objectId) {
258
+ var result = [];
259
+ var _iterator2 = _createForOfIteratorHelper(this._groupMembers),
260
+ _step2;
261
+ try {
262
+ for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
263
+ var _step2$value = _slicedToArray(_step2.value, 2),
264
+ groupId = _step2$value[0],
265
+ members = _step2$value[1];
266
+ if (members.has(objectId)) result.push(groupId);
267
+ }
268
+ } catch (err) {
269
+ _iterator2.e(err);
270
+ } finally {
271
+ _iterator2.f();
272
+ }
273
+ return result;
274
+ }
275
+
276
+ // ─── Writes ───────────────────────────────────────────────────────────────
277
+
278
+ /**
279
+ * Set a desired value through the single write door and run one bounded
280
+ * propagation pass: no-op stop, gate recompute, declared chains with depth
281
+ * and visited-set guards, then batched notifications (Framework §11, §12).
282
+ *
283
+ * @param {string} address
284
+ * @param {*} value
285
+ * @returns {boolean} true if the desired value changed
286
+ */
287
+ }, {
288
+ key: "setDesired",
289
+ value: function setDesired(address, value) {
290
+ this._beginBatch();
291
+ var ctx = {
292
+ visited: new Map()
293
+ };
294
+ var changed = this._writeHop(address, value, 0, ctx);
295
+ this._endBatch();
296
+ return changed;
297
+ }
298
+
299
+ /**
300
+ * One hop of a propagation pass: apply the write, then walk declared chains.
301
+ * @param {string} address
302
+ * @param {*} value
303
+ * @param {number} depth
304
+ * @param {{ visited: Map<string, *> }} ctx
305
+ * @returns {boolean} whether this hop changed the desired value
306
+ */
307
+ }, {
308
+ key: "_writeHop",
309
+ value: function _writeHop(address, value, depth, ctx) {
310
+ var result = this._applyWrite(address, value, ctx);
311
+ if (!result.changed) return false;
312
+ this._walkChains(result, depth, ctx);
313
+ return true;
314
+ }
315
+
316
+ /**
317
+ * Atomic write door: no-op stop (§11.1), visited-set cycle guard (§11.5),
318
+ * store desired, update gate mirrors, compute which effectives changed, and
319
+ * queue notifications for changed addresses only.
320
+ *
321
+ * @param {string} address
322
+ * @param {*} value
323
+ * @param {{ visited: Map<string, *> }} ctx
324
+ * @returns {{ changed: boolean, desiredChanged?: string, effectiveChanged?: Set<string> }}
325
+ */
326
+ }, {
327
+ key: "_applyWrite",
328
+ value: function _applyWrite(address, value, ctx) {
329
+ var previous = this.getDesired(address);
330
+ if (previous !== undefined && valuesEqual(previous, value)) {
331
+ return {
332
+ changed: false
333
+ }; // §11.1 no-op stop
334
+ }
335
+
336
+ // §11.5 visited-set: writing the same pair a different value this pass = cycle conflict
337
+ if (ctx.visited.has(address) && !valuesEqual(ctx.visited.get(address), value)) {
338
+ console.warn("[StateEngine] cycle conflict: ".concat(address, " rewritten to a different value in one pass; stopping branch"));
339
+ return {
340
+ changed: false
341
+ };
342
+ }
343
+ ctx.visited.set(address, value);
344
+
345
+ // Snapshot effective for every address this write might gate, before mutating.
346
+ var affected = this._affectedByWrite(address);
347
+ var before = new Map();
348
+ var _iterator3 = _createForOfIteratorHelper(affected),
349
+ _step3;
350
+ try {
351
+ for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
352
+ var a = _step3.value;
353
+ before.set(a, this.getEffective(a));
354
+ }
355
+
356
+ // Store desired + keep plant/group gate mirrors in sync.
357
+ } catch (err) {
358
+ _iterator3.e(err);
359
+ } finally {
360
+ _iterator3.f();
361
+ }
362
+ this._desired.set(address, value);
363
+ if (address === makeAddress('plant', 'power')) {
364
+ this._plantPowerDesired = Boolean(value);
365
+ }
366
+ var groupMatch = /^group:([^:]+)::power$/.exec(address);
367
+ if (groupMatch) {
368
+ this._groupDesired.set(groupMatch[1], Boolean(value));
369
+ }
370
+
371
+ // Which effectives actually changed (§11.4 dedupe: notify once, changed only).
372
+ var effectiveChanged = new Set();
373
+ var _iterator4 = _createForOfIteratorHelper(affected),
374
+ _step4;
375
+ try {
376
+ for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
377
+ var _a = _step4.value;
378
+ if (!valuesEqual(before.get(_a), this.getEffective(_a))) effectiveChanged.add(_a);
379
+ }
380
+ } catch (err) {
381
+ _iterator4.e(err);
382
+ } finally {
383
+ _iterator4.f();
384
+ }
385
+ this._queueNotification(address); // desired changed
386
+ var _iterator5 = _createForOfIteratorHelper(effectiveChanged),
387
+ _step5;
388
+ try {
389
+ for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) {
390
+ var _a2 = _step5.value;
391
+ if (_a2 !== address) this._queueNotification(_a2);
392
+ }
393
+ } catch (err) {
394
+ _iterator5.e(err);
395
+ } finally {
396
+ _iterator5.f();
397
+ }
398
+ return {
399
+ changed: true,
400
+ desiredChanged: address,
401
+ effectiveChanged: effectiveChanged
402
+ };
403
+ }
404
+
405
+ /**
406
+ * Addresses whose effective value a write to `address` could change: the
407
+ * address itself, plus gate dependents (all keys for plant power, member
408
+ * keys for group power).
409
+ * @param {string} address
410
+ * @returns {string[]}
411
+ */
412
+ }, {
413
+ key: "_affectedByWrite",
414
+ value: function _affectedByWrite(address) {
415
+ var affected = new Set([address]);
416
+ if (address === makeAddress('plant', 'power')) {
417
+ var _iterator6 = _createForOfIteratorHelper(this._desired.keys()),
418
+ _step6;
419
+ try {
420
+ for (_iterator6.s(); !(_step6 = _iterator6.n()).done;) {
421
+ var key = _step6.value;
422
+ affected.add(key);
423
+ }
424
+ } catch (err) {
425
+ _iterator6.e(err);
426
+ } finally {
427
+ _iterator6.f();
428
+ }
429
+ return _toConsumableArray(affected);
430
+ }
431
+ var groupMatch = /^group:([^:]+)::power$/.exec(address);
432
+ if (groupMatch) {
433
+ var members = this._groupMembers.get(groupMatch[1]);
434
+ if (members) {
435
+ var _iterator7 = _createForOfIteratorHelper(members),
436
+ _step7;
437
+ try {
438
+ for (_iterator7.s(); !(_step7 = _iterator7.n()).done;) {
439
+ var memberId = _step7.value;
440
+ var _iterator8 = _createForOfIteratorHelper(this._desired.keys()),
441
+ _step8;
442
+ try {
443
+ for (_iterator8.s(); !(_step8 = _iterator8.n()).done;) {
444
+ var _key = _step8.value;
445
+ if (_key.startsWith("".concat(memberId, "::"))) affected.add(_key);
446
+ }
447
+ } catch (err) {
448
+ _iterator8.e(err);
449
+ } finally {
450
+ _iterator8.f();
451
+ }
452
+ }
453
+ } catch (err) {
454
+ _iterator7.e(err);
455
+ } finally {
456
+ _iterator7.f();
457
+ }
458
+ }
459
+ }
460
+ return _toConsumableArray(affected);
461
+ }
462
+
463
+ /**
464
+ * Walk declared chains triggered by this write (§10, §11.3). Desired-source
465
+ * chains fire when the trigger's desired changed; effective-source chains fire
466
+ * when the trigger's effective changed (so plant/group-off ripples through
467
+ * `pump effective → pipe flow`, §17). Only `copy` is supported.
468
+ *
469
+ * @param {{ desiredChanged?: string, effectiveChanged?: Set<string> }} result
470
+ * @param {number} depth
471
+ * @param {{ visited: Map<string, *> }} ctx
472
+ */
473
+ }, {
474
+ key: "_walkChains",
475
+ value: function _walkChains(result, depth, ctx) {
476
+ if (this._chains.length === 0) return;
477
+ var _iterator9 = _createForOfIteratorHelper(this._chains),
478
+ _step9;
479
+ try {
480
+ for (_iterator9.s(); !(_step9 = _iterator9.n()).done;) {
481
+ var _result$effectiveChan;
482
+ var chain = _step9.value;
483
+ if (chain.action && chain.action !== 'copy') continue;
484
+ var trigger = makeAddress(chain.when.address, chain.when.attribute);
485
+ var useEffective = chain.when.source === 'effective';
486
+ var fires = useEffective ? (_result$effectiveChan = result.effectiveChanged) === null || _result$effectiveChan === void 0 ? void 0 : _result$effectiveChan.has(trigger) : result.desiredChanged === trigger;
487
+ if (!fires) continue;
488
+ if (depth + 1 > this.maxChainDepth) {
489
+ var _chain$id;
490
+ console.warn("[StateEngine] ripple depth limit (".concat(this.maxChainDepth, ") reached at chain ") + "\"".concat((_chain$id = chain.id) !== null && _chain$id !== void 0 ? _chain$id : '<unnamed>', "\" (").concat(trigger, " \u2192 ").concat(makeAddress(chain.set.address, chain.set.attribute), "); stopping path"));
491
+ continue;
492
+ }
493
+ var nextValue = useEffective ? this.getEffective(trigger) : this.getDesired(trigger);
494
+ var target = makeAddress(chain.set.address, chain.set.attribute);
495
+ this._writeHop(target, nextValue, depth + 1, ctx);
496
+ }
497
+ } catch (err) {
498
+ _iterator9.e(err);
499
+ } finally {
500
+ _iterator9.f();
501
+ }
502
+ }
503
+
504
+ // ─── Pipe lifecycle (Framework §9, §15) ─────────────────────────────────────
505
+
506
+ /**
507
+ * Register a pipe/path in the registry so its flow state exists and is tied to
508
+ * pipe lifecycle (no orphan keys). Keyed on the pipe/path id, never on segment
509
+ * declared/computed status (§9). No-op if already registered.
510
+ * @param {string} pipeId
511
+ * @param {object} [options]
512
+ * @param {boolean} [options.defaultFlow=false]
513
+ * @returns {string} the flow address
514
+ */
515
+ }, {
516
+ key: "registerPipe",
517
+ value: function registerPipe(pipeId) {
518
+ var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
519
+ var address = makeAddress(pipeId, 'flow');
520
+ if (!this._desired.has(address)) {
521
+ var _options$defaultFlow;
522
+ this._beginBatch();
523
+ this._desired.set(address, Boolean((_options$defaultFlow = options.defaultFlow) !== null && _options$defaultFlow !== void 0 ? _options$defaultFlow : false));
524
+ this._queueNotification(address);
525
+ this._endBatch();
526
+ }
527
+ return address;
528
+ }
529
+
530
+ /**
531
+ * Remove a pipe and all of its registry entries when the pipe is destroyed, so
532
+ * keys do not linger (§15). Emits a final notification (desired/effective
533
+ * undefined) per removed key.
534
+ * @param {string} pipeId
535
+ * @returns {number} number of registry keys removed
536
+ */
537
+ }, {
538
+ key: "unregisterPipe",
539
+ value: function unregisterPipe(pipeId) {
540
+ var prefix = "".concat(pipeId, "::");
541
+ var removed = _toConsumableArray(this._desired.keys()).filter(function (k) {
542
+ return k.startsWith(prefix);
543
+ });
544
+ if (removed.length === 0) return 0;
545
+ this._beginBatch();
546
+ var _iterator0 = _createForOfIteratorHelper(removed),
547
+ _step0;
548
+ try {
549
+ for (_iterator0.s(); !(_step0 = _iterator0.n()).done;) {
550
+ var key = _step0.value;
551
+ this._desired.delete(key);
552
+ }
553
+ } catch (err) {
554
+ _iterator0.e(err);
555
+ } finally {
556
+ _iterator0.f();
557
+ }
558
+ var _iterator1 = _createForOfIteratorHelper(removed),
559
+ _step1;
560
+ try {
561
+ for (_iterator1.s(); !(_step1 = _iterator1.n()).done;) {
562
+ var _key2 = _step1.value;
563
+ this._queueNotification(_key2);
564
+ }
565
+ } catch (err) {
566
+ _iterator1.e(err);
567
+ } finally {
568
+ _iterator1.f();
569
+ }
570
+ this._endBatch();
571
+ return removed.length;
572
+ }
573
+ }, {
574
+ key: "_beginBatch",
575
+ value: function _beginBatch() {
576
+ this._batching = true;
577
+ this._pendingNotifications.clear();
578
+ }
579
+ }, {
580
+ key: "_queueNotification",
581
+ value: function _queueNotification(address) {
582
+ var change = {
583
+ address: address,
584
+ desired: this.getDesired(address),
585
+ effective: this.getEffective(address)
586
+ };
587
+ if (this._batching) {
588
+ this._pendingNotifications.set(address, change);
589
+ } else {
590
+ this._emit(change);
591
+ }
592
+ }
593
+ }, {
594
+ key: "_endBatch",
595
+ value: function _endBatch() {
596
+ this._batching = false;
597
+ var changes = _toConsumableArray(this._pendingNotifications.values());
598
+ this._pendingNotifications.clear();
599
+ var _iterator10 = _createForOfIteratorHelper(changes),
600
+ _step10;
601
+ try {
602
+ for (_iterator10.s(); !(_step10 = _iterator10.n()).done;) {
603
+ var change = _step10.value;
604
+ this._emit(change);
605
+ }
606
+ } catch (err) {
607
+ _iterator10.e(err);
608
+ } finally {
609
+ _iterator10.f();
610
+ }
611
+ }
612
+ }, {
613
+ key: "_emit",
614
+ value: function _emit(change) {
615
+ var _iterator11 = _createForOfIteratorHelper(this._listeners),
616
+ _step11;
617
+ try {
618
+ for (_iterator11.s(); !(_step11 = _iterator11.n()).done;) {
619
+ var listener = _step11.value;
620
+ try {
621
+ listener(change);
622
+ } catch (err) {
623
+ console.error('[StateEngine] listener error:', err);
624
+ }
625
+ }
626
+ } catch (err) {
627
+ _iterator11.e(err);
628
+ } finally {
629
+ _iterator11.f();
630
+ }
631
+ }
632
+
633
+ /**
634
+ * @param {(change: { address: string, desired: *, effective: * }) => void} listener
635
+ * @returns {() => void} unsubscribe
636
+ */
637
+ }, {
638
+ key: "subscribe",
639
+ value: function subscribe(listener) {
640
+ var _this = this;
641
+ this._listeners.add(listener);
642
+ return function () {
643
+ return _this._listeners.delete(listener);
644
+ };
645
+ }
646
+
647
+ /**
648
+ * Snapshot of all desired values (for save / debug).
649
+ * @returns {Record<string, *>}
650
+ */
651
+ }, {
652
+ key: "toRegistry",
653
+ value: function toRegistry() {
654
+ var out = {};
655
+ var _iterator12 = _createForOfIteratorHelper(this._desired),
656
+ _step12;
657
+ try {
658
+ for (_iterator12.s(); !(_step12 = _iterator12.n()).done;) {
659
+ var _step12$value = _slicedToArray(_step12.value, 2),
660
+ k = _step12$value[0],
661
+ v = _step12$value[1];
662
+ out[k] = v;
663
+ }
664
+ } catch (err) {
665
+ _iterator12.e(err);
666
+ } finally {
667
+ _iterator12.f();
668
+ }
669
+ return out;
670
+ }
671
+
672
+ /**
673
+ * Reconstruct the §17 scene declaration (plant / groups / chains /
674
+ * stateRegistry) from current engine state, for scene save. Round-trips with
675
+ * {@link StateEngine#load}.
676
+ * @returns {{ version: string, plant: object, groups: object, chains: object[], stateRegistry: Record<string, *> }}
677
+ */
678
+ }, {
679
+ key: "toDeclaration",
680
+ value: function toDeclaration() {
681
+ var groups = {};
682
+ var _iterator13 = _createForOfIteratorHelper(this._groupMembers),
683
+ _step13;
684
+ try {
685
+ for (_iterator13.s(); !(_step13 = _iterator13.n()).done;) {
686
+ var _this$_groupDesired$g;
687
+ var _step13$value = _slicedToArray(_step13.value, 2),
688
+ groupId = _step13$value[0],
689
+ members = _step13$value[1];
690
+ groups[groupId] = {
691
+ members: _toConsumableArray(members),
692
+ states: {
693
+ power: {
694
+ type: 'Boolean',
695
+ default: (_this$_groupDesired$g = this._groupDesired.get(groupId)) !== null && _this$_groupDesired$g !== void 0 ? _this$_groupDesired$g : true,
696
+ gate: true
697
+ }
698
+ }
699
+ };
700
+ }
701
+ } catch (err) {
702
+ _iterator13.e(err);
703
+ } finally {
704
+ _iterator13.f();
705
+ }
706
+ return {
707
+ version: '3.0',
708
+ plant: {
709
+ states: {
710
+ power: {
711
+ type: 'Boolean',
712
+ default: this._plantPowerDesired,
713
+ gate: true
714
+ }
715
+ }
716
+ },
717
+ groups: groups,
718
+ chains: _toConsumableArray(this._chains),
719
+ stateRegistry: this.toRegistry()
720
+ };
721
+ }
722
+ }]);
723
+ }();
724
+
725
+ export { StateEngine, StateEngine as default, makeAddress, offValue, valuesEqual };