@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.
- package/dist/bundle/index.js +1635 -32
- package/dist/cjs/src/core/centralPlant.js +80 -11
- package/dist/cjs/src/core/centralPlantInternals.js +4 -0
- package/dist/cjs/src/core/centralPlantValidator.js +10 -6
- package/dist/cjs/src/core/stateEngine.js +733 -0
- package/dist/cjs/src/core/stateEngineScene.js +97 -0
- package/dist/cjs/src/index.js +10 -0
- package/dist/cjs/src/managers/pathfinding/PathFlowManager.js +94 -13
- package/dist/cjs/src/managers/pathfinding/PathRenderingManager.js +15 -0
- package/dist/cjs/src/managers/scene/componentTooltipManager.js +7 -1
- package/dist/cjs/src/managers/scene/controlPanelManager.js +377 -0
- package/dist/cjs/src/managers/scene/sceneExportManager.js +10 -1
- package/dist/cjs/src/managers/state/EffectiveVisualManager.js +270 -0
- package/dist/esm/src/core/centralPlant.js +80 -11
- package/dist/esm/src/core/centralPlantInternals.js +4 -0
- package/dist/esm/src/core/centralPlantValidator.js +10 -6
- package/dist/esm/src/core/stateEngine.js +725 -0
- package/dist/esm/src/core/stateEngineScene.js +90 -0
- package/dist/esm/src/index.js +2 -0
- package/dist/esm/src/managers/pathfinding/PathFlowManager.js +94 -13
- package/dist/esm/src/managers/pathfinding/PathRenderingManager.js +15 -0
- package/dist/esm/src/managers/scene/componentTooltipManager.js +7 -1
- package/dist/esm/src/managers/scene/controlPanelManager.js +352 -0
- package/dist/esm/src/managers/scene/sceneExportManager.js +10 -1
- package/dist/esm/src/managers/state/EffectiveVisualManager.js +266 -0
- package/dist/index.d.ts +85 -0
- 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 };
|