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