@things-factory/headless-twin 10.0.7 → 10.0.9
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-server/engine/attention-digest.d.ts +52 -0
- package/dist-server/engine/attention-digest.js +76 -0
- package/dist-server/engine/attention-digest.js.map +1 -0
- package/dist-server/engine/board-vocabulary.d.ts +17 -0
- package/dist-server/engine/board-vocabulary.js +63 -0
- package/dist-server/engine/board-vocabulary.js.map +1 -0
- package/dist-server/engine/command-routing.d.ts +33 -0
- package/dist-server/engine/command-routing.js +53 -0
- package/dist-server/engine/command-routing.js.map +1 -0
- package/dist-server/engine/entity-delta.d.ts +13 -6
- package/dist-server/engine/entity-delta.js +38 -12
- package/dist-server/engine/entity-delta.js.map +1 -1
- package/dist-server/engine/index.d.ts +6 -0
- package/dist-server/engine/index.js +10 -0
- package/dist-server/engine/index.js.map +1 -1
- package/dist-server/engine/kpi-baseline.d.ts +78 -0
- package/dist-server/engine/kpi-baseline.js +123 -0
- package/dist-server/engine/kpi-baseline.js.map +1 -0
- package/dist-server/engine/kpi-broadcast.d.ts +4 -0
- package/dist-server/engine/kpi-broadcast.js +16 -0
- package/dist-server/engine/kpi-broadcast.js.map +1 -0
- package/dist-server/engine/kpi-query.d.ts +31 -0
- package/dist-server/engine/kpi-query.js +50 -2
- package/dist-server/engine/kpi-query.js.map +1 -1
- package/dist-server/engine/live-attentions.d.ts +1 -0
- package/dist-server/engine/live-attentions.js +7 -1
- package/dist-server/engine/live-attentions.js.map +1 -1
- package/dist-server/engine/runtime-key.d.ts +15 -0
- package/dist-server/engine/runtime-key.js +64 -0
- package/dist-server/engine/runtime-key.js.map +1 -0
- package/dist-server/engine/state-axes.d.ts +16 -0
- package/dist-server/engine/state-axes.js +54 -0
- package/dist-server/engine/state-axes.js.map +1 -0
- package/dist-server/engine/twin-engine.d.ts +92 -16
- package/dist-server/engine/twin-engine.js +335 -77
- package/dist-server/engine/twin-engine.js.map +1 -1
- package/dist-server/engine/twin-level.d.ts +23 -0
- package/dist-server/engine/twin-level.js +52 -0
- package/dist-server/engine/twin-level.js.map +1 -0
- package/dist-server/engine/warm-start.d.ts +58 -12
- package/dist-server/engine/warm-start.js +80 -9
- package/dist-server/engine/warm-start.js.map +1 -1
- package/dist-server/service/reference/discovery-result.d.ts +34 -0
- package/dist-server/service/reference/discovery-result.js +84 -0
- package/dist-server/service/reference/discovery-result.js.map +1 -0
- package/dist-server/service/reference/ingest-space.d.ts +30 -0
- package/dist-server/service/reference/ingest-space.js +63 -0
- package/dist-server/service/reference/ingest-space.js.map +1 -0
- package/dist-server/service/reference/knob-defaults.d.ts +20 -0
- package/dist-server/service/reference/knob-defaults.js +59 -0
- package/dist-server/service/reference/knob-defaults.js.map +1 -0
- package/dist-server/service/reference/reference-live.js +2 -2
- package/dist-server/service/reference/reference-live.js.map +1 -1
- package/dist-server/service/reference/reference-master.d.ts +37 -2
- package/dist-server/service/reference/reference-master.js +55 -5
- package/dist-server/service/reference/reference-master.js.map +1 -1
- package/dist-server/service/reference/reference-resolver.d.ts +2 -2
- package/dist-server/service/reference/reference-resolver.js +71 -15
- package/dist-server/service/reference/reference-resolver.js.map +1 -1
- package/dist-server/service/twin-attention/twin-attention-query.d.ts +8 -1
- package/dist-server/service/twin-attention/twin-attention-query.js +39 -8
- package/dist-server/service/twin-attention/twin-attention-query.js.map +1 -1
- package/dist-server/service/twin-control/twin-control-mutation.d.ts +2 -0
- package/dist-server/service/twin-control/twin-control-mutation.js +29 -10
- package/dist-server/service/twin-control/twin-control-mutation.js.map +1 -1
- package/dist-server/service/twin-forecast/twin-forecast-query.js +2 -1
- package/dist-server/service/twin-forecast/twin-forecast-query.js.map +1 -1
- package/dist-server/service/twin-instance/twin-instance.js +4 -2
- package/dist-server/service/twin-instance/twin-instance.js.map +1 -1
- package/dist-server/service/twin-journal/twin-journal-query.d.ts +6 -2
- package/dist-server/service/twin-journal/twin-journal-query.js +25 -7
- package/dist-server/service/twin-journal/twin-journal-query.js.map +1 -1
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.d.ts +8 -0
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +23 -1
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js.map +1 -1
- package/dist-server/service/twin-metrics/twin-metrics-query.js +1 -1
- package/dist-server/service/twin-metrics/twin-metrics-query.js.map +1 -1
- package/dist-server/service/twin-space/twin-space-resolver.js +9 -0
- package/dist-server/service/twin-space/twin-space-resolver.js.map +1 -1
- package/dist-server/service/twin-state/twin-state-subscription.js +1 -1
- package/dist-server/service/twin-state/twin-state-subscription.js.map +1 -1
- package/dist-server/service/twin-structure/twin-structure.js +2 -1
- package/dist-server/service/twin-structure/twin-structure.js.map +1 -1
- package/dist-server/service/twin-target/twin-target-resolver.js +21 -4
- package/dist-server/service/twin-target/twin-target-resolver.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -6
- package/server/engine/attention-digest.ts +102 -0
- package/server/engine/board-vocabulary.ts +61 -0
- package/server/engine/command-routing.ts +67 -0
- package/server/engine/entity-delta.ts +33 -11
- package/server/engine/index.ts +10 -0
- package/server/engine/kpi-baseline.ts +202 -0
- package/server/engine/kpi-broadcast.ts +13 -0
- package/server/engine/kpi-query.ts +81 -3
- package/server/engine/live-attentions.ts +7 -2
- package/server/engine/runtime-key.ts +58 -0
- package/server/engine/state-axes.ts +55 -0
- package/server/engine/twin-engine.ts +350 -77
- package/server/engine/twin-level.ts +48 -0
- package/server/engine/warm-start.ts +130 -16
- package/server/service/reference/discovery-result.ts +95 -0
- package/server/service/reference/ingest-space.ts +70 -0
- package/server/service/reference/knob-defaults.ts +59 -0
- package/server/service/reference/reference-live.ts +2 -2
- package/server/service/reference/reference-master.ts +94 -8
- package/server/service/reference/reference-resolver.ts +80 -16
- package/server/service/twin-attention/twin-attention-query.ts +43 -6
- package/server/service/twin-control/twin-control-mutation.ts +31 -12
- package/server/service/twin-forecast/twin-forecast-query.ts +3 -2
- package/server/service/twin-instance/twin-instance.ts +6 -2
- package/server/service/twin-journal/twin-journal-query.ts +35 -5
- package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +18 -2
- package/server/service/twin-metrics/twin-metrics-query.ts +1 -1
- package/server/service/twin-space/twin-space-resolver.ts +10 -1
- package/server/service/twin-state/twin-state-subscription.ts +1 -1
- package/server/service/twin-structure/twin-structure.ts +4 -1
- package/server/service/twin-target/twin-target-resolver.ts +22 -4
- package/test/attention-digest.test.ts +135 -0
- package/test/board-vocabulary.test.ts +114 -0
- package/test/capability-mapping.test.ts +4 -4
- package/test/command-routing.test.ts +61 -0
- package/test/discovery-result.test.ts +75 -0
- package/test/entity-delta.test.ts +25 -25
- package/test/ingest-bench.test.ts +3 -3
- package/test/ingest-space.test.ts +50 -0
- package/test/knob-defaults.test.ts +72 -0
- package/test/kpi-baseline-db.test.ts +214 -0
- package/test/kpi-baseline.test.ts +196 -0
- package/test/kpi-query-bench.test.ts +128 -0
- package/test/live-mirror-parity.test.ts +35 -2
- package/test/master-to-twin.test.ts +7 -3
- package/test/mutation-gate.test.ts +108 -0
- package/test/oee-accumulator.test.ts +65 -1
- package/test/registry-key-guard.test.ts +80 -0
- package/test/runtime-key.test.ts +66 -0
- package/test/scale-twin-bench.test.ts +2 -2
- package/test/state-axes.test.ts +74 -0
- package/test/streamline-e2e.test.ts +2 -2
- package/test/structure-revision-db.test.ts +5 -4
- package/test/tenant-registry-db.test.ts +149 -0
- package/test/warm-start-seam.test.ts +140 -0
- package/test/warm-start.test.ts +138 -4
|
@@ -14,6 +14,8 @@ const cache_service_1 = require("@things-factory/cache-service");
|
|
|
14
14
|
const twin_event_js_1 = require("../service/twin-event/twin-event.js");
|
|
15
15
|
const twin_event_keys_js_1 = require("../service/twin-event/twin-event-keys.js");
|
|
16
16
|
const warm_start_js_1 = require("./warm-start.js");
|
|
17
|
+
const runtime_key_js_1 = require("./runtime-key.js");
|
|
18
|
+
const command_routing_js_1 = require("./command-routing.js");
|
|
17
19
|
const twin_instance_js_1 = require("../service/twin-instance/twin-instance.js");
|
|
18
20
|
const twin_structure_js_1 = require("../service/twin-structure/twin-structure.js");
|
|
19
21
|
const twin_space_js_1 = require("../service/twin-space/twin-space.js");
|
|
@@ -22,6 +24,8 @@ const twin_space_representation_js_1 = require("../service/twin-space/twin-space
|
|
|
22
24
|
const twin_space_area_js_1 = require("../service/twin-space/twin-space-area.js");
|
|
23
25
|
const twin_area_js_1 = require("../service/twin-space/twin-area.js");
|
|
24
26
|
const reference_master_js_2 = require("../service/reference/reference-master.js");
|
|
27
|
+
const ingest_space_js_1 = require("../service/reference/ingest-space.js");
|
|
28
|
+
const reference_master_js_3 = require("../service/reference/reference-master.js");
|
|
25
29
|
const travel_estimator_js_1 = require("./travel-estimator.js");
|
|
26
30
|
const measured_estimator_js_1 = require("./measured-estimator.js");
|
|
27
31
|
const kpi_query_js_1 = require("./kpi-query.js");
|
|
@@ -29,13 +33,23 @@ const node_crypto_1 = require("node:crypto");
|
|
|
29
33
|
const entity_delta_js_1 = require("./entity-delta.js");
|
|
30
34
|
const structure_diff_js_1 = require("./structure-diff.js");
|
|
31
35
|
const oee_accumulator_js_1 = require("./oee-accumulator.js");
|
|
36
|
+
const live_attentions_js_1 = require("./live-attentions.js");
|
|
37
|
+
const attention_digest_js_1 = require("./attention-digest.js");
|
|
32
38
|
/* 커널 런타임 로드 — CJS 번들(dist-cjs). 타입은 위 import type 로. replay = 이벤트열→상태 재구성(복구·시간여행). */
|
|
33
|
-
const { WmsKernel, YmsKernel, MesKernel, TwinRuntime, StateProjector, replay, replaySegments, DOMAIN_CATALOG, OP_EVENT } = require('@operato/twin-kernel');
|
|
39
|
+
const { WmsKernel, YmsKernel, MesKernel, TwinRuntime, StateProjector, replay, replaySegments, readBoardLocations, readBoardEquipment, DOMAIN_CATALOG, OP_EVENT } = require('@operato/twin-kernel');
|
|
34
40
|
const KERNELS = { wms: WmsKernel, yms: YmsKernel, mes: MesKernel };
|
|
35
41
|
exports.DEFAULT_REALITY_MODE = 'sim-experiment';
|
|
36
42
|
class TwinEngine {
|
|
43
|
+
/*
|
|
44
|
+
* 떠 있는 런타임 — **키는 `runtimeKey(domainId, instanceId)`** 다(`runtime-key.ts` 에 이유).
|
|
45
|
+
*
|
|
46
|
+
* 예전엔 `instanceId` 하나로 키를 잡았다. 그런데 정체성은 `(domain, instanceId)` 이고 DB 유일성도
|
|
47
|
+
* 그쪽이라, 두 테넌트가 같은 id 를 쓰면(레퍼런스 경로가 소스 이름을 id 로 쓴다) 한 자리를 다퉜다.
|
|
48
|
+
* **엔진 밖에서 이 맵을 직접 색인하지 않는다** — `owns`·`runtime`·`kernel` 같은 접근자를 쓴다.
|
|
49
|
+
*/
|
|
37
50
|
static { this.instances = {}; }
|
|
38
51
|
/** 라이브 런타임이 없을 때(복구 후 미기동) 저널에서 재구성한 상태 캐시. */
|
|
52
|
+
/** 웜스타트 씨앗 — `instances` 와 **같은 키**(겹치면 남의 스냅샷으로 재고가 섞인다). */
|
|
39
53
|
static { this.recovered = {}; }
|
|
40
54
|
static { this.TICK_MS = 1000; }
|
|
41
55
|
/* 최신 스냅샷 영속(warm-start) — 재기동 시 저널 fold-from-0 replay 대신 마지막 라이브 스냅샷으로 복원.
|
|
@@ -46,10 +60,15 @@ class TwinEngine {
|
|
|
46
60
|
static { this.CHECKPOINT_MS = 20000; } // 체크포인트 주기(핫 브로드캐스트 경로와 분리, O(state) 스로틀)
|
|
47
61
|
/** 최신 스냅샷을 cache-service 에 체크포인트(도메인+instanceId 키). display-only·비차단·오류흡수. */
|
|
48
62
|
static async persistSnapshot(domainId, instanceId) {
|
|
49
|
-
const inst = this.instances[instanceId];
|
|
63
|
+
const inst = this.instances[(0, runtime_key_js_1.runtimeKey)(domainId, instanceId)];
|
|
50
64
|
if (!inst)
|
|
51
65
|
return;
|
|
52
|
-
|
|
66
|
+
/*
|
|
67
|
+
* **봉투가 아니라 상태를 저장한다.** `snapshot()` 은 시뮬에서 `runtime.resync()` 봉투를 주는데,
|
|
68
|
+
* 그것을 다시 `{revision, state}` 로 감싸 넣어 왔다 → 꺼낸 값에 축이 하나도 없어 웜스타트가
|
|
69
|
+
* 조용히 넘어갔다(저널에 수천 건이 있어도 트윈이 빈 채로 떴다).
|
|
70
|
+
*/
|
|
71
|
+
const state = (0, warm_start_js_1.unwrapState)(this.snapshot(domainId, instanceId));
|
|
53
72
|
if (!state)
|
|
54
73
|
return;
|
|
55
74
|
const revision = inst.revision ?? state.revision ?? 0;
|
|
@@ -65,7 +84,8 @@ class TwinEngine {
|
|
|
65
84
|
if (this.checkpointTimer)
|
|
66
85
|
return;
|
|
67
86
|
this.checkpointTimer = setInterval(() => {
|
|
68
|
-
for (const [
|
|
87
|
+
for (const [key, inst] of Object.entries(this.instances)) {
|
|
88
|
+
const { instanceId } = (0, runtime_key_js_1.parseRuntimeKey)(key);
|
|
69
89
|
this.persistSnapshot(inst.domainId, instanceId).catch(err => console.error(`[twin-engine] snapshot checkpoint fail "${instanceId}"`, err?.message ?? err));
|
|
70
90
|
}
|
|
71
91
|
}, this.CHECKPOINT_MS);
|
|
@@ -114,13 +134,14 @@ class TwinEngine {
|
|
|
114
134
|
// 없으면 저널 fold-from-0 replay(진실 폴백 — replay 는 라이브 파생상태를 못 담으므로 캐시가 더 충실).
|
|
115
135
|
const cached = await this.loadSnapshot(row.domainId, row.instanceId).catch(() => null);
|
|
116
136
|
if (cached?.state) {
|
|
117
|
-
|
|
137
|
+
/* 예전에 겹포장으로 저장된 값이 남아 있을 수 있다 — 읽는 쪽에서도 벗긴다(한 번은 반드시 만난다). */
|
|
138
|
+
this.recovered[(0, runtime_key_js_1.runtimeKey)(row.domainId, row.instanceId)] = { revision: cached.revision, state: (0, warm_start_js_1.unwrapState)(cached.state) };
|
|
118
139
|
console.log(`[twin-engine] warm-started "${row.instanceId}" from snapshot cache → revision ${cached.revision}.`);
|
|
119
140
|
continue;
|
|
120
141
|
}
|
|
121
142
|
const state = await this.recover(row.domainId, row.instanceId).catch(() => null);
|
|
122
143
|
if (state) {
|
|
123
|
-
this.recovered[row.instanceId] = { revision: state.revision, state };
|
|
144
|
+
this.recovered[(0, runtime_key_js_1.runtimeKey)(row.domainId, row.instanceId)] = { revision: state.revision, state };
|
|
124
145
|
console.log(`[twin-engine] recovered "${row.instanceId}" from journal → revision ${state.revision}.`);
|
|
125
146
|
}
|
|
126
147
|
}
|
|
@@ -134,25 +155,33 @@ class TwinEngine {
|
|
|
134
155
|
* 웜스타트 — 기동하는 커널에 **직전 관측 상태**를 심는다.
|
|
135
156
|
*
|
|
136
157
|
* ── 왜 필요한가 ─────────────────────────────────────────────────────────────
|
|
137
|
-
* `loadBoard` 는 **구조만** 싣는다(
|
|
158
|
+
* `loadBoard` 는 **구조만** 싣는다(자리·설비). 상태(무엇이 어디에 얼마나)는 없다. 그래서 재기동한
|
|
138
159
|
* 트윈은 저널에 입고 540건이 남아 있어도 재고가 0 이었고, 화면은 "보유 중인 것이 없습니다" 라고
|
|
139
160
|
* 말했다 — 있는 재고를 없다고 하는 셈이다(2026-07-31 hatiolab-wms 실측으로 확인).
|
|
140
161
|
* `bootstrap()` 이 이미 체크포인트 캐시(없으면 저널 replay)로 상태를 복구해 `recovered` 에 담아 두는데,
|
|
141
162
|
* 기동 순간 그걸 **버리고** 있었다. 반만 연결돼 있던 장치를 잇는다.
|
|
142
163
|
*
|
|
143
|
-
* ──
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
164
|
+
* ── 반쪽이던 복구를 마무리한다 (2026-08-05) ─────────────────────────────────
|
|
165
|
+
* 재고는 살아났는데 **진행 중 주문이 통째로 사라진** 화면이 남아 있었다. 씨앗이 자리·물품·설비
|
|
166
|
+
* 셋만 넘겼기 때문이다. 그 근거는 "스냅샷 오더에 progress 밖에 없다" 였고, 그때는 맞았다 —
|
|
167
|
+
* 그러나 커널 `OrderState` 가 원값(`requested`·`fulfilled`·`lines`)을 되찾은 뒤에도 이 자리만
|
|
168
|
+
* 그대로 남았다. 오류를 내지 않는 종류라 오래 버텼다.
|
|
169
|
+
*
|
|
170
|
+
* 지금은 **관측 스냅샷 전체**를 넘긴다(오더·작업·사람·자산 포함). 시뮬은 커널 `snapshot()`,
|
|
171
|
+
* 라이브는 `StateProjector` 가 그 일곱 축을 모두 담는다. 무엇을 심을 수 있는지 판정하는 규칙
|
|
172
|
+
* (이행 완료 오더 제외·고아 작업 제외)은 커널에 있고, 호스트가 미리 골라내지 않는다.
|
|
173
|
+
*
|
|
174
|
+
* ── 남은 한계는 숨기지 않고 센다 ────────────────────────────────────────────
|
|
175
|
+
* · 원값이 없는 오더(progress 만 있는 것)는 남은 수량을 알 수 없어 심지 않는다. **지어내지 않는
|
|
176
|
+
* 대신 몇 건인지 말한다** — 세지 않으면 "주문이 없다" 와 "주문을 못 심었다" 가 화면에서 같아진다.
|
|
177
|
+
* · 진행 중 개별 작업의 내부 상태는 관측만으로 완전히 복원되지 않는다(커널이 명시한 한계, 재계획에 맡김).
|
|
149
178
|
*
|
|
150
179
|
* ── 벤치는 시드하지 않는다 ──────────────────────────────────────────────────
|
|
151
180
|
* 부하 벤치는 **새 시작에서 용량을 재는 것**이 목적이라 현재 상태를 심으면 측정이 오염된다.
|
|
152
181
|
*/
|
|
153
|
-
static warmStart(id, kernel, purpose) {
|
|
182
|
+
static warmStart(domainId, id, kernel, purpose) {
|
|
154
183
|
const hydrate = kernel.hydrateObserved;
|
|
155
|
-
const plan = (0, warm_start_js_1.planWarmStart)(this.recovered[id]?.state, purpose, typeof hydrate === 'function');
|
|
184
|
+
const plan = (0, warm_start_js_1.planWarmStart)(this.recovered[(0, runtime_key_js_1.runtimeKey)(domainId, id)]?.state, purpose, typeof hydrate === 'function');
|
|
156
185
|
if (plan.action === 'skip') {
|
|
157
186
|
if (plan.reason === 'bench') {
|
|
158
187
|
console.log(`[twin-engine] "${id}" is a bench twin — starting empty on purpose (seeding would skew the measurement).`);
|
|
@@ -163,8 +192,22 @@ class TwinEngine {
|
|
|
163
192
|
return;
|
|
164
193
|
}
|
|
165
194
|
hydrate.call(kernel, plan.seed);
|
|
166
|
-
|
|
167
|
-
|
|
195
|
+
const restored = [
|
|
196
|
+
`${plan.itemCount} item(s)`,
|
|
197
|
+
`${plan.equipmentCount} equipment`,
|
|
198
|
+
`${plan.orderCount} open order(s)`,
|
|
199
|
+
`${plan.taskCount} task(s)`,
|
|
200
|
+
...(plan.personCount ? [`${plan.personCount} person(s)`] : []),
|
|
201
|
+
...(plan.assetCount ? [`${plan.assetCount} asset(s)`] : []),
|
|
202
|
+
/* 확인해 둔 신호를 이어받았다는 사실도 말한다 — 잃으면 확인 처리가 다시 빨개지는 것으로 보인다. */
|
|
203
|
+
...(plan.ackedCount ? [`${plan.ackedCount} acknowledged attention(s)`] : [])
|
|
204
|
+
].join(', ');
|
|
205
|
+
console.log(`[twin-engine] warm-started "${id}" — restored ${restored}.`);
|
|
206
|
+
/* 뺀 것은 조용히 넘기지 않는다 — 지어내지 않았다는 사실 자체를 말해야 화면의 빈칸이 읽힌다. */
|
|
207
|
+
if (plan.ordersWithoutDemand > 0) {
|
|
208
|
+
console.warn(`[twin-engine] "${id}": ${plan.ordersWithoutDemand} order(s) could not be restored — they carry progress only, ` +
|
|
209
|
+
'with no requested/fulfilled counts, so the remaining demand is unknown. They are left out rather than guessed.');
|
|
210
|
+
}
|
|
168
211
|
}
|
|
169
212
|
/** 트윈 인스턴스 시작 — 커널 생성 + 보드 로드 + 직전 상태 웜스타트 + State 스트림 브리지 + 워커 tick + 레지스트리 영속. */
|
|
170
213
|
/**
|
|
@@ -280,8 +323,8 @@ class TwinEngine {
|
|
|
280
323
|
where.spaceId = target.spaceId;
|
|
281
324
|
const registered = await (0, shell_1.getRepository)(twin_instance_js_1.TwinInstance).find({ where });
|
|
282
325
|
return registered.map(reg => {
|
|
283
|
-
const rt = this.instances[reg.instanceId];
|
|
284
|
-
const kernel = rt?.
|
|
326
|
+
const rt = this.instances[(0, runtime_key_js_1.runtimeKey)(domainId, reg.instanceId)];
|
|
327
|
+
const kernel = rt?.kernel; // 키에 도메인이 있으므로 남의 런타임을 집을 수 없다
|
|
285
328
|
if (typeof kernel?.capacity !== 'function')
|
|
286
329
|
return { instanceId: reg.instanceId, kind: reg.kind, reason: 'not-running' };
|
|
287
330
|
const analysis = kernel.capacity({ unitsPerDay, sampleWeekStartMs });
|
|
@@ -293,15 +336,16 @@ class TwinEngine {
|
|
|
293
336
|
});
|
|
294
337
|
}
|
|
295
338
|
static start(id, domainId, kind, board, realityMode, purpose, resumeFrom) {
|
|
296
|
-
|
|
297
|
-
|
|
339
|
+
const key = (0, runtime_key_js_1.runtimeKey)(domainId, id);
|
|
340
|
+
if (this.instances[key])
|
|
341
|
+
return this.instances[key];
|
|
298
342
|
const Kernel = KERNELS[kind] ?? WmsKernel;
|
|
299
|
-
const kernel = new Kernel(domainId, undefined, this.
|
|
343
|
+
const kernel = new Kernel(domainId, undefined, this.productionSpecOf(board));
|
|
300
344
|
kernel.loadBoard(board); // 구조만. 상태는 아래 웜스타트가 심는다.
|
|
301
345
|
this.applyOperations(kernel, board, id); // 시간·수율 명세(있으면) — 없으면 커널 기본값
|
|
302
346
|
/* 추정기는 DB 조회를 포함해 비동기 — 기동을 막지 않고 붙는다(붙기 전 작업은 명세·상수로 산출). */
|
|
303
347
|
this.installEstimators(kernel, domainId, id, board).catch(err => console.warn('[twin-engine] estimator install failed', err?.message));
|
|
304
|
-
this.warmStart(id, kernel, purpose);
|
|
348
|
+
this.warmStart(domainId, id, kernel, purpose);
|
|
305
349
|
/*
|
|
306
350
|
* **번호를 이어 센다** — 저널에 이미 있는 번호와 겹치지 않게.
|
|
307
351
|
*
|
|
@@ -313,8 +357,8 @@ class TwinEngine {
|
|
|
313
357
|
const runtime = new TwinRuntime(kernel);
|
|
314
358
|
/* subscribe 는 RuntimeSubscription({ unsubscribe() }) 반환 → () => void 로 감쌈. */
|
|
315
359
|
const inst = { id, domainId, runtime, kernel, realityMode: realityMode ?? exports.DEFAULT_REALITY_MODE, unsub: () => { } };
|
|
316
|
-
this.instances[
|
|
317
|
-
delete this.recovered[
|
|
360
|
+
this.instances[key] = inst;
|
|
361
|
+
delete this.recovered[key]; // 웜스타트로 커널에 옮겨 심었다 — 이제 라이브가 진실이다.
|
|
318
362
|
/* 라이브 바인딩(P3): data 채널 필터가 subdomain 을 보므로 Domain 객체를 1회 해석해 둔다. */
|
|
319
363
|
(0, shell_1.getRepository)(shell_1.Domain).findOne({ where: { id: domainId } }).then(d => (inst.domain = d)).catch(() => { });
|
|
320
364
|
/* State 채널: runtime.subscribe(snapshot→delta→clock) → pubsub 방송(구독 리졸버가 instanceId 필터). */
|
|
@@ -332,7 +376,7 @@ class TwinEngine {
|
|
|
332
376
|
/* 복구 앵커: 레지스트리에 board/kind/status/realityMode 영속(재부팅 시 이게 있어야 replay·선언 거동 가능). */
|
|
333
377
|
this.register(domainId, id, kind, board, 'running', inst.realityMode).catch(err => console.error('twin register fail', err));
|
|
334
378
|
/* 워커 tick — 스켈레톤은 setInterval(메인 루프). 긴 시뮬 오프-루프(worker thread)는 스케일 하드닝(향후, §host 경계). */
|
|
335
|
-
inst.timer = setInterval(() =>
|
|
379
|
+
inst.timer = setInterval(() => this.tickGuarded(domainId, id, runtime), this.TICK_MS);
|
|
336
380
|
return inst;
|
|
337
381
|
}
|
|
338
382
|
/**
|
|
@@ -357,8 +401,23 @@ class TwinEngine {
|
|
|
357
401
|
*
|
|
358
402
|
* 보드에 없으면 `undefined` — 레거시 경로 그대로다(기존 트윈의 거동을 바꾸지 않는다).
|
|
359
403
|
*/
|
|
360
|
-
|
|
361
|
-
|
|
404
|
+
/**
|
|
405
|
+
* 생산 선언을 꺼낸다 — 커널 생성자에 넘긴다.
|
|
406
|
+
*
|
|
407
|
+
* **옛 이름(`mesSpec`)은 읽지 않는다.** 그것은 계약에 선언조차 없던 필드였고(MES 커널 생성자 인자
|
|
408
|
+
* 이름이 그대로 굳은 것), 일반 기제에 한 시스템 이름이 붙어 있었기 때문에 창고 트윈이 이 자리를
|
|
409
|
+
* 쓰지 못했다. 별명으로 남겨 두면 그 혼동이 계속되므로 하나로 통일했다.
|
|
410
|
+
*
|
|
411
|
+
* 옛 이름만 가진 보드가 있으면 **조용히 생산 선언을 잃는 대신 분명히 멈춘다** — 그 트윈은 공정이
|
|
412
|
+
* 없는 채로 돌게 되고(라인이 서 있는 창고), 원인을 찾기 어렵다.
|
|
413
|
+
*/
|
|
414
|
+
static productionSpecOf(board) {
|
|
415
|
+
const legacy = board?.mesSpec;
|
|
416
|
+
if (legacy && !board?.productionSpec) {
|
|
417
|
+
throw new Error('board carries the retired `mesSpec` field — rename it to `productionSpec` ' +
|
|
418
|
+
'(same shape; the name was tied to one kernel while the declaration is ISA-95 operations + BOM)');
|
|
419
|
+
}
|
|
420
|
+
return board?.productionSpec;
|
|
362
421
|
}
|
|
363
422
|
/**
|
|
364
423
|
* 현장(공간)의 **시각 기준**을 보드에 얹는다 — 커널이 교대의 `HH:MM` 을 읽을 기준.
|
|
@@ -385,10 +444,11 @@ class TwinEngine {
|
|
|
385
444
|
return { ...board, utcOffsetMinutes: offset };
|
|
386
445
|
}
|
|
387
446
|
static startLive(id, domainId, kind, board) {
|
|
388
|
-
|
|
389
|
-
|
|
447
|
+
const key = (0, runtime_key_js_1.runtimeKey)(domainId, id);
|
|
448
|
+
if (this.instances[key])
|
|
449
|
+
return this.instances[key];
|
|
390
450
|
const Kernel = KERNELS[kind] ?? WmsKernel;
|
|
391
|
-
const kernel = new Kernel(domainId, undefined, this.
|
|
451
|
+
const kernel = new Kernel(domainId, undefined, this.productionSpecOf(board));
|
|
392
452
|
kernel.loadBoard(board);
|
|
393
453
|
this.applyOperations(kernel, board, id); // 명세는 라이브에도 실린다(예측 자격이 sim 과 같아진다)
|
|
394
454
|
/* `projector` 필드는 옛 이름으로 남긴다 — 소비처가 `snapshot()` 을 부르므로 얇은 어댑터로 잇는다.
|
|
@@ -398,8 +458,8 @@ class TwinEngine {
|
|
|
398
458
|
/* 추정기(실측·거리)도 라이브에 붙인다 — 예측이 상수로 돌지 않게. 기동을 막지 않는다. */
|
|
399
459
|
this.installEstimators(kernel, domainId, id, board).catch(err => console.warn('[twin-engine] estimator install failed', err?.message));
|
|
400
460
|
inst.metrics = { ingestedTotal: 0, broadcastTotal: 0, journaledTotal: 0, ingestRate: 0, broadcastRate: 0, journalRate: 0, backlog: 0, _accIngest: 0, _accBroadcast: 0, _accJournal: 0, _windowStartMs: Date.now() };
|
|
401
|
-
this.instances[
|
|
402
|
-
delete this.recovered[
|
|
461
|
+
this.instances[key] = inst;
|
|
462
|
+
delete this.recovered[key];
|
|
403
463
|
/* 라이브 바인딩(data 채널) subdomain 필터용 Domain 1회 해석(sim 과 동일). */
|
|
404
464
|
(0, shell_1.getRepository)(shell_1.Domain).findOne({ where: { id: domainId } }).then(d => (inst.domain = d)).catch(() => { });
|
|
405
465
|
/* 저널 revision 카운터 시드 — 기존 저널 최대치에서 이어붙임(재기동 시 revision 충돌 방지). 이후 인메모리 증가. */
|
|
@@ -415,8 +475,8 @@ class TwinEngine {
|
|
|
415
475
|
* reference 어댑터가 낸 records → 커널 face2-adapter.ingest → CanonicalEnvelope 를 여기로 밀어넣는다.
|
|
416
476
|
* (State 채널 델타/저널 결선은 후속 — 스켈레톤은 data(tag) 미러 중심.)
|
|
417
477
|
*/
|
|
418
|
-
static ingestLive(id, envelopes) {
|
|
419
|
-
const inst = this.instances[id];
|
|
478
|
+
static ingestLive(domainId, id, envelopes) {
|
|
479
|
+
const inst = this.instances[(0, runtime_key_js_1.runtimeKey)(domainId, id)];
|
|
420
480
|
if (inst?.mode !== 'live' || !inst.projector)
|
|
421
481
|
return;
|
|
422
482
|
for (const e of envelopes) {
|
|
@@ -436,6 +496,18 @@ class TwinEngine {
|
|
|
436
496
|
inst.dirty = true;
|
|
437
497
|
this.ensureBroadcastCoalescer();
|
|
438
498
|
}
|
|
499
|
+
/**
|
|
500
|
+
* 구간 성과 방송은 **없앴다**(2026-08-06). 카드가 `twinKpi` 를 직접 묻는다.
|
|
501
|
+
*
|
|
502
|
+
* 왜: 카드를 여러 단계(공간·트윈·구역·자리·설비)에 붙이려면 방송으로는 태그가 트윈당 1,200개가 되고,
|
|
503
|
+
* **보드에 카드를 하나도 안 놓아도** 30초마다 트윈마다 저널을 접었다. 질의로 바꾸니 보고 있는 카드
|
|
504
|
+
* 수만큼만 들고, 같은 (대상·창·축) 은 클라이언트가 하나로 합친다.
|
|
505
|
+
*
|
|
506
|
+
* 덤으로 질의만 할 수 있는 것이 둘 생겼다 — **과거 시각**(`toTime`)과 **공간 단위 합산**(여러 트윈을
|
|
507
|
+
* 한 번에 접기). 방송 루프는 트윈별이라 둘 다 못 했다.
|
|
508
|
+
*
|
|
509
|
+
* 축을 나눠도 폴드 비용이 같다는 실측이 근거다(`test/kpi-query-bench.test.ts`).
|
|
510
|
+
*/
|
|
439
511
|
/** 방송 병합 주기(ms) — 방송률 상한. 인제스트가 아무리 빨라도 이 주기로만 방송. */
|
|
440
512
|
static { this.BROADCAST_COALESCE_MS = 200; }
|
|
441
513
|
/** live 방송 coalescer — dirty 인 live 인스턴스만 주기적으로 1회 방송(entitySigs 로 변경 엔티티만). */
|
|
@@ -573,7 +645,7 @@ class TwinEngine {
|
|
|
573
645
|
* 저널은 보존된다(추가는 이미 equipment 델타로 저널됨 → replay 는 id-keyed upsert 라 이중계산 없음).
|
|
574
646
|
*/
|
|
575
647
|
static async syncBoardEquipment(domainId, instanceId) {
|
|
576
|
-
const inst = this.instances[instanceId];
|
|
648
|
+
const inst = this.instances[(0, runtime_key_js_1.runtimeKey)(domainId, instanceId)];
|
|
577
649
|
const snap = inst?.kernel?.getSnapshot?.();
|
|
578
650
|
if (!snap?.equipment)
|
|
579
651
|
return;
|
|
@@ -598,7 +670,7 @@ class TwinEngine {
|
|
|
598
670
|
* board 는 replay 의 마스터라 구조가 바뀌면 과거 이벤트의 전제가 깨진다. 좌표(layout)만 바뀌면 저널 보존.
|
|
599
671
|
*/
|
|
600
672
|
static async provision(domainId, instanceId, kind, board, comment) {
|
|
601
|
-
if (this.instances[instanceId])
|
|
673
|
+
if (this.instances[(0, runtime_key_js_1.runtimeKey)(domainId, instanceId)])
|
|
602
674
|
throw new Error(`instance "${instanceId}" is running — stop before re-provisioning`);
|
|
603
675
|
const repo = (0, shell_1.getRepository)(twin_instance_js_1.TwinInstance);
|
|
604
676
|
const existing = await repo.findOne({ where: { domain: { id: domainId }, instanceId } });
|
|
@@ -648,7 +720,7 @@ class TwinEngine {
|
|
|
648
720
|
await repo.save(repo.create({ domain: { id: domainId }, instanceId, rev, signature, board: board, ...(comment ? { comment } : {}) }));
|
|
649
721
|
this.structureRevCache[`${domainId}:${instanceId}`] = rev;
|
|
650
722
|
/* 구조가 바뀌면 재구성 캐시는 옛 공장의 것이다 — 버린다(지우는 건 캐시뿐, 사실은 남는다). */
|
|
651
|
-
delete this.recovered[instanceId];
|
|
723
|
+
delete this.recovered[(0, runtime_key_js_1.runtimeKey)(domainId, instanceId)];
|
|
652
724
|
if (latest)
|
|
653
725
|
console.info(`[twin-engine] "${instanceId}" structure changed → revision ${rev} (history kept; older events stay under revision ${latest.rev}).`);
|
|
654
726
|
return rev;
|
|
@@ -716,8 +788,9 @@ class TwinEngine {
|
|
|
716
788
|
}
|
|
717
789
|
/** 레지스트리 board 로 기동(프로비전된 인스턴스 start). board 인자 없이 저장된 구조로 재기동. */
|
|
718
790
|
static async startFromRegistry(domainId, instanceId, realityMode) {
|
|
719
|
-
|
|
720
|
-
|
|
791
|
+
const key = (0, runtime_key_js_1.runtimeKey)(domainId, instanceId);
|
|
792
|
+
if (this.instances[key])
|
|
793
|
+
return this.instances[key];
|
|
721
794
|
const reg = await (0, shell_1.getRepository)(twin_instance_js_1.TwinInstance).findOne({ where: { domain: { id: domainId }, instanceId } });
|
|
722
795
|
if (!reg?.board)
|
|
723
796
|
throw new Error(`instance "${instanceId}" not provisioned (no board)`);
|
|
@@ -728,15 +801,15 @@ class TwinEngine {
|
|
|
728
801
|
* 어떤 날은 조용히 빈 채로 뜬다 — 재현되지 않는 결함이 가장 나쁘다.
|
|
729
802
|
* 체크포인트 캐시 우선(O(1) + 라이브 파생상태 보존), 없으면 저널 replay 폴백(부팅과 같은 순서).
|
|
730
803
|
*/
|
|
731
|
-
if (!this.recovered[
|
|
804
|
+
if (!this.recovered[key] && reg.purpose !== 'bench') {
|
|
732
805
|
const cached = await this.loadSnapshot(domainId, instanceId).catch(() => null);
|
|
733
806
|
if (cached?.state) {
|
|
734
|
-
this.recovered[
|
|
807
|
+
this.recovered[key] = { revision: cached.revision, state: cached.state };
|
|
735
808
|
}
|
|
736
809
|
else {
|
|
737
810
|
const state = await this.recover(domainId, instanceId).catch(() => null);
|
|
738
811
|
if (state)
|
|
739
|
-
this.recovered[
|
|
812
|
+
this.recovered[key] = { revision: state.revision, state };
|
|
740
813
|
}
|
|
741
814
|
}
|
|
742
815
|
/*
|
|
@@ -797,12 +870,12 @@ class TwinEngine {
|
|
|
797
870
|
*/
|
|
798
871
|
static async resetJournal(domainId, instanceId) {
|
|
799
872
|
await (0, shell_1.getRepository)(twin_event_js_1.TwinEvent).delete({ domain: { id: domainId }, instanceId });
|
|
800
|
-
delete this.recovered[instanceId];
|
|
873
|
+
delete this.recovered[(0, runtime_key_js_1.runtimeKey)(domainId, instanceId)];
|
|
801
874
|
}
|
|
802
875
|
/** 삭제 — 정지 + 레지스트리 삭제 + 저널 purge(domain 스코프). */
|
|
803
876
|
static async remove(domainId, instanceId) {
|
|
804
|
-
await this.stop(instanceId);
|
|
805
|
-
delete this.recovered[instanceId];
|
|
877
|
+
await this.stop(domainId, instanceId);
|
|
878
|
+
delete this.recovered[(0, runtime_key_js_1.runtimeKey)(domainId, instanceId)];
|
|
806
879
|
await (0, shell_1.getRepository)(twin_event_js_1.TwinEvent).delete({ domain: { id: domainId }, instanceId });
|
|
807
880
|
await (0, shell_1.getRepository)(twin_instance_js_1.TwinInstance).delete({ domain: { id: domainId }, instanceId });
|
|
808
881
|
}
|
|
@@ -824,10 +897,17 @@ class TwinEngine {
|
|
|
824
897
|
realityMode: r.realityMode ?? exports.DEFAULT_REALITY_MODE, // 현실 출처 선언(§0 ①) — mirror/sim-world/sim-experiment
|
|
825
898
|
purpose: r.purpose ?? 'operational', // 운영 vs 벤치 사본(1급 구별 — 이름 접두사 아님)
|
|
826
899
|
copyOf: r.copyOf ?? undefined,
|
|
827
|
-
running: !!this.instances[r.instanceId],
|
|
900
|
+
running: !!this.instances[(0, runtime_key_js_1.runtimeKey)(domainId, r.instanceId)],
|
|
828
901
|
revision: last?.revision ?? 0,
|
|
829
|
-
|
|
830
|
-
|
|
902
|
+
/*
|
|
903
|
+
* **리더를 거쳐 센다** — 보드 키를 직접 읽으면 옛 세대 보드가 0 으로 보인다.
|
|
904
|
+
*
|
|
905
|
+
* 커널은 자리·설비 배열의 **옛 세대 키까지 흡수해** 읽어 주는데, 이 목록은 새 이름만 직접
|
|
906
|
+
* 세고 있었다. 그래서 옛 보드 12개가 화면에 **자리 0 · 설비 0**
|
|
907
|
+
* 으로 떴다 — 오류 없이, 그냥 빈 공장처럼. 세는 규칙이 두 벌이면 이런 식으로 갈라진다.
|
|
908
|
+
*/
|
|
909
|
+
locationCount: readBoardLocations(board).length,
|
|
910
|
+
equipmentCount: readBoardEquipment(board).length
|
|
831
911
|
});
|
|
832
912
|
}
|
|
833
913
|
return out;
|
|
@@ -847,8 +927,9 @@ class TwinEngine {
|
|
|
847
927
|
const board = r.board ?? {};
|
|
848
928
|
const a = agg.get(r.spaceId) ?? { spaceId: r.spaceId, name: nameOf.get(r.spaceId) ?? r.spaceId, instances: 0, locations: 0, equipment: 0 };
|
|
849
929
|
a.instances++;
|
|
850
|
-
|
|
851
|
-
a.
|
|
930
|
+
/* 목록과 **같은 규칙**으로 센다 — 여기만 직접 세면 공간 요약과 인스턴스 목록이 다른 수를 말한다. */
|
|
931
|
+
a.locations += readBoardLocations(board).length;
|
|
932
|
+
a.equipment += readBoardEquipment(board).length;
|
|
852
933
|
agg.set(r.spaceId, a);
|
|
853
934
|
}
|
|
854
935
|
return [...agg.values()];
|
|
@@ -887,12 +968,32 @@ class TwinEngine {
|
|
|
887
968
|
* Face2 마스터 인제스트(ADR-0018) — 레퍼런스 시스템(실 또는 가상)의 마스터를 읽어 트윈을 생성.
|
|
888
969
|
* 엔티티를 손배선/발명하지 않고 마스터에서 반영: 공간(Space) upsert + 인스턴스 provision(미기동).
|
|
889
970
|
* 노드타입은 커널 카탈로그로 검증(warning). start 는 호출측(bootstrap/mutation)이 결정.
|
|
971
|
+
*
|
|
972
|
+
* `into.spaceId` — **이 현장에 더한다.** 한 현실을 여러 렌즈(WMS·MES·YMS)가 비추므로 새 트윈이
|
|
973
|
+
* 기존 공간으로 들어갈 수 있다. 아래 병합은 원래 그 경우를 위해 있었는데(N:1) 만드는 흐름에서
|
|
974
|
+
* 공간을 고를 방법이 없었다 — 마스터가 파생한 id 만 쓰였다. 판정은 `resolveIngestSpace`(순수).
|
|
890
975
|
*/
|
|
891
|
-
static async ingestMaster(domainId, master) {
|
|
976
|
+
static async ingestMaster(domainId, master, into) {
|
|
892
977
|
const { board, spaceContent, warnings } = (0, reference_master_js_2.masterToTwin)(master, DOMAIN_CATALOG);
|
|
893
|
-
const spaceId = board.spaceId;
|
|
894
978
|
const repo = (0, shell_1.getRepository)(twin_space_js_1.TwinSpace);
|
|
979
|
+
/* 합칠 공간을 골랐으면 **있는지 먼저 확인한다** — 없는 곳에 조용히 새 공간을 만들면 사용자는
|
|
980
|
+
합쳤다고 믿고 화면은 따로 논다. 판정 함수가 그 경우 거절한다. */
|
|
981
|
+
const wanted = (into?.spaceId ?? '').trim();
|
|
982
|
+
const wantedExists = wanted
|
|
983
|
+
? (await repo.count({ where: { domain: { id: domainId }, spaceId: wanted } })) > 0
|
|
984
|
+
: false;
|
|
985
|
+
const choice = (0, ingest_space_js_1.resolveIngestSpace)(board.spaceId, wanted, wantedExists);
|
|
986
|
+
const spaceId = choice.spaceId;
|
|
987
|
+
board.spaceId = spaceId; // 보드(커널 소비)도 같은 공간을 가리켜야 한다
|
|
895
988
|
const existing = await repo.findOne({ where: { domain: { id: domainId }, spaceId } });
|
|
989
|
+
if (choice.joined) {
|
|
990
|
+
/* 같은 id 의 구역·랜드마크는 합집합으로 접힌다 — 가릴 수 없는 것을 고르지 않고 무엇이
|
|
991
|
+
합쳐졌는지 말한다(정당한 경우가 많으므로 막지 않는다). */
|
|
992
|
+
const prevContent = existing?.content ?? {};
|
|
993
|
+
const lm = (0, ingest_space_js_1.collidingIds)(prevContent.landmarks, spaceContent.landmarks);
|
|
994
|
+
if (lm.length)
|
|
995
|
+
warnings.push((0, reference_master_js_3.landmarkIdsMerged)(spaceId, lm));
|
|
996
|
+
}
|
|
896
997
|
/*
|
|
897
998
|
* 공유 공간(여러 트윈이 한 spaceId, N:1) content 병합 — 마지막 인제스트가 통째로 덮어써 area(그룹)·표현이
|
|
898
999
|
* 유실되던 문제 보정. area·landmark 는 id 합집합, representations 는 비어있지 않은 쪽 보존, 나머지는 first-wins.
|
|
@@ -971,7 +1072,7 @@ class TwinEngine {
|
|
|
971
1072
|
}
|
|
972
1073
|
await this.provision(domainId, master.source, master.system, board);
|
|
973
1074
|
if (warnings.length)
|
|
974
|
-
console.warn(`[twin-engine] ingest "${master.source}" warnings
|
|
1075
|
+
console.warn(`[twin-engine] ingest "${master.source}" warnings: ${(0, reference_master_js_3.describeWarnings)(warnings)}`);
|
|
975
1076
|
return { instanceId: master.source, spaceId, warnings };
|
|
976
1077
|
}
|
|
977
1078
|
/** 단건 상세 — 프로비저닝 에디터가 편집할 board(구조+layout) 포함. */
|
|
@@ -983,7 +1084,7 @@ class TwinEngine {
|
|
|
983
1084
|
instanceId: r.instanceId,
|
|
984
1085
|
kind: r.kind,
|
|
985
1086
|
status: r.status,
|
|
986
|
-
running: !!this.instances[r.instanceId],
|
|
1087
|
+
running: !!this.instances[(0, runtime_key_js_1.runtimeKey)(domainId, r.instanceId)],
|
|
987
1088
|
realityMode: r.realityMode, // 현실 선언 — what-if 적용 가드용(mirror=예측 전용, 자극 주입 불가)
|
|
988
1089
|
board: r.board
|
|
989
1090
|
};
|
|
@@ -1014,7 +1115,7 @@ class TwinEngine {
|
|
|
1014
1115
|
const seen = new Set();
|
|
1015
1116
|
/* payload 매핑은 순수 함수(buildEntityDeltas)로 분리 — 여기선 시그니처 dedup + 발행만.
|
|
1016
1117
|
* 변화한 엔티티만 발행(최신-상태 채널이라 무변화 재방송 무의미). */
|
|
1017
|
-
for (const { tag, data } of (0, entity_delta_js_1.buildEntityDeltas)(st)) {
|
|
1118
|
+
for (const { tag, data } of (0, entity_delta_js_1.buildEntityDeltas)(st, inst.id)) {
|
|
1018
1119
|
seen.add(tag);
|
|
1019
1120
|
const sig = JSON.stringify(data);
|
|
1020
1121
|
if (sigs.get(tag) === sig)
|
|
@@ -1044,13 +1145,13 @@ class TwinEngine {
|
|
|
1044
1145
|
// 라이브 최신(시간여행 아님) + 인메모리 인스턴스 → 라이브 커널 스냅샷을 직접 사용.
|
|
1045
1146
|
// replay(StateProjector)는 attentions·ack 등 라이브 전용 파생 상태를 담지 못하므로, 최신은 커널 진실을 쓴다.
|
|
1046
1147
|
if (untilRevision == null && untilTime == null) {
|
|
1047
|
-
const live = this.instances[instanceId];
|
|
1148
|
+
const live = this.instances[(0, runtime_key_js_1.runtimeKey)(domainId, instanceId)];
|
|
1048
1149
|
if (live?.kernel?.getSnapshot)
|
|
1049
1150
|
return live.kernel.getSnapshot();
|
|
1050
1151
|
// live 모드는 kernel 이 없고 projector 미러 → 인메모리 최신 스냅샷(projector+OEE+attentions) 직접 사용.
|
|
1051
1152
|
// (저널이 있어도 최신은 인메모리가 진실 — replay 는 시간여행/복구 전용.)
|
|
1052
1153
|
if (live?.mode === 'live' && live.projector)
|
|
1053
|
-
return this.snapshot(instanceId);
|
|
1154
|
+
return this.snapshot(domainId, instanceId);
|
|
1054
1155
|
}
|
|
1055
1156
|
const reg = await (0, shell_1.getRepository)(twin_instance_js_1.TwinInstance).findOne({ where: { domain: { id: domainId }, instanceId } });
|
|
1056
1157
|
if (!reg?.board)
|
|
@@ -1111,16 +1212,61 @@ class TwinEngine {
|
|
|
1111
1212
|
const newest = structures[structures.length - 1]?.board;
|
|
1112
1213
|
if (asOfNow && newest && segments[segments.length - 1]?.board !== newest)
|
|
1113
1214
|
segments.push({ board: newest, events: [] });
|
|
1215
|
+
/*
|
|
1216
|
+
* 접은 상태에 **주의 신호와 시각을 채운다.**
|
|
1217
|
+
*
|
|
1218
|
+
* 라이브·시뮬은 커널이 신호를 스스로 내지만, 저널을 접는 이 경로는 프로젝터 상태만 낸다 — 신호도
|
|
1219
|
+
* `nowTime` 도 없다. 그래서 **과거를 되짚으면 주의 레일이 텅 비었고**(지도는 `snap.attentions` 를
|
|
1220
|
+
* 읽는다), 기동돼 있지 않은 트윈을 보는 화면도 같았다. 신호는 상태에서 계산되는 것이므로
|
|
1221
|
+
* 여기서 같은 공식(`deriveAttentions`)으로 채우면 된다 — 두 벌을 두지 않는다.
|
|
1222
|
+
*
|
|
1223
|
+
* `nowTime` 이 먼저다: "늦었나" 판정이 그 값을 본다. 되짚은 시점의 정직한 "지금" 은 물어본 시각
|
|
1224
|
+
* (`untilTime`)이고, 없으면 마지막으로 적용한 이벤트의 시각이다. 둘 다 없으면 채우지 않는다 —
|
|
1225
|
+
* 벽시계를 끼워 넣으면 과거 화면이 "지금 기준으로 늦었다" 고 말하게 된다.
|
|
1226
|
+
*/
|
|
1227
|
+
const lastEventTime = rows.length ? String(rows[rows.length - 1]?.eventTime ?? '') : '';
|
|
1228
|
+
/*
|
|
1229
|
+
* **가동 이력도 되살린다** — 되짚은 화면에 설비 계측이 비어 있던 것.
|
|
1230
|
+
*
|
|
1231
|
+
* OEE 는 원 시스템이 누적을 보내 주지 않아 호스트가 상태 전이를 적분해 만든다(그래서 커널이 아니라
|
|
1232
|
+
* 여기 있다). 그런데 그 누적기는 **라이브에서만** 돌았고, 저널을 접는 경로는 그 계산을 하지 않았다 —
|
|
1233
|
+
* 과거를 되짚으면 모든 설비가 "가동 이력이 전혀 없음" 으로 보였다.
|
|
1234
|
+
*
|
|
1235
|
+
* 없는 것은 데이터가 아니라 계산이다: 입력(`equipment.status` 전이·`quality.output`)은 저널에 다
|
|
1236
|
+
* 있다. 그래서 **같은 누적기에 같은 이벤트를 태운다** — 규칙을 두 벌 만들지 않는다.
|
|
1237
|
+
*
|
|
1238
|
+
* 시간여행에서도 맞다: 여기 태우는 것은 이미 잘라 낸(`wanted`) 이벤트뿐이므로, 그 시점까지의
|
|
1239
|
+
* 가동 이력이 나온다(그 뒤에 일어난 고장이 과거 화면에 섞이지 않는다).
|
|
1240
|
+
*/
|
|
1241
|
+
const oee = new oee_accumulator_js_1.OeeAccumulator();
|
|
1242
|
+
for (const r of wanted) {
|
|
1243
|
+
try {
|
|
1244
|
+
oee.apply(r.payload);
|
|
1245
|
+
}
|
|
1246
|
+
catch {
|
|
1247
|
+
/* 한 건이 이상해도 나머지 계측을 버리지 않는다 — 누적기는 모르는 이벤트를 무시하는 계약이다. */
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
const withNow = (st) => {
|
|
1251
|
+
if (!st || typeof st !== 'object')
|
|
1252
|
+
return st;
|
|
1253
|
+
const nowTime = st.nowTime || untilTime || lastEventTime || undefined;
|
|
1254
|
+
const based = nowTime ? { ...st, nowTime } : st;
|
|
1255
|
+
/* 계측 시점은 그 화면의 "지금" 이다 — 벽시계로 재면 과거 화면의 가용률이 시간이 갈수록 떨어진다. */
|
|
1256
|
+
const nowMs = nowTime ? Date.parse(String(nowTime)) : Number.NaN;
|
|
1257
|
+
const withMetrics = (0, oee_accumulator_js_1.withLiveOee)(based, oee, Number.isFinite(nowMs) ? nowMs : undefined);
|
|
1258
|
+
return (0, live_attentions_js_1.withLiveAttentions)(withMetrics);
|
|
1259
|
+
};
|
|
1114
1260
|
if (!segments.length)
|
|
1115
|
-
return replay(((asOfNow && newest) || reg.board), []);
|
|
1261
|
+
return withNow(replay(((asOfNow && newest) || reg.board), []));
|
|
1116
1262
|
if (segments.length === 1)
|
|
1117
|
-
return replay(segments[0].board, segments[0].events);
|
|
1263
|
+
return withNow(replay(segments[0].board, segments[0].events));
|
|
1118
1264
|
const { state, shifts } = replaySegments(segments);
|
|
1119
1265
|
/* 경계에서 사라진 것을 조용히 넘기지 않는다 — 수가 줄어든 이유를 어딘가에는 남겨야 한다. */
|
|
1120
1266
|
for (const sh of shifts)
|
|
1121
1267
|
if (sh.equipmentDropped || sh.locationsDropped || sh.personsDropped || sh.assetsDropped)
|
|
1122
1268
|
console.info(`[twin-engine] "${instanceId}" replay crossed a structure change — dropped ${sh.equipmentDropped} equipment, ${sh.locationsDropped} locations, ${sh.personsDropped} persons, ${sh.assetsDropped} assets that no longer exist.`);
|
|
1123
|
-
return state;
|
|
1269
|
+
return withNow(state);
|
|
1124
1270
|
}
|
|
1125
1271
|
/**
|
|
1126
1272
|
* 공간(공동배치) 시각 범위 — 스크러버 앵커(runtime-state-model §4·§6). 그 공간 전 인스턴스 저널의 min/max eventTime.
|
|
@@ -1154,12 +1300,12 @@ class TwinEngine {
|
|
|
1154
1300
|
static onStop(fn) {
|
|
1155
1301
|
this.stopHooks.push(fn);
|
|
1156
1302
|
}
|
|
1157
|
-
static async stop(id) {
|
|
1158
|
-
const i = this.instances[id];
|
|
1303
|
+
static async stop(domainId, id) {
|
|
1304
|
+
const i = this.instances[(0, runtime_key_js_1.runtimeKey)(domainId, id)];
|
|
1159
1305
|
if (i) {
|
|
1160
1306
|
clearInterval(i.timer);
|
|
1161
1307
|
i.unsub();
|
|
1162
|
-
delete this.instances[id];
|
|
1308
|
+
delete this.instances[(0, runtime_key_js_1.runtimeKey)(domainId, id)];
|
|
1163
1309
|
await (0, shell_1.getRepository)(twin_instance_js_1.TwinInstance)
|
|
1164
1310
|
.update({ domain: { id: i.domainId }, instanceId: id }, { status: 'stopped' })
|
|
1165
1311
|
.catch(err => console.error('twin deregister fail', err));
|
|
@@ -1171,8 +1317,70 @@ class TwinEngine {
|
|
|
1171
1317
|
catch { /* 훅 격리 */ }
|
|
1172
1318
|
}
|
|
1173
1319
|
}
|
|
1174
|
-
|
|
1175
|
-
|
|
1320
|
+
/*
|
|
1321
|
+
* tick 한 번 — **한 트윈의 예외가 서버를 내리지 못하게** 감싼다.
|
|
1322
|
+
*
|
|
1323
|
+
* 잘못된 시나리오가 실렸을 때 다음 tick 에서 `rate.meanPerHour` 를 읽다 터졌고, 타이머 콜백의
|
|
1324
|
+
* 예외는 아무도 받지 않아 **uncaught exception 으로 프로세스가 내려갔다.** 한 테넌트의 잘못된
|
|
1325
|
+
* 선언이 모든 테넌트를 멈춘 셈이다. 문 앞에서 막는 것이 1차 방벽이고(`validateScenario`),
|
|
1326
|
+
* 이것이 2차 방벽이다.
|
|
1327
|
+
*
|
|
1328
|
+
* **조용히 삼키지 않는다.** 굴리기를 멈추고 그 사실을 남긴다 — 예외를 무시하고 계속 tick 하면
|
|
1329
|
+
* 같은 오류가 매 주기 쏟아지고, 그 트윈은 "도는 것처럼 보이면서" 아무것도 진행하지 않는다.
|
|
1330
|
+
*/
|
|
1331
|
+
static tickGuarded(domainId, id, runtime) {
|
|
1332
|
+
try {
|
|
1333
|
+
runtime.tick(this.TICK_MS);
|
|
1334
|
+
}
|
|
1335
|
+
catch (err) {
|
|
1336
|
+
const inst = this.instances[(0, runtime_key_js_1.runtimeKey)(domainId, id)];
|
|
1337
|
+
if (inst?.timer) {
|
|
1338
|
+
clearInterval(inst.timer);
|
|
1339
|
+
inst.timer = undefined;
|
|
1340
|
+
}
|
|
1341
|
+
console.error(`[twin-engine] "${id}" tick failed — simulation stopped for this twin (other twins keep running). ` +
|
|
1342
|
+
`Fix the declaration and start it again. Reason: ${err?.message ?? err}`);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
static runtime(domainId, id) {
|
|
1346
|
+
return this.instances[(0, runtime_key_js_1.runtimeKey)(domainId, id)]?.runtime;
|
|
1347
|
+
}
|
|
1348
|
+
/*
|
|
1349
|
+
* 커맨드 실행 — **판정은 `routeCommand`(순수)가 하고 여기서는 실행만** 한다.
|
|
1350
|
+
* 판정을 코드 한가운데 두면 이 부류를 테스트로 못 잡는다(엔진은 DB 를 물고 있어 단위 테스트가
|
|
1351
|
+
* 불러올 수 없다). 예전에는 리졸버가 `inst.runtime.dispatch` 를 곧바로 불러 미러에서 터졌다.
|
|
1352
|
+
*/
|
|
1353
|
+
static async dispatchCommand(domainId, instanceId, command) {
|
|
1354
|
+
const inst = this.instances[(0, runtime_key_js_1.runtimeKey)(domainId, instanceId)];
|
|
1355
|
+
const type = String(command?.type ?? '');
|
|
1356
|
+
const route = (0, command_routing_js_1.routeCommand)(inst && { mode: inst.mode, hasRuntime: !!inst.runtime, hasKernelDispatch: typeof inst.kernel?.dispatch === 'function' }, !!inst && inst.domainId === domainId, type);
|
|
1357
|
+
if (route.target === 'reject') {
|
|
1358
|
+
return { accepted: false, errorCode: route.errorCode, errorParams: route.errorParams, error: route.errorCode };
|
|
1359
|
+
}
|
|
1360
|
+
const cmd = { ...command, tenantId: domainId }; // tenantId 는 호출자 도메인으로 각인(감사·무결성)
|
|
1361
|
+
if (route.target === 'runtime')
|
|
1362
|
+
return inst.runtime.dispatch(cmd);
|
|
1363
|
+
/*
|
|
1364
|
+
* 미러 — 관측 커널로 보내고, **커맨드가 낸 사실을 저널 큐에 실어** 코얼레서가 번호를 부여하게 한다.
|
|
1365
|
+
* 미러에는 State 구독 배관이 없어(시뮬은 그 경로로 저널링) 커널 방출이 아무 데도 닿지 않는다.
|
|
1366
|
+
* 여기서 DB 를 따로 읽어 번호를 매기면 인메모리 카운터와 어긋나 리비전이 겹친다(겹침은 오류를
|
|
1367
|
+
* 내지 않고 재생 순서만 조용히 뒤섞는다).
|
|
1368
|
+
*/
|
|
1369
|
+
const kernel = inst.kernel;
|
|
1370
|
+
const emitted = [];
|
|
1371
|
+
const off = kernel.onEvent?.((e) => emitted.push(e));
|
|
1372
|
+
let ack;
|
|
1373
|
+
try {
|
|
1374
|
+
ack = kernel.dispatch(cmd);
|
|
1375
|
+
}
|
|
1376
|
+
finally {
|
|
1377
|
+
off?.();
|
|
1378
|
+
}
|
|
1379
|
+
if (ack?.accepted && emitted.length) {
|
|
1380
|
+
inst.pendingJournal = [...(inst.pendingJournal ?? []), ...emitted];
|
|
1381
|
+
inst.dirty = true; // 코얼레서가 이번 주기에 비우고 방송까지 하게 한다
|
|
1382
|
+
}
|
|
1383
|
+
return ack ?? { accepted: false, errorCode: 'unknown-command', error: 'unknown-command' };
|
|
1176
1384
|
}
|
|
1177
1385
|
/**
|
|
1178
1386
|
* 도메인 소유 게이트 — 인스턴스가 이 도메인 소유일 때만 true(테넌트 격리).
|
|
@@ -1180,8 +1388,17 @@ class TwinEngine {
|
|
|
1180
1388
|
* this.instances[id] 를 만지기 전에 반드시 이걸로 확인해야 한다. 인스턴스 id 는 추측 가능하므로
|
|
1181
1389
|
* 검증 없이 접근하면 크로스테넌트 읽기/정지가 가능해진다.
|
|
1182
1390
|
*/
|
|
1391
|
+
/**
|
|
1392
|
+
* 이 런타임의 **관측 모드** — `'live'`(외부 실물을 미러) 또는 `'sim'`(커널이 굴린다). 떠 있지 않으면 없다.
|
|
1393
|
+
*
|
|
1394
|
+
* 소비처가 `instances[id].mode` 를 직접 읽던 자리를 대신한다. 레지스트리를 밖에 열면 도메인 확인이
|
|
1395
|
+
* 자리마다 제각각이 되고, 실제로 그렇게 됐다(테넌트 격리 전수 확인, 2026-08-06).
|
|
1396
|
+
*/
|
|
1397
|
+
static modeOf(domainId, id) {
|
|
1398
|
+
return this.instances[(0, runtime_key_js_1.runtimeKey)(domainId, id)]?.mode;
|
|
1399
|
+
}
|
|
1183
1400
|
static owns(domainId, id) {
|
|
1184
|
-
return this.instances[id]
|
|
1401
|
+
return !!this.instances[(0, runtime_key_js_1.runtimeKey)(domainId, id)];
|
|
1185
1402
|
}
|
|
1186
1403
|
/**
|
|
1187
1404
|
* 이 트윈이 이 테넌트 것인가 — **떠 있든 아니든.**
|
|
@@ -1198,13 +1415,50 @@ class TwinEngine {
|
|
|
1198
1415
|
return true;
|
|
1199
1416
|
return !!(await (0, shell_1.getRepository)(twin_instance_js_1.TwinInstance).findOne({ where: { domain: { id: domainId }, instanceId } }));
|
|
1200
1417
|
}
|
|
1418
|
+
/**
|
|
1419
|
+
* **이 공간의 인스턴스들** — 한 현실을 여러 렌즈가 비추므로, 공간을 물으면 그 렌즈 전부를 답한다.
|
|
1420
|
+
*
|
|
1421
|
+
* 화면이 이 규칙을 손으로 짜지 않게 서버가 답한다. 예측 화면은 같은 확장을 **한 파일에서 두 번**
|
|
1422
|
+
* 복제하고 있었다(전체 목록을 받아 클라이언트에서 걸렀다) — 규칙이 흩어지면 한쪽만 고쳐진다.
|
|
1423
|
+
*
|
|
1424
|
+
* 떠 있지 않은 것도 포함한다: 공간에 무엇이 있는지는 기동 여부와 다른 사실이다.
|
|
1425
|
+
*/
|
|
1426
|
+
static async instanceIdsOfSpace(domainId, spaceId) {
|
|
1427
|
+
if (!spaceId)
|
|
1428
|
+
return [];
|
|
1429
|
+
const rows = await (0, shell_1.getRepository)(twin_instance_js_1.TwinInstance).find({ where: { domain: { id: domainId }, spaceId } });
|
|
1430
|
+
return rows.map(r => r.instanceId).filter((x) => !!x);
|
|
1431
|
+
}
|
|
1432
|
+
/**
|
|
1433
|
+
* **이 공간에서 봐야 할 것** — 공간의 모든 렌즈에서 신호를 모은다.
|
|
1434
|
+
*
|
|
1435
|
+
* 합집합이 그대로 뜻이 있는 유일한 렌즈다(예측은 합칠 수 없다 — P50 두 개를 더할 수 없다).
|
|
1436
|
+
* 정렬·잘라내기는 화면과 **같은 규칙**을 쓴다(`digestAttentions`) — 서버가 고른 상위 N 이 화면이
|
|
1437
|
+
* 고를 N 과 달라지면 그 어긋남은 아무 데서도 오류로 드러나지 않는다.
|
|
1438
|
+
*
|
|
1439
|
+
* 신호마다 어느 트윈에서 왔는지(`instanceId`)를 붙인다. 공간에서 보면 같은 자리 id 가 렌즈마다
|
|
1440
|
+
* 다른 것을 가리킬 수 있고, 조치는 결국 그 트윈에 보내야 한다.
|
|
1441
|
+
*/
|
|
1442
|
+
static async attentionsOfSpace(domainId, spaceId, limit) {
|
|
1443
|
+
const ids = await this.instanceIdsOfSpace(domainId, spaceId);
|
|
1444
|
+
const lenses = ids
|
|
1445
|
+
/* 떠 있지 않은 트윈은 **지금** 신호가 없다(그 시절 신호는 시간여행이 답한다). */
|
|
1446
|
+
.filter(id => this.owns(domainId, id))
|
|
1447
|
+
.map(id => {
|
|
1448
|
+
const kernel = this.kernel(domainId, id);
|
|
1449
|
+
const snap = kernel?.getSnapshot?.() ?? this.snapshot(domainId, id);
|
|
1450
|
+
return { instanceId: id, attentions: snap?.attentions ?? [] };
|
|
1451
|
+
});
|
|
1452
|
+
/* 모으는 규칙(태깅·급한 순서·자리 색)은 순수 함수가 들고 있다 — 여기서 손으로 접지 않는다. */
|
|
1453
|
+
return (0, attention_digest_js_1.mergeLensAttentions)(lenses, limit);
|
|
1454
|
+
}
|
|
1201
1455
|
/** 라이브 커널(ForecastTwin) — forecast/divergence 예측 연산용. */
|
|
1202
|
-
static kernel(id) {
|
|
1203
|
-
return this.instances[id]?.kernel;
|
|
1456
|
+
static kernel(domainId, id) {
|
|
1457
|
+
return this.instances[(0, runtime_key_js_1.runtimeKey)(domainId, id)]?.kernel;
|
|
1204
1458
|
}
|
|
1205
1459
|
/** 라이브 처리량 계측 스냅샷(모니터, ④-1) — 내부 누적(_acc*) 제외한 공개 지표. live 아니면 null. */
|
|
1206
|
-
static metrics(id) {
|
|
1207
|
-
const m = this.instances[id]?.metrics;
|
|
1460
|
+
static metrics(domainId, id) {
|
|
1461
|
+
const m = this.instances[(0, runtime_key_js_1.runtimeKey)(domainId, id)]?.metrics;
|
|
1208
1462
|
if (!m)
|
|
1209
1463
|
return null;
|
|
1210
1464
|
return {
|
|
@@ -1216,9 +1470,13 @@ class TwinEngine {
|
|
|
1216
1470
|
}
|
|
1217
1471
|
/** 전체 라이브 인스턴스 계측(모니터 대시보드용). */
|
|
1218
1472
|
static async allMetrics(domainId) {
|
|
1473
|
+
/* 도메인 없이 부르면 전 테넌트를 훑는다(내부 모니터용) — 키에서 도메인을 되돌려 각자에게 묻는다. */
|
|
1219
1474
|
const rows = Object.keys(this.instances)
|
|
1220
|
-
.filter(
|
|
1221
|
-
.map(
|
|
1475
|
+
.filter(key => !domainId || (0, runtime_key_js_1.isOfDomain)(key, domainId))
|
|
1476
|
+
.map(key => {
|
|
1477
|
+
const at = (0, runtime_key_js_1.parseRuntimeKey)(key);
|
|
1478
|
+
return this.metrics(at.domainId, at.instanceId);
|
|
1479
|
+
})
|
|
1222
1480
|
.filter(Boolean);
|
|
1223
1481
|
if (!rows.length || !domainId)
|
|
1224
1482
|
return rows;
|
|
@@ -1252,7 +1510,7 @@ class TwinEngine {
|
|
|
1252
1510
|
if (!reg?.board)
|
|
1253
1511
|
return null;
|
|
1254
1512
|
const Kernel = KERNELS[reg.kind ?? 'wms'] ?? WmsKernel;
|
|
1255
|
-
const k = new Kernel(domainId, undefined, this.
|
|
1513
|
+
const k = new Kernel(domainId, undefined, this.productionSpecOf(reg.board));
|
|
1256
1514
|
k.loadBoard(reg.board);
|
|
1257
1515
|
this.applyOperations(k, reg.board, instanceId); // 예측도 같은 명세로 굴러야 한다(화면과 다른 숫자 금지)
|
|
1258
1516
|
await this.installEstimators(k, domainId, instanceId, reg.board); // 예측은 기다린다 — 실측을 놓치면 예측이 상수로 돈다
|
|
@@ -1273,8 +1531,8 @@ class TwinEngine {
|
|
|
1273
1531
|
return k;
|
|
1274
1532
|
}
|
|
1275
1533
|
/** 현재 전체 스냅샷 — 라이브 우선, 없으면 저널 복구 캐시. */
|
|
1276
|
-
static snapshot(id) {
|
|
1277
|
-
const inst = this.instances[id];
|
|
1534
|
+
static snapshot(domainId, id) {
|
|
1535
|
+
const inst = this.instances[(0, runtime_key_js_1.runtimeKey)(domainId, id)];
|
|
1278
1536
|
if (inst?.mode === 'live' && inst.projector) {
|
|
1279
1537
|
/* live: **커널이 주목 신호를 스스로 낸다**(관측 모드) — 호스트가 덧붙이던 withLiveAttentions 는
|
|
1280
1538
|
* 필요 없다. OEE 만 호스트가 채운다: 원 시스템이 시간 누적을 보내 주지 않아 상태 전이를 적분해
|
|
@@ -1282,7 +1540,7 @@ class TwinEngine {
|
|
|
1282
1540
|
const st = inst.projector.snapshot();
|
|
1283
1541
|
return inst.oee ? (0, oee_accumulator_js_1.withLiveOee)(st, inst.oee) : st;
|
|
1284
1542
|
}
|
|
1285
|
-
return inst?.runtime?.resync() ?? this.recovered[id];
|
|
1543
|
+
return inst?.runtime?.resync() ?? this.recovered[(0, runtime_key_js_1.runtimeKey)(domainId, id)];
|
|
1286
1544
|
}
|
|
1287
1545
|
}
|
|
1288
1546
|
exports.TwinEngine = TwinEngine;
|