@things-factory/headless-twin 10.0.12 โ 10.0.14
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/canonical-ingest.d.ts +57 -2
- package/dist-server/engine/canonical-ingest.js +43 -2
- package/dist-server/engine/canonical-ingest.js.map +1 -1
- package/dist-server/engine/command-routing.js +1 -1
- package/dist-server/engine/command-routing.js.map +1 -1
- package/dist-server/engine/index.d.ts +3 -0
- package/dist-server/engine/index.js +4 -0
- package/dist-server/engine/index.js.map +1 -1
- package/dist-server/engine/ingest-health.d.ts +376 -0
- package/dist-server/engine/ingest-health.js +482 -0
- package/dist-server/engine/ingest-health.js.map +1 -0
- package/dist-server/engine/integration-coverage.d.ts +76 -0
- package/dist-server/engine/integration-coverage.js +73 -0
- package/dist-server/engine/integration-coverage.js.map +1 -0
- package/dist-server/engine/integration-probes.d.ts +65 -0
- package/dist-server/engine/integration-probes.js +100 -0
- package/dist-server/engine/integration-probes.js.map +1 -0
- package/dist-server/engine/integration-runner.d.ts +45 -0
- package/dist-server/engine/integration-runner.js +59 -0
- package/dist-server/engine/integration-runner.js.map +1 -0
- package/dist-server/engine/integration-target-profile.d.ts +57 -0
- package/dist-server/engine/integration-target-profile.js +79 -0
- package/dist-server/engine/integration-target-profile.js.map +1 -0
- package/dist-server/engine/kpi-fold.d.ts +27 -0
- package/dist-server/engine/kpi-fold.js +47 -4
- package/dist-server/engine/kpi-fold.js.map +1 -1
- package/dist-server/engine/kpi-query.js +128 -14
- package/dist-server/engine/kpi-query.js.map +1 -1
- package/dist-server/engine/live-feed-registry.js +2 -16
- package/dist-server/engine/live-feed-registry.js.map +1 -1
- package/dist-server/engine/load-meter.d.ts +1 -1
- package/dist-server/engine/load-meter.js +12 -3
- package/dist-server/engine/load-meter.js.map +1 -1
- package/dist-server/engine/log.d.ts +18 -0
- package/dist-server/engine/log.js +80 -0
- package/dist-server/engine/log.js.map +1 -0
- package/dist-server/engine/loop-lag.d.ts +54 -0
- package/dist-server/engine/loop-lag.js +87 -0
- package/dist-server/engine/loop-lag.js.map +1 -0
- package/dist-server/engine/model-gap.d.ts +108 -0
- package/dist-server/engine/model-gap.js +95 -0
- package/dist-server/engine/model-gap.js.map +1 -0
- package/dist-server/engine/restart-policy.d.ts +3 -3
- package/dist-server/engine/restart-policy.js +4 -4
- package/dist-server/engine/restart-policy.js.map +1 -1
- package/dist-server/engine/runtime-key.js +1 -1
- package/dist-server/engine/runtime-key.js.map +1 -1
- package/dist-server/engine/stage-path.d.ts +83 -0
- package/dist-server/engine/stage-path.js +118 -0
- package/dist-server/engine/stage-path.js.map +1 -0
- package/dist-server/engine/twin-engine.d.ts +386 -16
- package/dist-server/engine/twin-engine.js +1217 -172
- package/dist-server/engine/twin-engine.js.map +1 -1
- package/dist-server/index.js +34 -2
- package/dist-server/index.js.map +1 -1
- package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.d.ts +5 -0
- package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.js +58 -0
- package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.js.map +1 -0
- package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.d.ts +5 -0
- package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.js +55 -0
- package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.js.map +1 -0
- package/dist-server/migrations/index.js +7 -1
- package/dist-server/migrations/index.js.map +1 -1
- package/dist-server/service/index.d.ts +5 -2
- package/dist-server/service/index.js +18 -7
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/reference/discovery-result.d.ts +1 -1
- package/dist-server/service/reference/discovery-result.js +1 -1
- package/dist-server/service/reference/discovery-result.js.map +1 -1
- package/dist-server/service/reference/index.d.ts +4 -1
- package/dist-server/service/reference/index.js +6 -1
- package/dist-server/service/reference/index.js.map +1 -1
- package/dist-server/service/reference/reference-adapter.d.ts +210 -3
- package/dist-server/service/reference/reference-adapter.js +28 -4
- package/dist-server/service/reference/reference-adapter.js.map +1 -1
- package/dist-server/service/reference/reference-assessment.d.ts +78 -0
- package/dist-server/service/reference/reference-assessment.js +156 -0
- package/dist-server/service/reference/reference-assessment.js.map +1 -0
- package/dist-server/service/reference/reference-live.d.ts +12 -1
- package/dist-server/service/reference/reference-live.js +134 -11
- package/dist-server/service/reference/reference-live.js.map +1 -1
- package/dist-server/service/reference/reference-master.d.ts +59 -3
- package/dist-server/service/reference/reference-master.js +34 -4
- package/dist-server/service/reference/reference-master.js.map +1 -1
- package/dist-server/service/reference/reference-probe.d.ts +92 -0
- package/dist-server/service/reference/reference-probe.js +186 -0
- package/dist-server/service/reference/reference-probe.js.map +1 -0
- package/dist-server/service/reference/reference-progress-subscription.d.ts +17 -0
- package/dist-server/service/reference/reference-progress-subscription.js +94 -0
- package/dist-server/service/reference/reference-progress-subscription.js.map +1 -0
- package/dist-server/service/reference/reference-progress.d.ts +38 -0
- package/dist-server/service/reference/reference-progress.js +71 -0
- package/dist-server/service/reference/reference-progress.js.map +1 -0
- package/dist-server/service/reference/reference-resolver.js +210 -43
- package/dist-server/service/reference/reference-resolver.js.map +1 -1
- package/dist-server/service/reference/twin-reference.d.ts +32 -0
- package/dist-server/service/reference/twin-reference.js +10 -0
- package/dist-server/service/reference/twin-reference.js.map +1 -1
- package/dist-server/service/twin-control/twin-control-mutation.js +4 -3
- package/dist-server/service/twin-control/twin-control-mutation.js.map +1 -1
- package/dist-server/service/twin-event/journal-count.d.ts +21 -0
- package/dist-server/service/twin-event/journal-count.js +26 -0
- package/dist-server/service/twin-event/journal-count.js.map +1 -0
- package/dist-server/service/twin-event/twin-event-keys.d.ts +9 -0
- package/dist-server/service/twin-event/twin-event-keys.js +15 -18
- package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
- package/dist-server/service/twin-event/twin-event-type.d.ts +3 -1
- package/dist-server/service/twin-event/twin-event-type.js +18 -1
- package/dist-server/service/twin-event/twin-event-type.js.map +1 -1
- package/dist-server/service/twin-event/twin-event.d.ts +1 -0
- package/dist-server/service/twin-event/twin-event.js +9 -1
- package/dist-server/service/twin-event/twin-event.js.map +1 -1
- package/dist-server/service/twin-forecast/twin-forecast-query.js +26 -2
- package/dist-server/service/twin-forecast/twin-forecast-query.js.map +1 -1
- package/dist-server/service/twin-ingest-window/index.d.ts +5 -0
- package/dist-server/service/twin-ingest-window/index.js +10 -0
- package/dist-server/service/twin-ingest-window/index.js.map +1 -0
- package/dist-server/service/twin-ingest-window/twin-ingest-window-query.d.ts +12 -0
- package/dist-server/service/twin-ingest-window/twin-ingest-window-query.js +123 -0
- package/dist-server/service/twin-ingest-window/twin-ingest-window-query.js.map +1 -0
- package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.d.ts +1 -0
- package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.js +51 -0
- package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.js.map +1 -0
- package/dist-server/service/twin-ingest-window/twin-ingest-window.d.ts +16 -0
- package/dist-server/service/twin-ingest-window/twin-ingest-window.js +108 -0
- package/dist-server/service/twin-ingest-window/twin-ingest-window.js.map +1 -0
- package/dist-server/service/twin-instance/twin-instance.js.map +1 -1
- package/dist-server/service/twin-journal/twin-journal-query.js +58 -5
- package/dist-server/service/twin-journal/twin-journal-query.js.map +1 -1
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.d.ts +14 -0
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +72 -2
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js.map +1 -1
- package/dist-server/service/twin-metrics/twin-metrics-query.d.ts +1 -0
- package/dist-server/service/twin-metrics/twin-metrics-query.js +78 -3
- package/dist-server/service/twin-metrics/twin-metrics-query.js.map +1 -1
- package/dist-server/service/twin-model/axis-journal-evidence.d.ts +2 -0
- package/dist-server/service/twin-model/axis-journal-evidence.js +89 -0
- package/dist-server/service/twin-model/axis-journal-evidence.js.map +1 -0
- package/dist-server/service/twin-model/epcis-coverage.d.ts +1 -1
- package/dist-server/service/twin-model/epcis-coverage.js +46 -7
- package/dist-server/service/twin-model/epcis-coverage.js.map +1 -1
- package/dist-server/service/twin-model/iec61850-coverage.d.ts +1 -1
- package/dist-server/service/twin-model/iec61850-coverage.js +18 -7
- package/dist-server/service/twin-model/iec61850-coverage.js.map +1 -1
- package/dist-server/service/twin-model/isa95-coverage.d.ts +14 -0
- package/dist-server/service/twin-model/isa95-coverage.js +108 -14
- package/dist-server/service/twin-model/isa95-coverage.js.map +1 -1
- package/dist-server/service/twin-model/item-ref.d.ts +24 -0
- package/dist-server/service/twin-model/item-ref.js +88 -0
- package/dist-server/service/twin-model/item-ref.js.map +1 -0
- package/dist-server/service/twin-model/name-index.d.ts +36 -0
- package/dist-server/service/twin-model/name-index.js +116 -0
- package/dist-server/service/twin-model/name-index.js.map +1 -0
- package/dist-server/service/twin-model/project-structure.js +1 -1
- package/dist-server/service/twin-model/project-structure.js.map +1 -1
- package/dist-server/service/twin-model/status-tally.d.ts +9 -0
- package/dist-server/service/twin-model/status-tally.js +37 -0
- package/dist-server/service/twin-model/status-tally.js.map +1 -0
- package/dist-server/service/twin-model/twin-lineage-query.js +40 -12
- package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -1
- package/dist-server/service/twin-model/twin-model-item-query.js +38 -39
- package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -1
- package/dist-server/service/twin-model/twin-model-query.js +156 -4
- package/dist-server/service/twin-model/twin-model-query.js.map +1 -1
- package/dist-server/service/twin-model/twin-model-tree-query.js +7 -0
- package/dist-server/service/twin-model/twin-model-tree-query.js.map +1 -1
- package/dist-server/service/twin-readiness/index.d.ts +2 -0
- package/dist-server/service/twin-readiness/index.js +6 -0
- package/dist-server/service/twin-readiness/index.js.map +1 -0
- package/dist-server/service/twin-readiness/twin-readiness-query.d.ts +3 -0
- package/dist-server/service/twin-readiness/twin-readiness-query.js +103 -0
- package/dist-server/service/twin-readiness/twin-readiness-query.js.map +1 -0
- package/dist-server/service/twin-space/twin-space-resolver.d.ts +2 -2
- package/dist-server/service/twin-space/twin-space-resolver.js +5 -4
- package/dist-server/service/twin-space/twin-space-resolver.js.map +1 -1
- package/dist-shared/entity-delta.d.ts +23 -3
- package/dist-shared/entity-delta.js +12 -8
- package/dist-shared/entity-delta.js.map +1 -1
- package/dist-shared/kpi-broadcast.js +1 -1
- package/dist-shared/kpi-broadcast.js.map +1 -1
- package/dist-shared/touched-items.d.ts +7 -0
- package/dist-shared/touched-items.js +80 -0
- package/dist-shared/touched-items.js.map +1 -0
- package/package.json +7 -7
- package/server/engine/canonical-ingest.ts +92 -7
- package/server/engine/command-routing.ts +1 -1
- package/server/engine/index.ts +4 -0
- package/server/engine/ingest-health.ts +782 -0
- package/server/engine/integration-coverage.ts +147 -0
- package/server/engine/integration-probes.ts +144 -0
- package/server/engine/integration-runner.ts +95 -0
- package/server/engine/integration-target-profile.ts +103 -0
- package/server/engine/kpi-fold.ts +72 -4
- package/server/engine/kpi-query.ts +129 -15
- package/server/engine/live-feed-registry.ts +2 -1
- package/server/engine/load-meter.ts +12 -3
- package/server/engine/log.ts +72 -0
- package/server/engine/loop-lag.ts +120 -0
- package/server/engine/model-gap.ts +168 -0
- package/server/engine/restart-policy.ts +4 -4
- package/server/engine/runtime-key.ts +1 -1
- package/server/engine/stage-path.ts +172 -0
- package/server/engine/twin-engine.ts +1334 -168
- package/server/index.ts +35 -2
- package/server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.ts +54 -0
- package/server/migrations/1786200000000-CarryLiveFeedCursor.ts +53 -0
- package/server/migrations/index.ts +7 -1
- package/server/service/index.ts +11 -0
- package/server/service/reference/discovery-result.ts +1 -1
- package/server/service/reference/index.ts +6 -1
- package/server/service/reference/reference-adapter.ts +220 -5
- package/server/service/reference/reference-assessment.ts +250 -0
- package/server/service/reference/reference-live.ts +143 -11
- package/server/service/reference/reference-master.ts +76 -6
- package/server/service/reference/reference-probe.ts +264 -0
- package/server/service/reference/reference-progress-subscription.ts +73 -0
- package/server/service/reference/reference-progress.ts +95 -0
- package/server/service/reference/reference-resolver.ts +200 -12
- package/server/service/reference/twin-reference.ts +39 -1
- package/server/service/twin-control/twin-control-mutation.ts +4 -3
- package/server/service/twin-event/journal-count.ts +53 -0
- package/server/service/twin-event/twin-event-keys.ts +16 -1
- package/server/service/twin-event/twin-event-type.ts +26 -2
- package/server/service/twin-event/twin-event.ts +7 -0
- package/server/service/twin-forecast/twin-forecast-query.ts +29 -2
- package/server/service/twin-ingest-window/index.ts +7 -0
- package/server/service/twin-ingest-window/twin-ingest-window-query.ts +125 -0
- package/server/service/twin-ingest-window/twin-ingest-window-writer.ts +57 -0
- package/server/service/twin-ingest-window/twin-ingest-window.ts +113 -0
- package/server/service/twin-instance/twin-instance.ts +1 -1
- package/server/service/twin-journal/twin-journal-query.ts +59 -5
- package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +77 -3
- package/server/service/twin-metrics/twin-metrics-query.ts +81 -3
- package/server/service/twin-model/axis-journal-evidence.ts +57 -0
- package/server/service/twin-model/epcis-coverage.ts +29 -8
- package/server/service/twin-model/iec61850-coverage.ts +2 -8
- package/server/service/twin-model/isa95-coverage.ts +107 -14
- package/server/service/twin-model/item-ref.ts +80 -0
- package/server/service/twin-model/name-index.ts +94 -0
- package/server/service/twin-model/project-structure.ts +1 -1
- package/server/service/twin-model/status-tally.ts +37 -0
- package/server/service/twin-model/twin-lineage-query.ts +38 -9
- package/server/service/twin-model/twin-model-item-query.ts +26 -27
- package/server/service/twin-model/twin-model-query.ts +155 -4
- package/server/service/twin-model/twin-model-tree-query.ts +7 -0
- package/server/service/twin-readiness/index.ts +3 -0
- package/server/service/twin-readiness/twin-readiness-query.ts +96 -0
- package/server/service/twin-space/twin-space-resolver.ts +5 -4
- package/shared/entity-delta.ts +31 -7
- package/shared/kpi-broadcast.ts +1 -1
- package/shared/touched-items.ts +73 -0
- package/test/axis-journal-evidence.test.ts +71 -0
- package/test/axis-read.test.ts +101 -1
- package/test/boot-resume.test.ts +92 -26
- package/test/broadcast-cost-baseline.test.ts +200 -0
- package/test/broadcast-period.test.ts +58 -0
- package/test/canonical-ingest-vocabularies.test.ts +36 -1
- package/test/canonical-quantity-door.test.ts +61 -0
- package/test/checkpoint-refuses-empty.test.ts +103 -0
- package/test/command-routing.test.ts +1 -1
- package/test/cursor-stall-not-read-failure.test.ts +126 -0
- package/test/declared-location-types.test.ts +89 -0
- package/test/discovery-result.test.ts +1 -1
- package/test/duration-estimators.test.ts +1 -1
- package/test/entity-delta.test.ts +2 -2
- package/test/ingest-bench.test.ts +3 -3
- package/test/ingest-health-engine.test.ts +248 -0
- package/test/ingest-health-wiring.test.ts +119 -0
- package/test/ingest-health.test.ts +306 -0
- package/test/ingest-history.test.ts +247 -0
- package/test/integration-probes.test.ts +103 -0
- package/test/integration-runner.test.ts +95 -0
- package/test/item-ref.test.ts +78 -0
- package/test/journal-read-discipline.test.ts +177 -0
- package/test/journal-retention.test.ts +133 -0
- package/test/journal-sort-axis.test.ts +142 -0
- package/test/journal-write-door.test.ts +110 -0
- package/test/journal-write-trend.test.ts +115 -0
- package/test/kernel-kind-guard.test.ts +4 -4
- package/test/kpi-fold.test.ts +90 -3
- package/test/kpi-query-bench.test.ts +2 -2
- package/test/lineage-survives-restart.test.ts +17 -1
- package/test/live-cursor-wiring.test.ts +87 -0
- package/test/live-feed-registry.test.ts +9 -1
- package/test/live-kernel-facts.test.ts +7 -7
- package/test/live-mirror-parity.test.ts +6 -0
- package/test/load-meter.test.ts +29 -15
- package/test/local-declarations.test.ts +1 -1
- package/test/log-stamp.test.ts +59 -0
- package/test/loop-lag.test.ts +82 -0
- package/test/mirror-resumes-from-checkpoint.test.ts +192 -0
- package/test/model-gap.test.ts +153 -0
- package/test/oee-accumulator.test.ts +4 -0
- package/test/operational-vocabulary.test.ts +4 -4
- package/test/read-failure-visible.test.ts +145 -0
- package/test/reference-grounding.test.ts +70 -0
- package/test/resolve-ts-siblings.mjs +52 -0
- package/test/restart-policy.test.ts +7 -7
- package/test/revision-axis.test.ts +104 -0
- package/test/runtime-key.test.ts +2 -2
- package/test/scale-twin-bench.test.ts +2 -2
- package/test/spec-coverage.test.ts +1 -1
- package/test/stage-path.test.ts +95 -0
- package/test/standard-coverage.test.ts +15 -3
- package/test/status-tally.test.ts +55 -0
- package/test/structure-revision-db.test.ts +4 -0
- package/test/tenant-registry-db.test.ts +1 -1
- package/test/time-range.test.ts +152 -0
- package/test/touched-items.test.ts +61 -0
- package/test/twin-event-keys.test.ts +10 -2
- package/test/twin-model-item-db.test.ts +2 -0
- package/test/warm-start-seam.test.ts +4 -0
- package/test/yield-loop.test.ts +57 -2
- package/tsconfig.shared.tsbuildinfo +1 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* (์ค๊ณ SoT: operato-twin/design/integration/things-factory-host.md)
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { twinLog, twinWarn, twinError } from './log.js'
|
|
10
11
|
import { pubsub, getRepository, Domain } from '@things-factory/shell'
|
|
11
12
|
/* ์ ๋์ ์๋ฅผ ์กฐ๊ฑด์ SQL ์ด ์๋ค โ ๊ด์ฉ๊ตฌ๋ง ์ด๋ค(์์ SQL ์ 5๊ฐ ๋๋ผ์ด๋ฒ์์ ๊ฐ๋ผ์ง๋ค). */
|
|
12
13
|
import { And, IsNull, LessThanOrEqual, MoreThan } from 'typeorm'
|
|
@@ -44,12 +45,29 @@ import { createHash } from 'node:crypto'
|
|
|
44
45
|
|
|
45
46
|
import { buildEntityDeltas } from '@things-factory/headless-twin/dist-shared/entity-delta.js'
|
|
46
47
|
import { fleetLoad, judgeCycle, loadSummary, newLoadMeter, recordFork, recordPhase, slowTickMessage, type LoadMeter, type LoadPhase } from './load-meter.js'
|
|
48
|
+
import { loopLag } from './loop-lag.js'
|
|
49
|
+
import { touchedItemKeys } from '@things-factory/headless-twin/dist-shared/touched-items.js'
|
|
47
50
|
import { diffStructures, type StructureDiff } from './structure-diff.js'
|
|
48
51
|
import { OeeAccumulator, withLiveOee } from './oee-accumulator.js'
|
|
49
52
|
import { withLiveAttentions } from './live-attentions.js'
|
|
50
53
|
import { digestAttentions, mergeLensAttentions } from './attention-digest.js'
|
|
51
54
|
|
|
52
55
|
import { liveFeedStateOf } from './live-feed-registry.js'
|
|
56
|
+
import {
|
|
57
|
+
ingestHealth,
|
|
58
|
+
newIngestLedger,
|
|
59
|
+
recordIngest,
|
|
60
|
+
recordReadFailure,
|
|
61
|
+
clearCursorStall,
|
|
62
|
+
clearReadFailure,
|
|
63
|
+
recordCursorStall,
|
|
64
|
+
rollIngestWindow,
|
|
65
|
+
/* ๋ณ์นญ โ ๊ฐ์ ์ด๋ฆ์ ์ ์ ๋ฉ์๋์ ํท๊ฐ๋ฆฌ์ง ์๊ฒ(๊ทธ ๋ฉ์๋๊ฐ ์ด๊ฒ์ ๋ถ๋ฅธ๋ค). */
|
|
66
|
+
recordJournalWrite as recordLedgerWrite,
|
|
67
|
+
type IngestHealthView,
|
|
68
|
+
type IngestLedger,
|
|
69
|
+
type IngestWindow
|
|
70
|
+
} from './ingest-health.js'
|
|
53
71
|
import { EMS_PROPERTY } from '@operato/twin-kernel'
|
|
54
72
|
import type { TwinKernel, TwinModelDef, StructureShift, SubscriptionMessage, TwinRuntime as TwinRuntimeType, CanonicalEnvelope } from '@operato/twin-kernel'
|
|
55
73
|
|
|
@@ -59,7 +77,44 @@ const { WmsKernel, YmsKernel, MesKernel, EmsKernel, TwinRuntime, StateProjector,
|
|
|
59
77
|
const KERNELS: Record<string, any> = { wms: WmsKernel, yms: YmsKernel, mes: MesKernel, ems: EmsKernel }
|
|
60
78
|
|
|
61
79
|
/**
|
|
62
|
-
*
|
|
80
|
+
* ์๊ฐ์ถ์ ๊ตฌํ ๋ ํ ๋ฒ์ ๋์ฐ๋ ํธ์ ์.
|
|
81
|
+
*
|
|
82
|
+
* ํธ์๋ง๋ค ์๋ณต์ด ๋์ด๋ผ, ๊ณต๊ฐ์ ํธ์์ด ์๋ฐฑ์ด๋ฉด ๋์ ์๋ณต๋ ์๋ฐฑ์ด ๋๋ค โ ์ปค๋ฅ์
ํ์ด ๋ง๋ฅด๋ฉด ์ด
|
|
83
|
+
* ํจ์ ํ๋ ๋๋ฌธ์ ๋ค๋ฅธ ์ง์๊ฐ ์ค์ ์ ๋ค. 20์ด๋ฉด ์๋ณต 40๊ฐ๋ก ์ง์ฐ์ ๊ฐ์ถ๋ฉด์ ํ์ ๋จ๋๋ค.
|
|
84
|
+
*/
|
|
85
|
+
const TIME_RANGE_FANOUT = 20
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* DB ๊ฐ ์ค ์๊ฐ์ epoch ms ๋ก โ **๋ชจ์์ด ๋๋ผ์ด๋ฒ๋ง๋ค ๋ค๋ฅด๋ค.**
|
|
89
|
+
*
|
|
90
|
+
* ์ง๊ณ(MIN/MAX)์ ๋ฐํ์ ๋๋ผ์ด๋ฒ๊ฐ ์ ํ๋ค: sqlite ๋ ๋ฌธ์์ด, pgยทmysqlยทmssql ์ `Date`. ์ํฐํฐ
|
|
91
|
+
* ํ์ด๋๋ ์ด์
์ ๊ฑด๋๋ฐ๋ฉด TypeORM ์ ๋ ์ง ๋ณํ๋ ํจ๊ป ๊ฑด๋๋ฐ๋ฏ๋ก ๋ฐ๋ ์ชฝ์์ ํ ๋ฒ ์ ๊ทํํ๋ค.
|
|
92
|
+
*
|
|
93
|
+
* โโ ์๊ฐ๋๋ฅผ ์์ง ์๋๋ค (์ํ์ด ์ก์๋ค) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
94
|
+
* ์ฒ์์ `Date.parse(String(v))` ๋ก ๋์๋๋ **9์๊ฐ์ด ๋ฐ๋ ธ๋ค.** sqlite ๋ UTC ๋ก ์ ์ฅํ ๊ฐ์
|
|
95
|
+
* `2026-01-15 09:00:00.000` ์ฒ๋ผ **์๊ฐ๋ ํ์ ์์ด** ๋๋ ค์ฃผ๋๋ฐ, ๊ทธ ํํ๋ฅผ `Date.parse` ๋ **๋ก์ปฌ
|
|
96
|
+
* ์๊ฐ**์ผ๋ก ์ฝ๋๋ค(์ฌ๊ธฐ๊ฐ KST ๋ผ UTC 00:00 ์ด ๋๋ค). ์๋ ์ฝ๋๊ฐ ์ด ํจ์ ์ ํผํ ๊ฒ์ TypeORM ์ด
|
|
97
|
+
* ํ์ด๋๋ ์ด์
์์ ๋จผ์ `Date` ๋ก ๋ฐ๊ฟ ์คฌ๊ธฐ ๋๋ฌธ์ด๋ค โ ๊ทธ ๋จ๊ณ๋ฅผ ๊ฑด๋๋ด ๋๊ฐ๋ฅผ ์ฌ๊ธฐ์ ์น๋ฅธ๋ค.
|
|
98
|
+
*
|
|
99
|
+
* ๊ทธ๋์ ์๊ฐ๋ ํ์๊ฐ **์๋ ๋ฌธ์์ด์ UTC ๋ก ์ฝ๋๋ค**(TypeORM ์ด UTC ๋ก ์ ์ผ๋ฏ๋ก). ํ์๊ฐ ์์ผ๋ฉด
|
|
100
|
+
* ๊ทธ๋๋ก ๋ฏฟ๊ณ , `Date` ๋ ์๋์ง ์๋๋ค.
|
|
101
|
+
*
|
|
102
|
+
* ์ฝ์ ์ ์๋ ๊ฐ์ **์์(`null`)** ์ด๋ค โ 0 ์ผ๋ก ๋จ์ด๋จ๋ฆฌ๋ฉด 1970๋
์ด ์๊ฐ์ถ์ ์์์ด ๋๋ค.
|
|
103
|
+
*/
|
|
104
|
+
function parseTime(v: unknown): number | null {
|
|
105
|
+
if (v == null) return null
|
|
106
|
+
if (v instanceof Date) return Number.isFinite(v.getTime()) ? v.getTime() : null
|
|
107
|
+
|
|
108
|
+
const s = String(v).trim()
|
|
109
|
+
if (!s) return null
|
|
110
|
+
/* ๋์ `Z` ๋ `ยฑhh:mm` ์ด ๋ถ์ด ์์ผ๋ฉด ์๊ฐ๋๋ฅผ ์๋ ๋ฌธ์์ด์ด๋ค โ ๊ทธ๋๋ก ๋ฏฟ๋๋ค. */
|
|
111
|
+
const zoned = /(?:Z|[+-]\d{2}:?\d{2})$/.test(s)
|
|
112
|
+
const t = Date.parse(zoned ? s : `${s.replace(' ', 'T')}Z`)
|
|
113
|
+
return Number.isFinite(t) ? t : null
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* ์ข
๋ฅ ๋ฌธ์์ด โ ์ปค๋. **๋ชจ๋ฅด๋ ๊ฐ์ด๋ฉด ์ค๋ฅ๋ฅผ ๋ธ๋ค.**
|
|
63
118
|
*
|
|
64
119
|
* ์์ ์๋ ํ๋ฅผ ์ฐพ๊ณ ์์ผ๋ฉด WmsKernel ๋ก ๋จ์ด์ก๋ค. `kind` ๋ ๊ฒ์ฆ ์๋ ์์ ๋ฌธ์์ด(`@Arg('kind') kind: string`)
|
|
65
120
|
* ์ด๋ผ ์คํ ํ๋ยท๋์๋ฌธ์ ํ๋๋ก ์ผ๋/์์ฐ ํธ์์ด **์กฐ์ฉํ ์ฐฝ๊ณ ์ปค๋๋ก ๋์๋ค.** ์ค๋ฅ๊ฐ ์์ผ๋ ํ๋ฉด์๋
|
|
@@ -74,15 +129,57 @@ function kernelFor(kind: string): any {
|
|
|
74
129
|
return K
|
|
75
130
|
}
|
|
76
131
|
|
|
77
|
-
|
|
132
|
+
/** `observe()` ๋ฅผ ๊ฐ์ง ์์ ์ปค๋์ ๋ง๋ ์ ์ด ์๋๊ฐ โ ๊ฒฝ๊ณ ๋ฅผ ํ ๋ฒ๋ง ๋ธ๋ค(ํฑ๋ง๋ค ์์ง ์๋๋ค). */
|
|
133
|
+
let observeUnavailableWarned = false
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* **์ด ์ปค๋์ ์ง์ค์ด ์๋ณธ์์ ์จ๋ค๊ณ ์ ์ธํ๋ค** โ ๊ทธ๋ฆฌ๊ณ ์ ์ธ์ด ๋ฟ์ง ์์์ผ๋ฉด ๋งํ๋ค.
|
|
137
|
+
*
|
|
138
|
+
* โโ ๋ฌด์์ด ํ๋ ธ๋ (2026-08-21) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
139
|
+
* ์์ ์๋ `kernel.observe?.()` ์๋ค. ์ต์
๋ ํธ์ถ์ด๋ผ **๋ฉ์๋๊ฐ ์์ผ๋ฉด ์กฐ์ฉํ ์๋ฌด๊ฒ๋ ํ์ง ์๋๋ค.**
|
|
140
|
+
* ์ค์น๋ ์ปค๋ 0.7.39 ์๋ ๊ทธ ๋ฉ์๋๊ฐ ์์ผ๋ฏ๋ก, ์ง๊ธ ๋ฐฐํฌ๋ณธ์์๋ ๋ฏธ๋ฌ๊ฐ ๊ด์ธก ๊ตฌ๋์ผ๋ก ์ ์ธ๋์ง
|
|
141
|
+
* ์๋๋ค โ ๊ทธ๋ฐ๋ฐ ์ฝ๋๋ฅผ ์ฝ์ผ๋ฉด ์ ์ธํ ๊ฒ์ฒ๋ผ ๋ณด์ธ๋ค.
|
|
142
|
+
*
|
|
143
|
+
* ๊ทธ ์ฐจ์ด๊ฐ ์ค์ ๊ฑฐ๋์ ๊ฐ๋ฅธ๋ค: ๊ด์ธก ๊ตฌ๋์ ์๋ณธ์ ๋นํ์ ๋ฐ์๋ค์ด๊ณ ์ธ์ง๋ง, ์๋ฎฌ๋ก ์ค์ธ๋ ๋ฏธ๋ฌ๋
|
|
144
|
+
* **๋ฉ์ถ๋ค.** ์ฆ ใ๊ด์ฉ์ด ์ผ์ง ์ค ์์๋๋ฐ ์ ์ผ์ ธ ์๋คใ๊ฐ ์กฐ์ฉํ ์ง๋๊ฐ๊ณ , ์ฅ์ ๊ฐ ๋ฌ์ ๋ ์์ธ์ด
|
|
145
|
+
* ์ปค๋์ ์๋ ๊ฒ์ฒ๋ผ ๋ณด์ธ๋ค โ ์์ธ์ ๋ฒ์ ์ด๋ค.
|
|
146
|
+
*
|
|
147
|
+
* ๊ทธ๋์ ์ต์
๋ ํธ์ถ์ ์์ ๊ณ , ์์ผ๋ฉด **ํ ๋ฒ ๊ฒฝ๊ณ ํ๋ค.** ์ปค๋์ด ๋ฐฐํฌ๋๋ฉด ์ด ๊ฒฝ๊ณ ๋ ์ฌ๋ผ์ง๋ค.
|
|
148
|
+
*/
|
|
149
|
+
function declareObserved(kernel: any, what: string): void {
|
|
150
|
+
if (typeof kernel?.observe === 'function') {
|
|
151
|
+
kernel.observe()
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
if (observeUnavailableWarned) return
|
|
155
|
+
observeUnavailableWarned = true
|
|
156
|
+
twinWarn(
|
|
157
|
+
`[twin-engine] ${what}: kernel has no observe() โ this mirror is not declared observation-driven. ` +
|
|
158
|
+
'Source gaps will stop it instead of being tolerated and counted. Deploy a kernel that provides observe().'
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/* ๋ผ์ด๋ธ ์ฒ๋ฆฌ๋ ๊ณ์ธก(๋ชจ๋ํฐ) โ ์ ์
/๋ธ๋ก๋์บ์คํ
/์ ๋๋ฅ ยท๋ฐฑ๋ก๊ทธ. ์ค ๋๊ธฐ ์ธํฐํ์ด์ค ๋ถํ๋ฅผ ์ฝ๋ ์ผ๊ธ ์งํ. */
|
|
78
163
|
interface TwinMetrics {
|
|
79
164
|
ingestedTotal: number // ๋์ ์ ์
์ด๋ฒคํธ
|
|
80
|
-
broadcastTotal: number // ๋์
|
|
165
|
+
broadcastTotal: number // ๋์ ๋ธ๋ก๋์บ์คํ
ํ์
|
|
81
166
|
journaledTotal: number // ๋์ ์ ๋ ๊ธฐ๋ก ์ด๋ฒคํธ
|
|
82
167
|
ingestRate: number // ์ต๊ทผ ์ ์
๋ฅ (ev/s)
|
|
83
|
-
broadcastRate: number // ์ต๊ทผ
|
|
168
|
+
broadcastRate: number // ์ต๊ทผ ๋ธ๋ก๋์บ์คํ
๋ฅ (ํ/s)
|
|
84
169
|
journalRate: number // ์ต๊ทผ ์ ๋๋ฅ (ev/s)
|
|
85
|
-
|
|
170
|
+
/**
|
|
171
|
+
* **์ง๊ธ ๋๊ธฐ ์ค์ธ** ์ ๋ ๊ฑด์ โ ์์ง DB ๋ก ๋๊ฐ์ง ์์ ๊ฒ.
|
|
172
|
+
*
|
|
173
|
+
* โโ ๋ฌด์์ด ํ๋ ธ์๋ (2026-08-21) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
174
|
+
* ์์ ์๋ ใ๋ง์ง๋ง flush ์์ ์ด ๊ฑด์ใ๋ฅผ ์ฌ๊ธฐ ๋ฃ์๋ค. ๊ทธ๋ฐ๋ฐ 200ms ๋ง๋ค ๋ชจ์ ํ ๋ฌธ์ผ๋ก ๋ฃ๋ ๊ฒ์
|
|
175
|
+
* **์ ์ ๋์**์ด๋ค(๊ทธ๋ ๊ฒ ๋ง๋ ๊ฒ์ด ์ด์ ๊ณ ์น ์ฑ๋ฅ ์์
์ด๋ค). ๊ทธ๋์ ์ ์์ ์ผ๋ก 10๊ฑด์ ์ด ์งํ
|
|
176
|
+
* ํ๋ฉด์ ใ๋ฐ๋ฆผ 10ใ ๊ฒฝ๊ณ ๊ฐ ๋ด๊ณ , ๋ค์ flush ๊น์ง ๊ทธ ๊ฐ์ด ๋จ์๋ค โ ํ๋ ์ด๋ฏธ 0 ์ธ๋ฐ๋.
|
|
177
|
+
*
|
|
178
|
+
* ์ด์์์๊ฒ ๊ทธ๊ฒ์ ใ์ ์ฅ์ด 10๊ฑด ๋ฐ๋ ค ์๋คใ๋ก ์ฝํ๋ค. ๊ฑฐ์ง ๊ฒฝ๋ณด๋ ์ฐธ ๊ฒฝ๋ณด๋ฅผ ์ฃฝ์ธ๋ค.
|
|
179
|
+
*
|
|
180
|
+
* ์ด์ ์ด ๊ฐ์ **์ง๊ธ ๋จ์ ์๋ ๊ฒ**์ด๋ค: ๋ค ๋๊ฐ์ผ๋ฉด 0 ์ด๊ณ , ์ ๋ง๋ก ์์ด๋ ์ค์ด๋ฉด ๊ทธ ์๊ฐ ์๋๋ค.
|
|
181
|
+
*/
|
|
182
|
+
backlog: number
|
|
86
183
|
// ์ฐฝ(window) ์ง๊ณ์ฉ ๋ด๋ถ ๋์
|
|
87
184
|
_accIngest: number
|
|
88
185
|
_accBroadcast: number
|
|
@@ -127,8 +224,19 @@ interface InstanceRuntime {
|
|
|
127
224
|
timer?: any
|
|
128
225
|
/** ์ํฐํฐ๋ณ ๋ง์ง๋ง ๋ฐํ ์๊ทธ๋์ฒ โ ๊ฐ์ด ๋ฐ๋ ์ํฐํฐ๋ง ์ฌ๋ฐํ(๋ฌด๋ณํ ๋ฐ๋ณต push ๋ฐฉ์ง). */
|
|
129
226
|
entitySigs?: Map<string, string>
|
|
130
|
-
/** live
|
|
227
|
+
/** live ๋ธ๋ก๋์บ์คํ
๋ณํฉ(ingest-scale ยง4.4) โ ingest ๋ง๋ค ๋ธ๋ก๋์บ์คํ
ํ์ง ์๊ณ dirty ํ์ โ coalescer tick ์ด ์ฃผ๊ธฐ ๋ธ๋ก๋์บ์คํ
. */
|
|
131
228
|
dirty?: boolean
|
|
229
|
+
/**
|
|
230
|
+
* ์ด ์ฐฝ์ **๊ฑด๋๋ฆฐ ๋ฌผํ ํ๊ทธ** โ ๋ธ๋ก๋์บ์คํ
์ด ๊ทธ๊ฒ๋ง ๋ง๋ ๋ค.
|
|
231
|
+
*
|
|
232
|
+
* `undefined` ๋ ใ๋งํ ์ ์๋คใ๋ค(๋ฏ์ ์ฌ๊ฑด์ด ํ๋๋ผ๋ ์๋ค) โ ์ ๋ถ ๋ง๋ ๋ค. ๋น ์งํฉ์ ใ๋ฌผํ์
|
|
233
|
+
* ํ๋๋ ๊ฑด๋๋ฆฌ์ง ์์๋คใ์ด๊ณ , ๊ทธ ๋์ ๋ค๋ฅธ ์ฌ์ค์ด๋ค.
|
|
234
|
+
*/
|
|
235
|
+
dirtyItems?: Set<string>
|
|
236
|
+
/** ๋ค์ ๋ธ๋ก๋์บ์คํ
์์ ์ ๋ถ ๋ง๋ค์ด์ผ ํ๋ โ ์ฃผ๊ธฐ๋ง๋ค ํ ๋ฒ์ ๊ทธ๋ฌผ๋ก ์ ๋ถ ๋ง๋ ๋ค. */
|
|
237
|
+
fullBroadcastDue?: boolean
|
|
238
|
+
/** ์ ๋ถ ๋ง๋ค๊ธฐ๊น์ง ๋จ์ ์ฐฝ ์. */
|
|
239
|
+
fullBroadcastCountdown?: number
|
|
132
240
|
/** live ์ ๋ ๊ฒฐ์ โ ingest ์ด๋ฒคํธ๋ฅผ ๋ชจ์๋ค๊ฐ coalescer tick ์์ ๋ฐฐ์น ๊ธฐ๋ก(์ด๋ฒคํธ๋ง๋ค DB write ๊ธ์ง). */
|
|
133
241
|
pendingJournal?: any[]
|
|
134
242
|
/** live ์ ๋ revision ์นด์ดํฐ(sim ์ delta revision ๋์ โ ์๊ฐ์ฌํยทํ์คํ ๋ฆฌ ๋
ธ๋ธ ์ต์ปค). */
|
|
@@ -169,13 +277,44 @@ export class TwinEngine {
|
|
|
169
277
|
* ๋ฉ์ถ๋ค โ HTTPยท๊ตฌ๋
ยท๋ค๋ฅธ ํธ์์ ํฑ๊น์ง. ์ค์ธก์ผ๋ก `order-check` ์ ํฑ ํ๋๊ฐ 34.9์ด์๊ณ , ๊ทธ ์ฌ์ด
|
|
170
278
|
* ๊ตฌ๋
์๊ฐ ์๋ฌด๊ฒ๋ ๋นผ๋ด์ง ๋ชปํด pubsub ์ด ๋์ณ ํ๋ก์ธ์ค๊ฐ ์ฃฝ์๋ค.
|
|
171
279
|
*
|
|
172
|
-
*
|
|
280
|
+
* ๋ธ๋ก๋์บ์คํ
๋ฐ๋ณต์ ๊ฑท์ด๋์ง๋ง(`flushLiveBroadcasts` ๋ก ๋ณํฉ) **์ปค๋ ํฑ ์์ฒด๋ ์ฌ์ ํ ๋ฉ์ธ ๋ฃจํ์ ์๋ค.**
|
|
173
281
|
* ๊ทผ๋ณธ ํด๊ฒฐ์ ๋ถ์ฐ์ด๊ณ ๊ทธ๊ฒ์ ์ด์ฐ๋๋ค โ ๊ทธ๋๊น์ง์ ์์ ๋ง์ด ์ด ์
์ด๋ค.
|
|
174
282
|
*
|
|
175
283
|
* ํ์ ์ ์์ฐ(500ms)์ด ์๋๋ผ **๊ตถ๊น ๋ฌธํฑ**์ผ๋ก ๋ฐ๋ก ๋๋ค: ์กฐ๊ธ ๋๋ฆฐ ํธ์์ ๊ณ๊ธฐํ์ด ๋งํ๊ฒ ๋๊ณ
|
|
176
284
|
* (๊ฒฝ๊ณ ), ํธ์คํธ๋ฅผ ๊ตถ๊ธฐ๋ ํธ์๋ง ๋ฉ์ถ๋ค. ํ ๋ฒ์ผ๋ก ๋ฉ์ถ์ง ์๋๋ค โ ์์คํํธ ์งํ์ ์ฒซ ํฑ์ ์๋
|
|
177
285
|
* ๋ฌด๊ฒ๋ค(๋ณต๊ตฌํ ์ํ๋ฅผ ์ฒ์ ์ ๋๋ค). **์ฐ์**์ผ๋ก ์ด์ด์ง ๋๊ฐ ๊ตฌ์กฐ์ ์ผ๋ก ๋๋ฆฐ ๊ฒ์ด๋ค.
|
|
178
286
|
*/
|
|
287
|
+
/*
|
|
288
|
+
* โโ ํฑ์ **ํ ์๊ฐ์ ๋ชฐ์ง ์๋๋ค** (2026-08-21 ์ค์ธก) โโโโโโโโโโโโโโโโโโโโโโโโ
|
|
289
|
+
* ํธ์๋ง๋ค `setInterval(TICK_MS)` ๋ฅผ ๋ถํ
๋ ๋๋ํ ์ธ์ฐ๋ฉด, ์ค๋ฌด ๊ฐ๊ฐ **๊ฐ์ ๋ฐ๋ฆฌ์ด์** ๊นจ์ด๋๋ค.
|
|
290
|
+
* ๊ฐ์์ ํฑ์ด ์งง์๋(์ค์ธก: ๋ฌผํ 2,400 ๊ฐ์์ 0.23ms) ๊ทธ ์๊ฐ์๋ ์ค๋ฌด ๊ฐ๊ฐ ํ ์ค๋ก ๋ถ์ด ์คํ๋๊ณ ,
|
|
291
|
+
* ๊ทธ ์ฌ์ด ๋์ฐฉํ HTTP ์์ฒญ์ ์ ๋ถ ๋ค์์ ๊ธฐ๋ค๋ฆฐ๋ค. ์ด๋น ํ ๋ฒ์ ์ ์ฒด๊ฐ ใ๋๋ฅผ ๋๋ง๋ค ๊ฑธ๋ฆฐ๋คใ๋ก
|
|
292
|
+
* ๋ํ๋๋ ์๋ฆฌ๋ค.
|
|
293
|
+
*
|
|
294
|
+
* ๊ทธ๋์ ์ฒซ ๋ฐํ๋ง ๊ฐ๊ฒฉ ์์์ **๊ณ ๋ฅด๊ฒ ํฉ๋๋ค** โ ์ด ์์
๋์ ๊ฐ๊ณ ํ ์๊ฐ์ ์ต๋์น๋ง ๋ฎ์์ง๋ค.
|
|
295
|
+
* ํธ์์ด ๊ฐ๊ฒฉ๋ณด๋ค ๋ง์์ง๋ฉด ๋ค์ ๊ฒน์น๋ฏ๋ก, ๊ทธ๋๋ ์ด ํฉ๊ธฐ๊ฐ ์๋๋ผ ๋ถ์ฐ์ด ๋ต์ด๋ค(์ด์ฐ๋จ).
|
|
296
|
+
*/
|
|
297
|
+
/** ๋ถํ
๋ ํธ์ ํ๋๋ฅผ ๋์ด๋ฆฐ ๋ค ๋ฃจํ๋ฅผ ๋น์ ์ฃผ๋ ์๊ฐ(ms) โ ๊ทธ ์ฌ์ด ๋์ฐฉํ ์์ฒญ์ด ์ฒ๋ฆฌ๋๋ค. */
|
|
298
|
+
static BOOT_YIELD_MS = 25
|
|
299
|
+
private static tickPhase = 0
|
|
300
|
+
/**
|
|
301
|
+
* ํฉ์ด์ง ์ฒซ ๋ฐํ ๋ค ์ฃผ๊ธฐ ํฑ. **ํธ๋ค์ ํญ์ ์ง์ง ํ์ด๋จธ**๋ค โ ์ ์งํ๋ ์ชฝ์ด `clearInterval(inst.timer)`
|
|
302
|
+
* ํ๋๋ก ๋๋ด์ผ ํ๋ฏ๋ก, ์ฒซ ๋ฐํ ์ ์๋ ๊ทธ `setTimeout` ์, ์ดํ์๋ `setInterval` ์ ๊ฐ์ ์๋ฆฌ์ ๋๋ค
|
|
303
|
+
* (๊ฐ์ผ ๊ฐ์ฒด๋ฅผ ์ฃผ๋ฉด `clearInterval` ์ด ์๋ฌด ์ผ๋ ํ์ง ์๊ณ ํธ์์ด ๋ฉ์ถ์ง ์๋๋ค โ ์กฐ์ฉํ ๊ฒฐํจ์ด ๋๋ค).
|
|
304
|
+
*/
|
|
305
|
+
private static startTickTimer(fn: () => void, hold: (t: any) => void): any {
|
|
306
|
+
const spread = Math.max(1, Math.round(this.TICK_MS / 20))
|
|
307
|
+
const offset = (this.tickPhase = (this.tickPhase + spread) % this.TICK_MS)
|
|
308
|
+
const first = setTimeout(() => {
|
|
309
|
+
const interval = setInterval(fn, this.TICK_MS)
|
|
310
|
+
if (typeof interval?.unref === 'function') interval.unref()
|
|
311
|
+
hold(interval) // ์ ์ง ๊ฒฝ๋ก๊ฐ ์ง์ธ ๋์์ ๋ฐ๊ฟ ์ค๋ค
|
|
312
|
+
fn()
|
|
313
|
+
}, offset)
|
|
314
|
+
if (typeof first?.unref === 'function') first.unref()
|
|
315
|
+
return first
|
|
316
|
+
}
|
|
317
|
+
|
|
179
318
|
/** ๊ตถ๊น ๋ฌธํฑ โ ํฑ ๊ฐ๊ฒฉ์ ๋ฐฐ์(1์ด ๊ฐ๊ฒฉ์ด๋ฉด 5์ด). ์ด ์๊ฐ๋งํผ ํธ์คํธ๊ฐ ๋ฉ์ถ๋ค. */
|
|
180
319
|
static STARVE_FACTOR = 5
|
|
181
320
|
/** ์ฐ์ ๋ช ๋ฒ์ด๋ฉด ๋ฉ์ถ๋ โ 3๋ฒ์ด๋ฉด 15์ด๋ฅผ ๊ตถ๊ธด ์
์ด๊ณ , ๊ทธ๊ฑด ์ฐ์ฐ์ด ์๋๋ค. */
|
|
@@ -208,6 +347,44 @@ export class TwinEngine {
|
|
|
208
347
|
static CHAIN_KEEP = 5 // ์ต๊ทผ ๋ช ์ง์ ์ ๋ค๊ณ ์๋(๊ทธ๋ณด๋ค ๊ณผ๊ฑฐ๋ 0๋ถํฐ ์ ๋๋ค)
|
|
209
348
|
static SNAPSHOT_TTL_S = 7 * 24 * 3600 // 7์ผ โ ์ ์ ๋ค์ดํ์ ์์กด, ๋ง๋ฃ ์ ์ ๋ replay ํด๋ฐฑ
|
|
210
349
|
static CHECKPOINT_MS = 20000 // ์ฒดํฌํฌ์ธํธ ์ฃผ๊ธฐ(ํซ ๋ธ๋ก๋์บ์คํธ ๊ฒฝ๋ก์ ๋ถ๋ฆฌ, O(state) ์ค๋กํ)
|
|
350
|
+
/**
|
|
351
|
+
* **์ ๋ ๋ณด์กด โ ์ง์ฐ๋ ๊ฒ์ ์ ์ธ์ด ์์ ๋๋ง ํ๋ค.**
|
|
352
|
+
*
|
|
353
|
+
* โโ ์ ํ์ํ๊ฐ (2026-08-22 ์ค์ธก) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
354
|
+
* ๊ฐ๋ฐ ์ ๋์ด ์๊ฐ๋น **581,794ํ ยท 0.85GB** ๋ก ์๋๋ค(ํ๋ฃจ 20GB). ๋ฐ๋ชจ MES ํธ์ ์
์ด ์ ์ฒด์ **97%**
|
|
355
|
+
* ๋ฅผ ๋ง๋ค๊ณ ์ง์ฐ๋ ๊ฒ์ด ์์๋ค. ์ค๋ ๊ณ ์น ๊ฒ๋ค์ ใ๊ทธ ํฌ๊ธฐ์์๋ ์ง์๊ฐ ๋น ๋ฅด๊ฒใ์ด๊ณ , **ํฌ๊ธฐ ์์ฒด๋ฅผ
|
|
356
|
+
* ์ค์ด๋ ๊ฒ์ ์๋ฌด๊ฒ๋ ์์๋ค.** ๊ทธ๋ฌ๋ฉด ๋ฉฐ์น ๋ง๋ค ๊ฐ์ ์๋ฆฌ๋ก ๋์์จ๋ค.
|
|
357
|
+
*
|
|
358
|
+
* โโ ๊ทธ๋ฐ๋ฐ ์ ๋์ ์ง์ฐ๋ ๊ฒ์ ์ฌ์ค์ ์๋ ์ผ์ด๋ค โโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
359
|
+
* ๊ทธ๋์ ์ธ ๊ท์จ์ ์งํจ๋ค.
|
|
360
|
+
*
|
|
361
|
+
* โ **์ ์ธ์ด ์์ผ๋ฉด ์๋ฌด๊ฒ๋ ์ง์ฐ์ง ์๋๋ค.** ๊ธฐ๋ณธ๊ฐ์ ์์์ด๋ค โ ์กฐ์ฉํ ์ง์ฐ๋ ํธ์ด ์กฐ์ฉํ ์๋
|
|
362
|
+
* ๊ฒ๋ณด๋ค ๋์๋ค. ์ง์ฐ๋ ๊ฒ์ ์ฌ๋์ด ์ ํ๋ค.
|
|
363
|
+
* โก **์ฒดํฌํฌ์ธํธ๊ฐ ๋์ ํ ์ ์๋ ๋งํผ๋ง.** ์ค๋
์ท์ด ์๊ฑฐ๋ ๊ทธ ๋ฆฌ๋น์ ์ ๋๋ ์๋ฆฌ๋ ๊ฑด๋๋ฆฌ์ง ์๋๋ค.
|
|
364
|
+
* ์ฃผ์์ด ใ๋ง๋ฃ ์ ์ ๋ replay ํด๋ฐฑใ์ด๋ผ๊ณ ์ ์ด ๋ ๊ทธ๋๋ก โ ์ค๋
์ท์ด ์ฌ๋ผ์ง๋ฉด ์ ๋์ด **์ ์ผํ**
|
|
365
|
+
* ๋ณต๊ตฌ ์๋จ์ด๋ฏ๋ก, ๋์ ํจ๊ป ์์ผ๋ฉด ๊ทธ ํธ์์ ์ํ๋ ๋๋๋ฆด ์ ์๋ค.
|
|
366
|
+
* โข **์ง์ด ๊ฒ์ ๋งํ๋ค.** ๋ช ๊ฑด์ ์ด๋ ์๊ฐ๊น์ง ์ง์ ๋์ง ๋ก๊ทธ์ ๋จ๊ธด๋ค. ์กฐ์ฉํ ์ค์ด๋ ์ ๋์
|
|
367
|
+
* ใ์์๋ ์ผใ๊ณผ ๊ตฌ๋ณ๋์ง ์๋๋ค.
|
|
368
|
+
*
|
|
369
|
+
* ๊ทธ๋ฆฌ๊ณ ๋ณด์กด ๊ธฐ๊ฐ์ **์ค๋
์ท TTL(7์ผ)๋ณด๋ค ์งง์ ์ ์๋ค.** ๋ ์งง์ผ๋ฉด ์ค๋
์ท์ด ์ด์ ์๋๋ฐ ๊ทธ๊ฒ์ด
|
|
370
|
+
* ๊ฐ๋ฆฌํค๋ ์์ชฝ ์ ๋์ด ์๋ ๊ตฌ๊ฐ์ด ์๊ธฐ๊ณ , ์๊ฐ์ฌํยท๊ณ๋ณด ์ถ์ ์ด ๊ทธ ๊ตฌ๊ฐ์์ ์กฐ์ฉํ ๋น๋ค.
|
|
371
|
+
*/
|
|
372
|
+
static JOURNAL_RETENTION_DAYS?: number = undefined
|
|
373
|
+
/**
|
|
374
|
+
* **๋๋ฉ์ธ๋ง๋ค ๋ค๋ฅธ ๋ณด์กด ๊ธฐ๊ฐ์ ์ฑ์ด ๋ตํ๋ค** โ ์์ง์ ๊ทธ ๊ฐ์ด ์ด๋์ ์ค๋์ง ๋ชจ๋ฅธ๋ค.
|
|
375
|
+
*
|
|
376
|
+
* ๋ณด์กด ๊ธฐ๊ฐ์ ํ
๋ํธ์ ์ ์ฑ
์ด๋ค: ํ ๊ณ ๊ฐ์ 30์ผ์ด ํ์ํ๊ณ ๋ค๋ฅธ ๊ณ ๊ฐ์ 7์ผ์ด๋ฉด ๋๋ค. ๊ทธ๋ฐ๋ฐ ์์ง์ด
|
|
377
|
+
* `Setting` ์ ์ฝ๊ฒ ๋ง๋ค๋ฉด **์์ง์ด ํ๋ฉด ๊ด์ฌ์ฌ๋ฅผ ์๊ฒ ๋๋ค** โ ๋ฐฉํฅ์ด ๊ฑฐ๊พธ๋ก๋ค(์์ง์ ์ปค๋์ ํธ์คํธ์ด๊ณ
|
|
378
|
+
* ๊ทธ ์์ ์ด๋ค ํ๋ฉด์ด ์๋์ง ๋ชฐ๋ผ์ผ ํ๋ค).
|
|
379
|
+
*
|
|
380
|
+
* ๊ทธ๋์ ์์์ ๋๋ค. ์ฑ์ด ์ด ํจ์๋ฅผ ์ฌ๊ณ , ๊ทธ ์์์ `Setting` ์ด๋ ๋ค๋ฅธ ๋ฌด์์ด๋ ์ฝ๋๋ค. ์ฌ์ง ์์ผ๋ฉด
|
|
381
|
+
* `JOURNAL_RETENTION_DAYS`(์ค์ ํ์ผ์์ ์จ ํ๋ก์ธ์ค ๊ธฐ๋ณธ๊ฐ)๊ฐ ๋ตํ๋ค.
|
|
382
|
+
*
|
|
383
|
+
* `undefined` ๋ฅผ ๋๋ ค์ฃผ๋ฉด **๊ทธ ๋๋ฉ์ธ์ ์ง์ฐ์ง ์๋๋ค** โ ใ๋ชจ๋ฅธ๋คใ๋ฅผ ใ์ง์๋ ๋๋คใ๋ก ์ฝ์ง ์๋๋ค.
|
|
384
|
+
*/
|
|
385
|
+
static retentionDaysOf?: (domainId: string) => Promise<number | undefined>
|
|
386
|
+
static RETENTION_SWEEP_MS = 10 * 60 * 1000 // 10๋ถ๋ง๋ค ํ ๋ฒ โ ์ง์ฐ๋ ์ผ์ ๊ธํ์ง ์๋ค
|
|
387
|
+
private static retentionTimer?: any
|
|
211
388
|
private static checkpointTimer?: any
|
|
212
389
|
|
|
213
390
|
/** ์ต์ ์ค๋
์ท์ cache-service ์ ์ฒดํฌํฌ์ธํธ(๋๋ฉ์ธ+instanceId ํค). display-onlyยท๋น์ฐจ๋จยท์ค๋ฅํก์. */
|
|
@@ -221,10 +398,68 @@ export class TwinEngine {
|
|
|
221
398
|
*/
|
|
222
399
|
const state = unwrapState(this.snapshot(domainId, instanceId))
|
|
223
400
|
if (!state) return
|
|
401
|
+
/*
|
|
402
|
+
* โโ **์๋ฌด๊ฒ๋ ๋ฃ์ง ๋ชปํ ๊ฒ์ ใ๋น์๋คใ๋ก ์ ์ง ์๋๋ค** (2026-08-24 ์ค์ธก) โโโโ
|
|
403
|
+
*
|
|
404
|
+
* ๋ฏธ๋ฌ๊ฐ ์ฌ๊ธฐ๋ํ๋ฉด ๊ด์ธก ์ถ(์ฌ๊ณ ยท์ค๋ยท์์
)์ ๋ค๊ณ ์ค์ง ์๋๋ค(`startLive` ๊ฐ ๋์ฐพ์ ์ํ๋ฅผ
|
|
405
|
+
* ๋ฒ๋ฆฐ๋ค โ ใ๋ค์ ๊ณ์ธก์ด ์ ์ ํ๋คใ๋ ์ ์ ). ๊ทธ๋ฐ๋ฐ ์๋ณธ์ด **์ปค์ ์ฆ๋ถ**์ผ๋ก ๋งํ๋ ํ์ฅ์์๋ ๊ทธ
|
|
406
|
+
* ์ ์ ์ด ์ค์ง ์๋๋ค: ์ปค์๊ฐ ๋ฐ๋ผ์กํ ๋ค ์๋ณธ์ด ๋ณํ์ง ์์ผ๋ฉด ๋ฏธ๋ฌ๋ ์๊ตฌํ ๋น ์ฑ๋ค.
|
|
407
|
+
*
|
|
408
|
+
* ๊ทธ ์ํ์์ ์ด ํจ์๊ฐ ๋๋ฉด **๋น ์ํ๋ฅผ ์ข์ ์ค๋
์ท ์์ ๋ฎ๋๋ค.** ๊ทธ๋์ ์์ค์ด ์๊ตฌํ๋๋ค:
|
|
409
|
+
*
|
|
410
|
+
* ์ ์ฅ๋ผ ์๋ ๊ฒ rev 221,884 ยท nowTime 2026-04-15 ยท items 741 ยท orders 2,780 ยท tasks 6,353
|
|
411
|
+
* ๋ฎ์ผ๋ ค๋ ๊ฒ ๊ฐ์ ํค ยท nowTime 2026-01-01 ยท ์ ๋ถ 0
|
|
412
|
+
*
|
|
413
|
+
* ๋ฆฌ๋น์ ์ผ๋ก๋ ๋ง์ ์ ์๋ค โ ์ ๋ ์ค ๋ฒํธ๋ ๊ด์ธก์ด ์์ด๋ ๊ณ์ ์๋๋ค. ๋ง๋ ๊ธฐ์ค์ **ใ๋ค์ ๊ฒ์ด
|
|
414
|
+
* ์๋ใ**๋ค. ์ ์
์ด ํ ๊ฑด๋ ์์๋ค๋ฉด ์ด ๋น ์ํ๋ **์๋ณธ์ด ใ๋น์๋คใ๊ณ ๋งํ ๊ฒ์ด ์๋๋ผ ์ฐ๋ฆฌ๊ฐ
|
|
415
|
+
* ์๋ฌด๊ฒ๋ ๋ชป ๋ค์ ๊ฒ**์ด๊ณ , ๊ทธ ๋์ ๊ฐ์ ๊ฐ์ผ๋ก ์ ์ผ๋ฉด ใ๋ชจ๋ฆใ์ด ใ์์ใ์ด ๋๋ค.
|
|
416
|
+
*
|
|
417
|
+
* ์๋ณธ์ด ์ค์ ๋ก ใ๋ค ๋น์๋คใ๊ณ ๋งํ ๊ฒฝ์ฐ๋ ๋ง์ง ์๋๋ค โ ๊ทธ๋๋ ์ ์
์ด ์์์ผ๋ฏ๋ก ์ด ๋ฌธ์ ์ง๋๋ค.
|
|
418
|
+
*
|
|
419
|
+
* ๊ทธ๋ฆฌ๊ณ **๊ฑฐ์ ์ ๋งํ๋ค**: ์กฐ์ฉํ ๊ฑฐ์ ํ๋ฉด ์ ์ฒดํฌํฌ์ธํธ๊ฐ ๋ก์ ๊ฐ๋์ง ์๋ฌด๋ ๋ชจ๋ฅธ๋ค.
|
|
420
|
+
*/
|
|
421
|
+
if (!(inst.metrics?.ingestedTotal > 0)) {
|
|
422
|
+
const observed = (s: any) => (s?.items?.length ?? 0) + (s?.orders?.length ?? 0) + (s?.tasks?.length ?? 0)
|
|
423
|
+
if (observed(state) === 0) {
|
|
424
|
+
const prev = await this.loadSnapshot(domainId, instanceId).catch(() => null)
|
|
425
|
+
const had = observed(prev?.state)
|
|
426
|
+
if (had > 0) {
|
|
427
|
+
twinWarn(
|
|
428
|
+
`[twin-engine] "${instanceId}": checkpoint refused โ this twin has heard nothing since start and its live ` +
|
|
429
|
+
`state is empty, while the stored snapshot holds ${had} observed fact(s) (revision ${prev?.revision}). ` +
|
|
430
|
+
'Writing the empty state would destroy the only recoverable copy. The journal still holds the truth.'
|
|
431
|
+
)
|
|
432
|
+
return
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
224
436
|
const revision = inst.revision ?? state.revision ?? 0
|
|
225
437
|
/* ๊ตฌ์กฐ ๋ฆฌ๋น์ ๋ ํจ๊ป โ ์ฝ๋ ์ชฝ์ด "์ด ์ํ๊ฐ ์ง๊ธ์ ๊ณต์ฅ์ธ๊ฐ" ๋ฅผ ๊ฐ๋ฆด ์ ์์ด์ผ ํ๋ค. */
|
|
226
438
|
const { structureRev } = await this.tipOf(domainId, instanceId).catch(() => ({ structureRev: null }) as any)
|
|
227
|
-
|
|
439
|
+
/*
|
|
440
|
+
* โโ **์ด์ด ์ ์ ์จ์์ ํจ๊ป ์ ๋๋ค** (2026-08-24) โโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
441
|
+
*
|
|
442
|
+
* ์ด ํจ์๋ ์ค๋ซ๋์ `{revision, state, structureRev}` ๋ง ์ ์๋ค. ๊ทธ๋์ **์ฌ๊ฐ์ ์ ์ฝ๋ ์ชฝ์ ๋ค
|
|
443
|
+
* ์๋๋ฐ ์ฐ๋ ์ชฝ์ด ์์๋ค**: ์กฐํ ๊ฒฝ๋ก(`recover`)๋ `fold` ๊ฐ ์์ผ๋ฉด ๊ผฌ๋ฆฌ๋ง ์ ๊ณ , ์์ผ๋ฉด ์ ๋์
|
|
444
|
+
* 0๋ถํฐ ์ ๋๋ค. ์ค์ธก์ผ๋ก ์ ์ฅ๋ ์ค๋
์ท 34๊ฑด ์ ๋ถ `fold` ๊ฐ ๋น์ด ์์๊ณ , ์ ๋์ 2,960๋ง ์ค์ด์๋ค โ
|
|
445
|
+
* ๊ทธ๋์ ์ฌ๊ธฐ๋ยท์กฐํ๋ง๋ค ์ฒ์๋ถํฐ ๋ค์ ์ ์๋ค. ๊ท๋ชจ ๊ธฐ์ค(์ํฐํฐ 10๋งยทํ๋ชฉ 100๋ง)์์ ์ด๊ฒ์
|
|
446
|
+
* ๋๋ฆฐ ๊ฒ์ด ์๋๋ผ **๋ชป ํ๋ ๊ฒ**์ด๋ค.
|
|
447
|
+
*
|
|
448
|
+
* ์จ์์ ์ํ๊ฐ ๋์ ํ ์ ์๋ค: ๋ฆฌ๋์๋ ์๋น์ฒ๊ฐ ๋ณด๋ ๊ฐ ๋ง๊ณ ๋ ๋ ๋ค(๋ถ๋ชจ๋ฅผ ๊ธฐ๋ค๋ฆฌ๋ ๋ด๊นยท์ง๊ณ
|
|
449
|
+
* ์ค์ธ ์๋ยท๋ด์ ์ค ๋ชฐ๋ผ ์ธ์ด ๋ ์ฌ๊ฑด). ์ํ๋ง ๋๋๋ฆฌ๊ณ ๋ค๋ฅผ ์ ์ผ๋ฉด 0๋ถํฐ ์ ์ ๊ฒฐ๊ณผ์ **์กฐ์ฉํ**
|
|
450
|
+
* ๋ฌ๋ผ์ง๋ค. ๊ทธ ๋์น๋ ์ปค๋ ์ํ์ด ์ฆ๋ช
ํ๋ค(`observed-checkpoint.test.ts`).
|
|
451
|
+
*
|
|
452
|
+
* ๊ด์ธก ๊ตฌ๋์ด ์๋๋ฉด ์จ์์ด ์๋ค โ ์๋ฎฌ์ ๋ฆฌ๋์๋ฅผ ๊ฐ์ง ์๊ณ , ๊ทธ ์ํ์ ๊ถ์๋ ์ปค๋ ์์ ์ด๋ค.
|
|
453
|
+
* ๊ทธ๋๋ `fold` ๋ฅผ **๋ฃ์ง ์๋๋ค**(๋น ์จ์์ ๋ฃ์ผ๋ฉด ์ฝ๋ ์ชฝ์ด ใ์ด์ด ์ ์ ์ ์๋คใ๊ณ ์๋ชป ๋ณธ๋ค).
|
|
454
|
+
*/
|
|
455
|
+
const reducer: ReducerCheckpoint | undefined = inst.kernel?.observedCheckpoint?.()
|
|
456
|
+
const fold = reducer && inst.oee ? { reducer, oee: inst.oee.serialize() } : undefined
|
|
457
|
+
await cacheService.setInCache(
|
|
458
|
+
this.SNAPSHOT_CACHE_ID,
|
|
459
|
+
{ domainId, instanceId },
|
|
460
|
+
{ revision, state, structureRev, ...(fold ? { fold } : {}) },
|
|
461
|
+
this.SNAPSHOT_TTL_S
|
|
462
|
+
)
|
|
228
463
|
}
|
|
229
464
|
|
|
230
465
|
/**
|
|
@@ -235,6 +470,64 @@ export class TwinEngine {
|
|
|
235
470
|
*/
|
|
236
471
|
private static readonly FOLD_NOTE = 'reducer + oee checkpoint โ the seed for folding only the tail'
|
|
237
472
|
|
|
473
|
+
/**
|
|
474
|
+
* ์ฌ๊ธฐ๋์ ์ธ **์์คํํธ ์จ์**์ ๋ง๋ ๋ค โ ์ํ + ์ด์ด ์ ์ ์ฌ๊ฐ์ .
|
|
475
|
+
*
|
|
476
|
+
* โโ ์ ์ด ์๋ฆฌ๊ฐ ์๊ฒผ๋ (2026-08-24) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
477
|
+
* ๋ ํธ์ถ๋ถ๊ฐ ๊ฐ์ ์ผ์ ์กฐ๊ธ์ฉ ๋ค๋ฅด๊ฒ ํ๊ณ ์์๊ณ (๊ฒนํฌ์ฅ์ ํ์ชฝ๋ง ๋ฒ๊ฒผ๋ค), ๋ ๋ค **์ฌ๊ฐ์ ์ ๋ฒ๋ฆฌ๊ณ **
|
|
478
|
+
* ์ํ๋ง ๋ค๊ณ ๊ฐ๋ค. ๊ทธ๋์ ์ ์ฅ๋ ์ฌ๊ฐ์ ์ ์ฝ๋ ์ชฝ์ด ๋ค ์๋๋ฐ๋ ์ฌ๊ธฐ๋์ ๋งค๋ฒ ์ ๋์ ์ฒ์๋ถํฐ
|
|
479
|
+
* ์ ์๋ค(์ค์ธก: ์ ๋ 2,960๋ง ์ค).
|
|
480
|
+
*
|
|
481
|
+
* ์ฌ๊ธฐ์ ํ๋ ์ผ ์
:
|
|
482
|
+
* โ ๊ฒนํฌ์ฅ์ ๋ฒ๊ธด๋ค โ ์ ํ์์ผ๋ก ์ ์ฅ๋ ๊ฐ์ด ํ ๋ฒ์ ๋ฐ๋์ ๋์จ๋ค
|
|
483
|
+
* โก **๊ทธ ๊ณต์ฅ์ด ์์ง ๊ทธ ๊ณต์ฅ์ธ์ง** ์ฌํํ๋ค โ ์๋๋ฉด ์จ์์ ๋ฒ๋ฆฐ๋ค(์๋)
|
|
484
|
+
* โข ์จ์์ด ์ ๋ ๋๋ณด๋ค ์์ ์์ผ๋ฉด **๊ทธ ๊ผฌ๋ฆฌ๋ง ์ ์ด** ๋๊น์ง ๋ฐ์ด ๋๋ค
|
|
485
|
+
*
|
|
486
|
+
* โก๊ฐ ํ์ํ ์ด์ : ์ฌ๊ฐ์ ์ ๊ทธ๋์ ๋ณด๋ ์์์ ๋ง๋ค์ด์ง ๊ฒ์ด๋ค. ๊ทธ ๋ค ๊ตฌ์กฐ๊ฐ ๋ฐ๋์๋ค๋ฉด(์๋ฆฌ๊ฐ
|
|
487
|
+
* ๋น ์ก๋คยท์ค๋น๊ฐ ์ฎ๊ฒจ์ก๋ค) ๋์ธ์ด ๋ฆฌ๋์๋ **์ง๊ธ ์๋ ์๋ฆฌ์ ์ค๋น๋ฅผ ๋ ๋ค** โ ์๋ ๋์ฅ์ค์ด ํ๋ฉด์
|
|
488
|
+
* ๋์ค๊ณ ๊ทธ ์๋ฆฌ์ ํ์ ์ด ๊ณ์ ๋์๊ฐ๋ค. ์ค๋ฅ ์์ด ํ๋ฆฌ๋ฏ๋ก ๋์ ๋์ง ์๋๋ค.
|
|
489
|
+
*
|
|
490
|
+
* โข์ด ํ์ํ ์ด์ : ์ค๋
์ท์ ์ฒดํฌํฌ์ธํธ ์ฃผ๊ธฐ๋ก ์ฐ์ด๋ฏ๋ก ๋ง์ง๋ง ์ฃผ๊ธฐ ์ดํ์ ์ฌ์ค์ ์ ๋์๋ง ์๋ค.
|
|
491
|
+
* ๊ทธ๊ฒ์ ๋นผ๊ณ ๋์ธ์ฐ๋ฉด ๊ทธ๋งํผ์ด ์กฐ์ฉํ ์ฌ๋ผ์ง๋ค โ ใ๋ชจ๋ฆใ์ ใ์์ใ์ผ๋ก ์ ๋ ๊ฒ๊ณผ ๊ฐ์ ๋ถ๋ฅ๋ค.
|
|
492
|
+
* ์ ๋ ๊ตฌ๊ฐ์ **๊ทธ ํ๋ฟ**์ด๊ณ (์ ๋ ์ ์ฒด๊ฐ ์๋๋ค), `recover` ๊ฐ ๊ทธ ์๋ฆฌ์์ ์ ์ฌ๊ฐ์ ์ ๋จ๊ฒจ ์ค๋ค.
|
|
493
|
+
*/
|
|
494
|
+
private static async warmSeedFor(
|
|
495
|
+
domainId: string,
|
|
496
|
+
instanceId: string
|
|
497
|
+
): Promise<{ revision: number; state: any; fold?: { reducer: ReducerCheckpoint; oee: OeeCheckpoint } } | null> {
|
|
498
|
+
let cached = await this.loadSnapshot(domainId, instanceId).catch(() => null)
|
|
499
|
+
if (!cached?.state) return null
|
|
500
|
+
|
|
501
|
+
const seedOf = (c: typeof cached) => (c?.fold?.reducer ? { fold: c.fold } : {})
|
|
502
|
+
if (!cached.fold?.reducer) return { revision: cached.revision, state: unwrapState(cached.state) }
|
|
503
|
+
|
|
504
|
+
const tip = await this.tipOf(domainId, instanceId).catch(() => null)
|
|
505
|
+
if (tip && (cached.structureRev ?? null) !== tip.structureRev) {
|
|
506
|
+
twinLog(
|
|
507
|
+
`[twin-engine] "${instanceId}": the stored fold seed is from structure ${cached.structureRev} but the factory is now ` +
|
|
508
|
+
`at ${tip.structureRev} โ starting without it (the journal is folded from the beginning instead).`
|
|
509
|
+
)
|
|
510
|
+
return { revision: cached.revision, state: unwrapState(cached.state) }
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (tip && (cached.revision ?? 0) < tip.revision) {
|
|
514
|
+
/*
|
|
515
|
+
* ํ์ ์ ๋๋ค โ `recover` ๊ฐ ์ด ์จ์์ผ๋ก **๊ผฌ๋ฆฌ๋ง** ์ ๊ณ , ๋ ์ง์ ์ ์ ์ฌ๊ฐ์ ์ ๋จ๊ธด๋ค.
|
|
516
|
+
* ์์ง ์ด ํธ์์ ๋ฐํ์์ด ์์ผ๋ฏ๋ก `recover` ๋ ๋ฉ๋ชจ๋ฆฌ ๋์ ์ ๋ ๊ฒฝ๋ก๋ฅผ ํ๋ค(๊ทธ๊ฒ์ด ์ฌ๊ธฐ์ ์ ์ ๋ค).
|
|
517
|
+
*/
|
|
518
|
+
const gap = tip.revision - (cached.revision ?? 0)
|
|
519
|
+
await this.recover(domainId, instanceId).catch(err =>
|
|
520
|
+
twinWarn(`[twin-engine] "${instanceId}": could not fold the ${gap} event(s) after the checkpoint โ ${err?.message ?? err}`)
|
|
521
|
+
)
|
|
522
|
+
const advanced = await this.loadSnapshot(domainId, instanceId).catch(() => null)
|
|
523
|
+
if (advanced?.state && (advanced.revision ?? 0) > (cached.revision ?? 0)) {
|
|
524
|
+
twinLog(`[twin-engine] "${instanceId}": folded ${gap} event(s) after the checkpoint โ revision ${advanced.revision}.`)
|
|
525
|
+
cached = advanced
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
return { revision: cached.revision, state: unwrapState(cached.state), ...seedOf(cached) }
|
|
529
|
+
}
|
|
530
|
+
|
|
238
531
|
/** ์ฒดํฌํฌ์ธํธ๋ ์ต์ ์ค๋
์ท ๋ก๋(์์ผ๋ฉด null). getFromCache ๋ CacheStore ์ํฐํฐ๋ฅผ ๋ฐํ โ ํ์ด๋ก๋๋ .value. */
|
|
239
532
|
static async loadSnapshot(
|
|
240
533
|
domainId: string,
|
|
@@ -262,7 +555,7 @@ export class TwinEngine {
|
|
|
262
555
|
if (!value?.state) return
|
|
263
556
|
await cacheService
|
|
264
557
|
.setInCache(this.SNAPSHOT_CACHE_ID, { domainId, instanceId }, value, this.SNAPSHOT_TTL_S)
|
|
265
|
-
.catch((err: any) =>
|
|
558
|
+
.catch((err: any) => twinError(`[twin-engine] snapshot save fail "${instanceId}"`, err?.message ?? err))
|
|
266
559
|
}
|
|
267
560
|
|
|
268
561
|
|
|
@@ -339,6 +632,113 @@ export class TwinEngine {
|
|
|
339
632
|
return { revision: tip?.revision ?? 0, structureRev: newest?.rev ?? null }
|
|
340
633
|
}
|
|
341
634
|
|
|
635
|
+
/**
|
|
636
|
+
* **์ ๋์ ๋ณด์กด ๊ธฐ๊ฐ๊น์ง๋ง ๋๋ค** โ ์ฒดํฌํฌ์ธํธ๊ฐ ๋์ ํ ์ ์๋ ๋งํผ๋ง ์ง์ด๋ค.
|
|
637
|
+
*
|
|
638
|
+
* ํ ์ธ์คํด์ค์์ ์ง์ฐ๋ ์กฐ๊ฑด์ **๋ ๋ค** ๋ง์กฑํด์ผ ํ๋ค.
|
|
639
|
+
*
|
|
640
|
+
* ยท `createdAt` ์ด ๋ณด์กด ๊ธฐ๊ฐ๋ณด๋ค ์ค๋๋๋ค โ **ํ์ด ์ฐ์ธ ์ค์ ์๊ฐ**์ด ๊ธฐ์ค์ด๋ค
|
|
641
|
+
* ยท `revision` ์ด **์ฒดํฌํฌ์ธํธ ๋ฆฌ๋น์ ์ดํ**๋ค โ ๊ทธ ์์ ์ค๋
์ท์ด ๋์ ํ๋ค
|
|
642
|
+
*
|
|
643
|
+
* โโ ์ `eventTime` ์ด ์๋๋ผ `createdAt` ์ธ๊ฐ (2026-08-22 ์ค์ธก์ผ๋ก ๊ณ ์นจ) โโโโ
|
|
644
|
+
* ์ฒ์์ `eventTime` ์ผ๋ก ์ ์๋ค. ๊ทธ๊ฒ์ **ํธ์์ ์๊ณ**๋ค โ ์๋ฎฌ๋ ์ด์
์ ์๊ธฐ ์๊ณ๋ก ์ฌ๊ฑด์ ์ฐ๊ณ ,
|
|
645
|
+
* ๊ทธ ์๊ณ๋ ์ค์ ์๊ฐ๊ณผ ๋ฌด๊ดํ๊ฒ ์์๊ฑฐ๋ ๋ค์ ๋ค. ์ค์ธก:
|
|
646
|
+
*
|
|
647
|
+
* order-check ๊ฐ์ฅ ๋ฆ์ eventTime = 2027-06-08 โ ์ค์ ์๊ฐ๋ณด๋ค 10๊ฐ์ ์
|
|
648
|
+
* hatio-mx2 2026-01-01 ~ ์ง๊ธ โ 7๊ฐ์์น๋ฅผ ํ ๋ฒ์ ์ง์ฐ๊ฒ ๋๋ค
|
|
649
|
+
*
|
|
650
|
+
* ์ฆ ใ7์ผใ์ด ํธ์๋ง๋ค ๋ค๋ฅธ ๋ป์ด ๋๋ค: ์๊ณ๊ฐ ์์ ํธ์์ ์์ํ ์ง์์ง์ง ์๊ณ , ๊ณผ๊ฑฐ๋ถํฐ ์ฐ์ ํธ์์
|
|
651
|
+
* ๊ฑฐ์ ์ ๋ถ๊ฐ ํ ๋ฒ์ ์ง์์ง๋ค. **๋ณด์กด์ ์ ์ฅ ๋์ด์ ๋ฌธ์ **์ด๊ณ ์ ์ฅ ๋์ด๋ ์ค์ ์๊ณ๋ค.
|
|
652
|
+
*
|
|
653
|
+
* `createdAt` ์ ๊ทธ ํ์ด DB ์ ์ฐ์ธ ์๊ฐ์ด๊ณ 1,599๋ง ํ ์ ๋ถ ์ฑ์์ ธ ์๋ค(ํ์ธํจ). ๋๋ฉ์ธ ์๊ฐ์
|
|
654
|
+
* ์ ์ฑ
์ ์ฐ์ง ์๋๋ค โ ๊ทธ ๋์ ์์ผ๋ฉด ์๋ฎฌ๊ณผ ๋ฏธ๋ฌ์์ ๊ฐ์ ์ค์ ์ด ๋ค๋ฅด๊ฒ ๋์ํ๋ค.
|
|
655
|
+
*
|
|
656
|
+
* ์ค๋
์ท์ด ์์ผ๋ฉด **๊ทธ ์ธ์คํด์ค๋ ๊ฑด๋๋ฆฌ์ง ์๋๋ค.** ์ ๋์ด ์ ์ผํ ๋ณต๊ตฌ ์๋จ์ธ ์ํ์ด๋ฏ๋ก, ์ง์ฐ๋ฉด
|
|
657
|
+
* ๊ทธ ํธ์์ ์ํ๋ฅผ ๋๋๋ฆด ์ ์๋ค. ใ์ง์ธ ์ ์์๋คใ๋ ์ฌ์ค๋ ํจ๊ป ์ผ๋ค โ ์กฐ์ฉํ ๋๊ธฐ๋ฉด ใ๋ณด์กด์ด
|
|
658
|
+
* ๋๋๋ฐ ์ ์ ์ค์ด๋๋ใ๊ฐ ๋๋ค.
|
|
659
|
+
*
|
|
660
|
+
* `createdAt` ์ด ๋น ์ ํ์ **์ง์ฐ์ง ์๋๋ค**(์๊ฐ์ ๋ชจ๋ฅด๋ ๊ฒ์ ใ์ค๋๋๋คใ๋ก ์ฝ์ง ์๋๋ค).
|
|
661
|
+
*
|
|
662
|
+
* ๋๋ผ์ด๋ฒ ๋ค์ฏ์ ๋ค ์ง๋์ผ ํ๋ฏ๋ก raw SQL ์ ์ฐ์ง ์๋๋ค โ ์กฐ๊ฑด ์ญ์ ๋ ์ฟผ๋ฆฌ๋น๋๊ฐ ์ด์ํ๋ค.
|
|
663
|
+
*/
|
|
664
|
+
static async pruneJournal(domainId: string): Promise<{ deleted: number; instances: number; skipped: string[] }> {
|
|
665
|
+
/* ๋๋ฉ์ธ์ ์ ์ฑ
์ด ๋จผ์ ๋ค(ยง`retentionDaysOf`). ์์ผ๋ฉด ํ๋ก์ธ์ค ๊ธฐ๋ณธ๊ฐ. ๋ ๋ค ์์ผ๋ฉด ์ง์ฐ์ง ์๋๋ค. */
|
|
666
|
+
const perDomain = this.retentionDaysOf ? await this.retentionDaysOf(domainId).catch(() => undefined) : undefined
|
|
667
|
+
const days = perDomain ?? this.JOURNAL_RETENTION_DAYS
|
|
668
|
+
if (!days || days <= 0) return { deleted: 0, instances: 0, skipped: [] }
|
|
669
|
+
|
|
670
|
+
/* ๋ณด์กด ๊ธฐ๊ฐ์ ์ค๋
์ท TTL ๋ณด๋ค ์งง์ ์ ์๋ค โ ์งง์ผ๋ฉด ์ค๋
์ท์ด ๊ฐ๋ฆฌํค๋ ์์ชฝ์ด ๋น๋ ๊ตฌ๊ฐ์ด ์๊ธด๋ค. */
|
|
671
|
+
const minDays = this.SNAPSHOT_TTL_S / 86400
|
|
672
|
+
const effectiveDays = Math.max(days, minDays)
|
|
673
|
+
if (effectiveDays !== days) {
|
|
674
|
+
twinWarn(
|
|
675
|
+
`[twin-engine] journal retention ${days}์ผ์ ์ค๋
์ท TTL(${minDays}์ผ)๋ณด๋ค ์งง๋ค โ ${effectiveDays}์ผ๋ก ์ฌ๋ฆฐ๋ค ` +
|
|
676
|
+
'(๋ ์งง์ผ๋ฉด ์ค๋
์ท์ด ์ด์ ์๋๋ฐ ๊ทธ๊ฒ์ด ๊ฐ๋ฆฌํค๋ ์์ชฝ ์ ๋์ด ์๋ ๊ตฌ๊ฐ์ด ์๊ธด๋ค)'
|
|
677
|
+
)
|
|
678
|
+
}
|
|
679
|
+
const cutoff = new Date(Date.now() - effectiveDays * 86400 * 1000)
|
|
680
|
+
|
|
681
|
+
const rows = await getRepository(TwinInstance).find({ where: { domain: { id: domainId } } })
|
|
682
|
+
let deleted = 0
|
|
683
|
+
let instances = 0
|
|
684
|
+
const skipped: string[] = []
|
|
685
|
+
|
|
686
|
+
for (const r of rows) {
|
|
687
|
+
const snap = await this.loadSnapshot(domainId, r.instanceId).catch(() => null)
|
|
688
|
+
const upTo = Number(snap?.revision ?? 0)
|
|
689
|
+
if (!upTo) {
|
|
690
|
+
/* ์ค๋
์ท์ด ์๋ค โ ์ ๋์ด ์ ์ผํ ๋ณต๊ตฌ ์๋จ์ด๋ฏ๋ก ์๋์ง ์๋๋ค. */
|
|
691
|
+
skipped.push(r.instanceId)
|
|
692
|
+
continue
|
|
693
|
+
}
|
|
694
|
+
const res = await getRepository(TwinEvent)
|
|
695
|
+
.createQueryBuilder()
|
|
696
|
+
.delete()
|
|
697
|
+
.from(TwinEvent)
|
|
698
|
+
.where('domain_id = :domainId', { domainId })
|
|
699
|
+
.andWhere('instance_id = :instanceId', { instanceId: r.instanceId })
|
|
700
|
+
.andWhere('revision <= :upTo', { upTo })
|
|
701
|
+
.andWhere('created_at IS NOT NULL')
|
|
702
|
+
.andWhere('created_at < :cutoff', { cutoff })
|
|
703
|
+
.execute()
|
|
704
|
+
const n = res.affected ?? 0
|
|
705
|
+
if (n > 0) {
|
|
706
|
+
deleted += n
|
|
707
|
+
instances++
|
|
708
|
+
/* **์ง์ด ๊ฒ์ ๋งํ๋ค** โ ์กฐ์ฉํ ์ค์ด๋ ์ ๋์ ใ์์๋ ์ผใ๊ณผ ๊ตฌ๋ณ๋์ง ์๋๋ค. */
|
|
709
|
+
twinLog(
|
|
710
|
+
`[twin-engine] journal pruned "${r.instanceId}" โ ${n}๊ฑด (revision โค ${upTo} ยท ${cutoff.toISOString()} ์ด์ ). ` +
|
|
711
|
+
'๊ทธ ์์ ์ฒดํฌํฌ์ธํธ๊ฐ ๋์ ํ๋ค.'
|
|
712
|
+
)
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
if (skipped.length) {
|
|
716
|
+
twinLog(
|
|
717
|
+
`[twin-engine] journal prune skipped ${skipped.length} instance(s) with no checkpoint โ ` +
|
|
718
|
+
`์ ๋์ด ์ ์ผํ ๋ณต๊ตฌ ์๋จ์ด๋ผ ์๋์ง ์์๋ค: ${skipped.slice(0, 5).join(', ')}${skipped.length > 5 ? ' โฆ' : ''}`
|
|
719
|
+
)
|
|
720
|
+
}
|
|
721
|
+
return { deleted, instances, skipped }
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/** ๋ณด์กด ์ ๋ฆฌ ์ฃผ๊ธฐ ๊ธฐ๋(1ํ) โ ์ ์ธ์ด ์์ผ๋ฉด ์๋ฌด๊ฒ๋ ํ์ง ์๋๋ค. */
|
|
725
|
+
static startRetentionLoop(domainId: string): void {
|
|
726
|
+
if (this.retentionTimer) return
|
|
727
|
+
/*
|
|
728
|
+
* ๊ฑธ์ง ์๋ ์กฐ๊ฑด: ํ๋ก์ธ์ค ๊ธฐ๋ณธ๊ฐ๋ ์๊ณ **๋๋ฉ์ธ์ ๋ฌผ์ ๊ธธ๋ ์์** ๋๋ค. ์์์ด ์ฌ๊ฒจ ์์ผ๋ฉด
|
|
729
|
+
* ๊ธฐ๋ณธ๊ฐ์ด ์์ด๋ ๊ฑธ์ด์ผ ํ๋ค โ ๊ทธ ๋๋ฉ์ธ์ด ์๊ธฐ ๊ฐ์ ๊ฐ์ง ์ ์๋ค.
|
|
730
|
+
*/
|
|
731
|
+
if (!this.JOURNAL_RETENTION_DAYS && !this.retentionDaysOf) return
|
|
732
|
+
this.retentionTimer = setInterval(() => {
|
|
733
|
+
this.pruneJournal(domainId).catch(err => twinError('[twin-engine] journal prune failed', err?.message ?? err))
|
|
734
|
+
}, this.RETENTION_SWEEP_MS)
|
|
735
|
+
if (typeof this.retentionTimer?.unref === 'function') this.retentionTimer.unref()
|
|
736
|
+
twinLog(
|
|
737
|
+
`[twin-engine] journal retention on โ ${this.JOURNAL_RETENTION_DAYS ?? '(๋๋ฉ์ธ ์ ์ฑ
)'}์ผ ยท ` +
|
|
738
|
+
`${this.RETENTION_SWEEP_MS / 60000}๋ถ๋ง๋ค`
|
|
739
|
+
)
|
|
740
|
+
}
|
|
741
|
+
|
|
342
742
|
/** ์ฒดํฌํฌ์ธํธ ๋ฃจํ ๊ธฐ๋(1ํ) โ ๋ผ์ด๋ธ ์ธ์คํด์ค๋ค์ ์ต์ ์ค๋
์ท์ ์ฃผ๊ธฐ ์์. */
|
|
343
743
|
static startCheckpointLoop(): void {
|
|
344
744
|
if (this.checkpointTimer) return
|
|
@@ -346,7 +746,7 @@ export class TwinEngine {
|
|
|
346
746
|
for (const [key, inst] of Object.entries(this.instances)) {
|
|
347
747
|
const { instanceId } = parseRuntimeKey(key)
|
|
348
748
|
this.persistSnapshot(inst.domainId, instanceId).catch(err =>
|
|
349
|
-
|
|
749
|
+
twinError(`[twin-engine] snapshot checkpoint fail "${instanceId}"`, err?.message ?? err)
|
|
350
750
|
)
|
|
351
751
|
}
|
|
352
752
|
}, this.CHECKPOINT_MS)
|
|
@@ -358,6 +758,8 @@ export class TwinEngine {
|
|
|
358
758
|
* ๋ผ์ด๋ธ tick ์ฌ๊ฐ(sim ๊ฒฐ์ ์ re-run / live ์ธ์ ์คํธ ์ง์)๋ ํ์. ์ฌ๊ธฐ์ ์ํ ๋ณต์ + ๋
ธ์ถ.
|
|
359
759
|
*/
|
|
360
760
|
static async bootstrap(): Promise<void> {
|
|
761
|
+
/* ๋ถํ
๋ถํฐ ์ฐ๋ค โ ์ฌ๋์ด ๊ฐ์ฅ ๋ต๋ตํ ๊ตฌ๊ฐ์ด ์ฌ๊ธฐ์ด๊ณ , ๊ทธ ๊ตฌ๊ฐ์ ์ฌ์ง ์์ผ๋ฉด ๊ฐ์ผ๋ก ๋งํ ์ ์๋ค. */
|
|
762
|
+
loopLag.start()
|
|
361
763
|
try {
|
|
362
764
|
const repo = getRepository(TwinInstance)
|
|
363
765
|
const rows = await repo.find({ where: { status: 'running' } })
|
|
@@ -365,10 +767,9 @@ export class TwinEngine {
|
|
|
365
767
|
if (!row.domainId || !row.instanceId) continue
|
|
366
768
|
// ์์คํํธ: ์บ์๋ ์ต์ ์ค๋
์ท ์ฐ์ (O(1) + attentions/OEE ๋ฑ ๋ผ์ด๋ธ ํ์์ํ ๋ณด์กด).
|
|
367
769
|
// ์์ผ๋ฉด ์ ๋ fold-from-0 replay(์ง์ค ํด๋ฐฑ โ replay ๋ ๋ผ์ด๋ธ ํ์์ํ๋ฅผ ๋ชป ๋ด์ผ๋ฏ๋ก ์บ์๊ฐ ๋ ์ถฉ์ค).
|
|
368
|
-
const cached = await this.
|
|
770
|
+
const cached = await this.warmSeedFor(row.domainId, row.instanceId).catch(() => null)
|
|
369
771
|
if (cached?.state) {
|
|
370
|
-
|
|
371
|
-
this.recovered[runtimeKey(row.domainId, row.instanceId)] = { revision: cached.revision, state: unwrapState(cached.state) }
|
|
772
|
+
this.recovered[runtimeKey(row.domainId, row.instanceId)] = cached
|
|
372
773
|
/*
|
|
373
774
|
* **ใ์์คํํธํ๋คใ๊ณ ๋งํ์ง ์๋๋ค** โ ์ฌ๊ธฐ์๋ ์ํ๋ฅผ **์ฐพ์ ๋ ๊ฒ**๋ฟ์ด๋ค.
|
|
374
775
|
*
|
|
@@ -377,20 +778,38 @@ export class TwinEngine {
|
|
|
377
778
|
* ๋ก๊ทธ๋ง ์ฝ์ผ๋ฉด ์ฌ๊ธด ์ค ์๊ฒ ๋๋ค โ ์ค์ ๋ก ๊ทธ๋ ๊ฒ ์ฝ๊ณ ์ฌ๊ธฐ๋ ๋ค ์ง์์๊ฐ์ด ์ฌ๋ผ์ง ๊ฒ์
|
|
378
779
|
* ๋ฐ์ดํฐ ๋ฌธ์ ๋ก ์ค์งํ ๋ปํ๋ค.
|
|
379
780
|
*/
|
|
380
|
-
|
|
781
|
+
twinLog(`[twin-engine] found cached state for "${row.instanceId}" โ revision ${cached.revision} (seeded at start).`)
|
|
381
782
|
continue
|
|
382
783
|
}
|
|
383
784
|
const state = await this.recover(row.domainId, row.instanceId).catch(() => null)
|
|
384
785
|
if (state) {
|
|
385
786
|
this.recovered[runtimeKey(row.domainId, row.instanceId)] = { revision: state.revision, state }
|
|
386
|
-
|
|
787
|
+
twinLog(`[twin-engine] recovered "${row.instanceId}" from journal โ revision ${state.revision}.`)
|
|
387
788
|
}
|
|
388
789
|
}
|
|
389
|
-
/*
|
|
390
|
-
|
|
790
|
+
/*
|
|
791
|
+
* ์ํ๋ง ๋์ฐพ๋ ๊ฒ์ผ๋ก๋ **๋๋ ํธ์์ด ๋์ง ์๋๋ค** โ ๋ฐํ์๊น์ง ๋์ด๋ฆฐ๋ค(์๋).
|
|
792
|
+
*
|
|
793
|
+
* ๋์ด๋ฆฌ๊ธฐ ์ฌ์ด์ **๋ฃจํ๋ฅผ ํ ๋ฒ ๋น์ ์ค๋ค** (2026-08-21). ์์คํํธ ํ๋๊ฐ ๋ฌผํ ์์ฒ ๊ฐ๋ฅผ ์ ์ผ๋ฏ๋ก
|
|
794
|
+
* ์ค๋ฌด ๊ฐ๋ฅผ ์ฐ๋ฌ์ ํ๋ฉด ๊ทธ ์๊ฐ ๋ด๋ด HTTP ๊ฐ ์์ง ์๋๋ค โ ์๋ฒ๋ ์ด๋ฏธ `Server ready` ๋ฅผ ์ฐ์
|
|
795
|
+
* ๋ค๋ผ์, ์ฌ๋์๊ฒ๋ ใ์ด๋ ธ๋๋ฐ ์๋ฌด ๋ฐ์์ด ์๋ ํ๋ฉดใ์ผ๋ก ๋ณด์ธ๋ค. ์ด ์๊ฐ์ ๊ฐ๊ณ , ๊ทธ ์ฌ์ด์
|
|
796
|
+
* ๋์ฐฉํ ์์ฒญ์ด ์ฒ๋ฆฌ๋ ํ๋ง ์๊ธด๋ค.
|
|
797
|
+
*/
|
|
798
|
+
for (const row of rows) {
|
|
799
|
+
await this.resumeRow(row)
|
|
800
|
+
await new Promise(resolve => setTimeout(resolve, this.BOOT_YIELD_MS))
|
|
801
|
+
}
|
|
391
802
|
this.startCheckpointLoop() // ์ดํ ๊ธฐ๋๋๋ ๋ผ์ด๋ธ ์ธ์คํด์ค์ ์ต์ ์ค๋
์ท์ ์ฃผ๊ธฐ ์์
|
|
803
|
+
/*
|
|
804
|
+
* ๋ณด์กด ์ ๋ฆฌ โ **์ ์ธ์ด ์์ ๋๋ง** ๋๋ค(ยง`JOURNAL_RETENTION_DAYS`). ๋๋ฉ์ธ๋ง๋ค ํ ๋ฒ ๊ฑด๋ค.
|
|
805
|
+
* ์ฒดํฌํฌ์ธํธ ๋ฃจํ ๋ค์ ๋๋ ์ด์ : ์ง์ธ ์ ์๋ ๊ฒฝ๊ณ๊ฐ ์ค๋
์ท์ด๋ฏ๋ก, ์ค๋
์ท์ ๋จ๊ธฐ๋ ์ชฝ์ด ๋จผ์
|
|
806
|
+
* ๋์์ผ ์ฒซ ์ ๋ฆฌ๊ฐ ์ค์ ๋ก ์ง์ธ ๊ฒ์ ๊ฐ๋๋ค.
|
|
807
|
+
*/
|
|
808
|
+
for (const domainId of new Set(rows.map(r => r.domainId).filter(Boolean) as string[])) {
|
|
809
|
+
this.startRetentionLoop(domainId)
|
|
810
|
+
}
|
|
392
811
|
} catch (err) {
|
|
393
|
-
|
|
812
|
+
twinError('[twin-engine] recovery scan failed', err)
|
|
394
813
|
}
|
|
395
814
|
}
|
|
396
815
|
|
|
@@ -427,16 +846,20 @@ export class TwinEngine {
|
|
|
427
846
|
}
|
|
428
847
|
|
|
429
848
|
try {
|
|
849
|
+
/*
|
|
850
|
+
* **์ ์ธ๋ ๋ชจ๋๋ก ๋์ด๋ฆฌ๋ ํ๋จ์ ํ ๊ณณ์ ์๋ค**(`startFromRegistry`) โ 2026-08-20.
|
|
851
|
+
*
|
|
852
|
+
* ์์ ์๋ ์ด ์๋ฆฌ์์ `resync` ๋ฅผ ๊ฐ๋ผ `startLive` ๋ฅผ ๋ถ๋ ๋ค. ๊ทธ๋์ ๋ถํ
์ผ๋ก ์ด์๋ ๋ฏธ๋ฌ๋
|
|
853
|
+
* ๊ด์ธก ๊ตฌ๋์ด์์ง๋ง **์ฌ๋์ด ํ๋ฉด์์ ์์ํ ๋ฏธ๋ฌ๋ ์๋ฎฌ๋ก ๋ด๋ค**(๊ทธ ๋ฌธ์๋ ๊ฐ๋ฆผ์ด ์์๋ค).
|
|
854
|
+
* ๊ฐ์ ์ ์ธ์ด ๋ ๊ฒฐ๊ณผ๋ฅผ ๋ด์ง ์๋๋ก ๊ฐ๋ฆผ์ ๋ฌธ ์์ผ๋ก ์ฎ๊ฒผ๋ค.
|
|
855
|
+
*/
|
|
856
|
+
await this.startFromRegistry(domainId, instanceId)
|
|
430
857
|
if (row.restartPolicy === 'resync') {
|
|
431
|
-
if (!row.model) throw new Error('no model')
|
|
432
|
-
/* ์๊ฐ ๊ธฐ์ค์ **๊ณต๊ฐ**์ด ๊ฐ๋๋ค โ ๊ต๋์ HH:MM ์ ์ด๋ ๊ธฐ์ค์ผ๋ก ์ฝ๋(๋ผ์ด๋ธ ๊ธฐ๋๊ณผ ๊ฐ์ ๊ท์น). */
|
|
433
|
-
this.startLive(instanceId, domainId, row.kind, await this.withSpaceTimeBase(row.model as TwinModelDef, domainId))
|
|
434
858
|
/* ๊ณ์ธก์ ๋๋ฅด๋ ํผ๋๋ ์ปค๋ฅํฐ์ ๊ฒ์ด๋ค โ ๋ ํผ๋ฐ์ค ๊ณ์ธต์ด ๋ถํ
ํ
์์ ๋ค์ ๋ถ์ธ๋ค
|
|
435
859
|
(`resumeReferenceLiveFeeds`). ์ฌ๊ธฐ์ ์ด๋ํฐ๋ฅผ ์๋ ๊ฒ์ ๊ณ์ธต์ ๊ฑฐ๊พธ๋ก ์๋ ๊ฒ์ด๋ค. */
|
|
436
|
-
|
|
860
|
+
twinLog(`[twin-engine] resumed mirror "${instanceId}" โ feed reattach is the reference layer's job.`)
|
|
437
861
|
} else {
|
|
438
|
-
|
|
439
|
-
console.log(`[twin-engine] resumed ${row.restartPolicy} "${instanceId}".`)
|
|
862
|
+
twinLog(`[twin-engine] resumed ${row.restartPolicy} "${instanceId}".`)
|
|
440
863
|
}
|
|
441
864
|
} catch (err: any) {
|
|
442
865
|
await this.markStopped(row, err?.message ?? 'resume failed')
|
|
@@ -445,11 +868,11 @@ export class TwinEngine {
|
|
|
445
868
|
|
|
446
869
|
/** ๋์ด๋ฆฌ์ง ๋ชปํ ํ์ ์ ์งํ๊ฒ ์ ๋๋ค โ ใ๋๋ ์คใ์ด๋ผ ๋งํ๋ ์ฑ๋ก ๋์ง ์๋๋ค. */
|
|
447
870
|
private static async markStopped(row: TwinInstance, why: string): Promise<void> {
|
|
448
|
-
|
|
871
|
+
twinWarn(`[twin-engine] "${row.instanceId}" not resumed (${why}) โ registry says stopped now.`)
|
|
449
872
|
try {
|
|
450
873
|
await getRepository(TwinInstance).update({ id: row.id }, { status: 'stopped' })
|
|
451
874
|
} catch (e: any) {
|
|
452
|
-
|
|
875
|
+
twinError(`[twin-engine] could not mark "${row.instanceId}" stopped โ registry now lies about it.`, e?.message)
|
|
453
876
|
}
|
|
454
877
|
}
|
|
455
878
|
|
|
@@ -487,9 +910,9 @@ export class TwinEngine {
|
|
|
487
910
|
|
|
488
911
|
if (plan.action === 'skip') {
|
|
489
912
|
if (plan.reason === 'bench') {
|
|
490
|
-
|
|
913
|
+
twinLog(`[twin-engine] "${id}" is a bench twin โ starting empty on purpose (seeding would skew the measurement).`)
|
|
491
914
|
} else if (plan.reason === 'unsupported') {
|
|
492
|
-
|
|
915
|
+
twinWarn(
|
|
493
916
|
`[twin-engine] kernel for "${id}" cannot be warm-started (no hydrateObserved) โ it starts with structure only, so held stock will read as zero.`
|
|
494
917
|
)
|
|
495
918
|
}
|
|
@@ -509,10 +932,10 @@ export class TwinEngine {
|
|
|
509
932
|
/* ใ์ธ์ ๋ถํฐ์ธ๊ฐใ๋ ๋งํ๋ค โ ์์ผ๋ฉด ์ง์๋ ์กฐ๊ฑด์ด ๋ชจ๋ ใ๋ฐฉ๊ธใ์ผ๋ก ๋ณด์ธ๋ค. */
|
|
510
933
|
...(plan.attentionSinceCount ? [`${plan.attentionSinceCount} attention start time(s)`] : [])
|
|
511
934
|
].join(', ')
|
|
512
|
-
|
|
935
|
+
twinLog(`[twin-engine] warm-started "${id}" โ restored ${restored}.`)
|
|
513
936
|
/* ๋บ ๊ฒ์ ์กฐ์ฉํ ๋๊ธฐ์ง ์๋๋ค โ ์ง์ด๋ด์ง ์์๋ค๋ ์ฌ์ค ์์ฒด๋ฅผ ๋งํด์ผ ํ๋ฉด์ ๋น์นธ์ด ์ฝํ๋ค. */
|
|
514
937
|
if (plan.ordersWithoutDemand > 0) {
|
|
515
|
-
|
|
938
|
+
twinWarn(
|
|
516
939
|
`[twin-engine] "${id}": ${plan.ordersWithoutDemand} order(s) could not be restored โ they carry progress only, ` +
|
|
517
940
|
'with no requested/fulfilled counts, so the remaining demand is unknown. They are left out rather than guessed.'
|
|
518
941
|
)
|
|
@@ -540,7 +963,7 @@ export class TwinEngine {
|
|
|
540
963
|
const declared = model?.localDurations as Record<string, string> | undefined
|
|
541
964
|
if (declared && Object.keys(declared).length) {
|
|
542
965
|
if (typeof kernel?.declareDurations !== 'function') {
|
|
543
|
-
|
|
966
|
+
twinWarn(
|
|
544
967
|
`[twin-engine] "${id}": this facility declared ${Object.keys(declared).length} operation duration(s) but the kernel cannot consume them ` +
|
|
545
968
|
'(declareDurations missing โ kernel needs publishing). The simulation keeps running on built-in constants, and specCoverage() will keep reporting "default".'
|
|
546
969
|
)
|
|
@@ -549,7 +972,7 @@ export class TwinEngine {
|
|
|
549
972
|
kernel.declareDurations(declared)
|
|
550
973
|
} catch (err: any) {
|
|
551
974
|
/* ์ปค๋์ด ๊ฑฐ์ ํ ๊ฐ์ ์กฐ์ฉํ ๋๊ธฐ์ง ์๋๋ค โ ํ๋ฉด์ ใ๋ฃ์์ต๋๋คใ๋ผ๊ณ ๋งํ ๊ฐ์ด๋ค. */
|
|
552
|
-
|
|
975
|
+
twinWarn(`[twin-engine] "${id}": declared operation duration rejected by the kernel โ ${err?.message ?? err}`)
|
|
553
976
|
}
|
|
554
977
|
}
|
|
555
978
|
}
|
|
@@ -557,7 +980,7 @@ export class TwinEngine {
|
|
|
557
980
|
const params = model?.localParams as Record<string, Record<string, string>> | undefined
|
|
558
981
|
if (params && Object.keys(params).length) {
|
|
559
982
|
if (typeof kernel?.declareParameters !== 'function') {
|
|
560
|
-
|
|
983
|
+
twinWarn(
|
|
561
984
|
`[twin-engine] "${id}": this facility declared operation parameters for ${Object.keys(params).length} operation(s) but the kernel cannot consume them ` +
|
|
562
985
|
'(declareParameters missing โ kernel needs publishing). Yield and setup keep running on built-in constants.'
|
|
563
986
|
)
|
|
@@ -565,14 +988,14 @@ export class TwinEngine {
|
|
|
565
988
|
try {
|
|
566
989
|
kernel.declareParameters(params)
|
|
567
990
|
} catch (err: any) {
|
|
568
|
-
|
|
991
|
+
twinWarn(`[twin-engine] "${id}": declared operation parameter rejected by the kernel โ ${err?.message ?? err}`)
|
|
569
992
|
}
|
|
570
993
|
}
|
|
571
994
|
}
|
|
572
995
|
const ops = model?.operations
|
|
573
996
|
if (!ops?.length) return
|
|
574
997
|
if (typeof kernel?.loadOperations !== 'function') {
|
|
575
|
-
|
|
998
|
+
twinWarn(`[twin-engine] "${id}": model declares ${ops.length} operation spec(s) but the kernel cannot consume them (loadOperations missing โ kernel needs publishing). Simulation will use built-in default durations.`)
|
|
576
999
|
return
|
|
577
1000
|
}
|
|
578
1001
|
kernel.loadOperations(ops)
|
|
@@ -597,7 +1020,7 @@ export class TwinEngine {
|
|
|
597
1020
|
const chained = chainEstimators([measured?.estimator, travel.estimator])
|
|
598
1021
|
if (!chained) {
|
|
599
1022
|
/* ์ ๋ชป ๋ฃ์๋์ง ํ ๋ฒ๋ง ์๋ฆฐ๋ค โ ์ด๋์๊ฐ์ด ์์๋ก ๋จ์ ์ด์ ๋ฅผ ๋ชจ๋ฅด๊ณ ์ง๋๊ฐ์ง ์๊ฒ. */
|
|
600
|
-
if (travel.reasons.length)
|
|
1023
|
+
if (travel.reasons.length) twinWarn(`[twin-engine] "${instanceId}": no duration estimator installed โ ${travel.reasons.join('; ')}`)
|
|
601
1024
|
return
|
|
602
1025
|
}
|
|
603
1026
|
kernel.durationEstimator = chained
|
|
@@ -610,13 +1033,13 @@ export class TwinEngine {
|
|
|
610
1033
|
const yields = await this.measuredYield(domainId, instanceId)
|
|
611
1034
|
if (yields?.estimator) {
|
|
612
1035
|
if (typeof kernel.yieldOf !== 'function') {
|
|
613
|
-
|
|
1036
|
+
twinWarn(
|
|
614
1037
|
`[twin-engine] "${instanceId}": learned yield for ${Object.keys(yields.learned).length} operation kind(s) but the kernel cannot consume it ` +
|
|
615
1038
|
'(yieldOf missing โ kernel needs publishing). Yield keeps running on the declared value or the built-in constant.'
|
|
616
1039
|
)
|
|
617
1040
|
} else {
|
|
618
1041
|
kernel.yieldEstimator = yields.estimator
|
|
619
|
-
|
|
1042
|
+
twinLog(
|
|
620
1043
|
`[twin-engine] "${instanceId}": measured yield installed โ ${Object.entries(yields.learned)
|
|
621
1044
|
.map(([k, v]) => `${k}=${Math.round(v * 1000) / 10}%(${yields.samples[k].good + yields.samples[k].scrap}๊ฑด)`)
|
|
622
1045
|
.join(', ')}${yields.skipped.length ? ` ยท ํ๋ณธ ๋ถ์กฑ์ผ๋ก ๋บ ์ข
๋ฅ: ${yields.skipped.map(s => `${s.kind}(${s.judged})`).join(', ')}` : ''}`
|
|
@@ -624,7 +1047,7 @@ export class TwinEngine {
|
|
|
624
1047
|
}
|
|
625
1048
|
} else if (yields?.skipped.length) {
|
|
626
1049
|
/* ๋ฐฐ์ด ๊ฒ์ด ์๊ณ ๋ฒ๋ฆฐ ๊ฒ๋ง ์์ผ๋ฉด ๊ทธ ์ฌ์ค๋ ๋งํ๋ค โ ใ์ด๋ ฅ์ด ์๋คใ์ ใํ๋ณธ์ด ๋ชจ์๋ผ๋คใ๋ ๋ค๋ฅด๋ค. */
|
|
627
|
-
|
|
1050
|
+
twinLog(
|
|
628
1051
|
`[twin-engine] "${instanceId}": no measured yield yet โ samples below ${yields.minSamples}: ${yields.skipped
|
|
629
1052
|
.map(s => `${s.kind}(${s.judged})`)
|
|
630
1053
|
.join(', ')}`
|
|
@@ -632,7 +1055,7 @@ export class TwinEngine {
|
|
|
632
1055
|
}
|
|
633
1056
|
const learned = Object.keys(measured?.learned ?? {})
|
|
634
1057
|
const spread = Object.keys(measured?.spreads ?? {})
|
|
635
|
-
|
|
1058
|
+
twinLog(
|
|
636
1059
|
`[twin-engine] "${instanceId}": duration estimator installed โ measured kinds: ${learned.length ? learned.join(',') : 'none'}` +
|
|
637
1060
|
` (with observed spread: ${spread.length ? spread.join(',') : 'none'})` +
|
|
638
1061
|
`${travel.estimator ? `, travel from distance (speeds: ${Object.keys(travel.speedsByKind).join(',')})` : `, travel not derived (${travel.reasons.join('; ')})`}`
|
|
@@ -691,7 +1114,7 @@ export class TwinEngine {
|
|
|
691
1114
|
/* ๊ฐ์ ๊ทธ๋ฃน์์ ์ํ๋ฅ ๋ ๋ฐฐ์ด๋ค โ ํ ๋ฒ ์ ์ ์ ๋์ ๋์ด ๋๋ ์ด๋ค. */
|
|
692
1115
|
yields = buildYieldEstimator(kpi?.groups?.items, {})
|
|
693
1116
|
} catch (err) {
|
|
694
|
-
|
|
1117
|
+
twinWarn(`[twin-engine] "${instanceId}": measured duration lookup failed โ falling back to declared/default durations.`, (err as any)?.message)
|
|
695
1118
|
}
|
|
696
1119
|
/* ๋ค์ ๋ฃ์ด **์ต๊ทผ ์ด ๊ฒ**์ผ๋ก ๋ง๋ ๋ค โ ์ฝ์
์์๊ฐ ๊ณง ๋ฒ๋ฆด ์์์ด๋ฏ๋ก ์ด ํ ์ค์ด LRU ๋ฅผ ๋ง๋ ๋ค. */
|
|
697
1120
|
this.measuredCache.delete(key)
|
|
@@ -723,14 +1146,14 @@ export class TwinEngine {
|
|
|
723
1146
|
const ref = await getRepository(TwinReference).findOne({ where: { domain: { id: domainId }, source: instanceId } })
|
|
724
1147
|
config = ref?.connectionConfig
|
|
725
1148
|
} catch (err: any) {
|
|
726
|
-
|
|
1149
|
+
twinWarn(`[twin-engine] "${instanceId}": could not read the declared stimulus โ ${err?.message ?? err}`)
|
|
727
1150
|
return
|
|
728
1151
|
}
|
|
729
1152
|
const plan = planStimulus(config, { hasScenarioEngine: !!inst.runtime?.scenario, mode: inst.mode }, validateScenario as any)
|
|
730
1153
|
if (plan.action === 'skip') {
|
|
731
1154
|
/* ์ ์ธ์ด ์๋ ๊ฒ์ ์ ์์ด๋ฏ๋ก ์กฐ์ฉํ ์ง๋๋ค. ๋๋จธ์ง ์
์ **๋งํ๋ค** โ ์ ์ธํ๋๋ฐ ์ ์ค๋ฆฐ ์ํ๋ค. */
|
|
732
1155
|
if (plan.reason !== 'none') {
|
|
733
|
-
|
|
1156
|
+
twinWarn(
|
|
734
1157
|
`[twin-engine] "${instanceId}": a stimulus is declared on its source but was not loaded (${plan.reason}${plan.detail ? `: ${plan.detail}` : ''}).` +
|
|
735
1158
|
(plan.reason === 'observed' ? ' This twin runs on observation โ we do not manufacture arrivals for it.' : '')
|
|
736
1159
|
)
|
|
@@ -738,12 +1161,18 @@ export class TwinEngine {
|
|
|
738
1161
|
return
|
|
739
1162
|
}
|
|
740
1163
|
try {
|
|
1164
|
+
/* ์๊ทน์ ์ฃ๊ณ ์์ํ๋ ๋ฐ ๋ ์๊ฐ โ ์ค์ธก์์ ํฐ ํธ์์ ์ด ๊ตฌ๊ฐ์ด 29~72์ด์๋ค. */
|
|
1165
|
+
const tStim = performance.now()
|
|
741
1166
|
inst.runtime!.scenario.load(plan.scenario)
|
|
742
1167
|
inst.runtime!.scenario.start()
|
|
1168
|
+
const stimMs = performance.now() - tStim
|
|
1169
|
+
recordPhase(inst.load ?? (inst.load = newLoadMeter()), 'stimulus', stimMs)
|
|
743
1170
|
const kinds = (plan.scenario.generators ?? []).map((g: any) => g?.kind).filter(Boolean)
|
|
744
|
-
|
|
1171
|
+
twinLog(
|
|
1172
|
+
`[twin-engine] "${instanceId}": stimulus from its source started in ${Math.round(stimMs)}ms โ ${kinds.length ? kinds.join(', ') : 'no generators'}.`
|
|
1173
|
+
)
|
|
745
1174
|
} catch (err: any) {
|
|
746
|
-
|
|
1175
|
+
twinWarn(`[twin-engine] "${instanceId}": the declared stimulus was rejected at load โ ${err?.message ?? err}`)
|
|
747
1176
|
}
|
|
748
1177
|
}
|
|
749
1178
|
|
|
@@ -757,7 +1186,7 @@ export class TwinEngine {
|
|
|
757
1186
|
const repo = getRepository(TwinReference)
|
|
758
1187
|
const ref = await repo.findOne({ where: { domain: { id: domainId }, source: instanceId } })
|
|
759
1188
|
if (!ref) {
|
|
760
|
-
|
|
1189
|
+
twinWarn(`[twin-engine] "${instanceId}": no source reference โ a stimulus has nowhere to be declared.`)
|
|
761
1190
|
return false
|
|
762
1191
|
}
|
|
763
1192
|
ref.connectionConfig = withStimulus(ref.connectionConfig, scenario)
|
|
@@ -877,7 +1306,7 @@ export class TwinEngine {
|
|
|
877
1306
|
const plan = planLiveContinuity(cached)
|
|
878
1307
|
if (!plan.ackedCount && !plan.attentionSinceCount && !plan.energy) return
|
|
879
1308
|
if (typeof kernel.hydrateContinuity !== 'function') {
|
|
880
|
-
|
|
1309
|
+
twinWarn(
|
|
881
1310
|
`[twin-engine] kernel for "${id}" cannot carry continuity (no hydrateContinuity) โ the open demand window, ` +
|
|
882
1311
|
'its peak and the attention start times are lost on every restart.'
|
|
883
1312
|
)
|
|
@@ -887,7 +1316,7 @@ export class TwinEngine {
|
|
|
887
1316
|
kernel.hydrateContinuity(plan.seed)
|
|
888
1317
|
} catch (err: any) {
|
|
889
1318
|
/* ์ด์ด๋ฐ๊ธฐ๊ฐ ์คํจํด๋ ๋ฏธ๋ฌ๋ ๋์์ผ ํ๋ค โ ๋ค๋ง ๋ฌด์์ ์์๋์ง ๋งํ๋ค. */
|
|
890
|
-
|
|
1319
|
+
twinWarn(`[twin-engine] continuity seed failed for "${id}" โ starting without it: ${err?.message ?? err}`)
|
|
891
1320
|
return
|
|
892
1321
|
}
|
|
893
1322
|
const carried = [
|
|
@@ -895,7 +1324,7 @@ export class TwinEngine {
|
|
|
895
1324
|
...(plan.ackedCount ? [`${plan.ackedCount} acknowledged attention(s)`] : []),
|
|
896
1325
|
...(plan.attentionSinceCount ? [`${plan.attentionSinceCount} attention start time(s)`] : [])
|
|
897
1326
|
].join(', ')
|
|
898
|
-
|
|
1327
|
+
twinLog(`[twin-engine] mirror "${id}" carried over ${carried} โ observation axes come from the source.`)
|
|
899
1328
|
}
|
|
900
1329
|
|
|
901
1330
|
static start(id: string, domainId: string, kind: string, model: TwinModelDef, restartPolicy?: RestartPolicy, purpose?: string, resumeFrom?: number): InstanceRuntime {
|
|
@@ -907,8 +1336,16 @@ export class TwinEngine {
|
|
|
907
1336
|
kernel.loadTwinModel(model) // ๊ตฌ์กฐ๋ง. ์ํ๋ ์๋ ์์คํํธ๊ฐ ์ฃผ์
ํ๋ค.
|
|
908
1337
|
this.applyOperations(kernel, model, id) // ์๊ฐยท์์จ ๋ช
์ธ(์์ผ๋ฉด) โ ์์ผ๋ฉด ์ปค๋ ๊ธฐ๋ณธ๊ฐ
|
|
909
1338
|
/* ์ถ์ ๊ธฐ๋ DB ์กฐํ๋ฅผ ํฌํจํด ๋น๋๊ธฐ โ ๊ธฐ๋์ ๋ง์ง ์๊ณ ๋ถ๋๋ค(๋ถ๊ธฐ ์ ์์
์ ๋ช
์ธยท์์๋ก ์ฐ์ถ). */
|
|
910
|
-
this.installEstimators(kernel, domainId, id, model).catch(err =>
|
|
1339
|
+
this.installEstimators(kernel, domainId, id, model).catch(err => twinWarn('[twin-engine] estimator install failed', err?.message))
|
|
1340
|
+
/*
|
|
1341
|
+
* **์์คํํธ์ ๋ ์๊ฐ์ ๊ฐ์ผ๋ก ๋จ๊ธด๋ค** (2026-08-22). ์ค์ธก์ผ๋ก ๋ถํ
์ด 620~717์ด์๊ณ ํธ์ ํ๋์
|
|
1342
|
+
* 15~93์ด์๋๋ฐ, ์ด๋ ์์
์ด ๊ทธ ์๊ฐ์ ์ฐ๋์ง ๋ตํ ๊ณ๊ธฐ๊ฐ ์์๋ค. ๋ก๊ทธ์๋ ๋ฐ๋ฆฌ์ด๊น์ง ์ ์ด
|
|
1343
|
+
* ๋ถํ
๋ก๊ทธ๋ง์ผ๋ก ๊ตฌ๊ฐ์ ์ฝ์ ์ ์๊ฒ ํ๋ค.
|
|
1344
|
+
*/
|
|
1345
|
+
const tWarm = performance.now()
|
|
911
1346
|
this.warmStart(domainId, id, kernel, purpose)
|
|
1347
|
+
const warmMs = performance.now() - tWarm
|
|
1348
|
+
if (warmMs >= 1000) twinLog(`[twin-engine] "${id}": warm start took ${Math.round(warmMs)}ms.`)
|
|
912
1349
|
/*
|
|
913
1350
|
* **๋ฒํธ๋ฅผ ์ด์ด ์ผ๋ค** โ ์ ๋์ ์ด๋ฏธ ์๋ ๋ฒํธ์ ๊ฒน์น์ง ์๊ฒ.
|
|
914
1351
|
*
|
|
@@ -946,6 +1383,17 @@ export class TwinEngine {
|
|
|
946
1383
|
/* ์ ์ฑ
์ ๋ถ๋ฅด๋ ์ชฝ์ด ์ ์ธํ๋ค โ ์ฌ๊ธฐ์ ๊ณ ๋ฅด๋ฉด ๊ฐ์ ํธ์์ด ๋ถ๋ฅด๋ ์๋ฆฌ์ ๋ฐ๋ผ ๋ค๋ฅด๊ฒ ์ฌ๊ธฐ๋ํ๋ค. */
|
|
947
1384
|
restartPolicy: readRestartPolicy(restartPolicy, `start("${id}")`),
|
|
948
1385
|
spaceId: (model as any)?.spaceId,
|
|
1386
|
+
/*
|
|
1387
|
+
* **์๋ฎฌ๋ ๊ณ๊ธฐ๋ฅผ ๋ ๋ค** (2026-08-20).
|
|
1388
|
+
*
|
|
1389
|
+
* ์์ ์๋ `startLive` ์์๋ง ๋ง๋ค์๋ค. ๊ทธ๋์ ๋๋ ํธ์ 22๊ฐ๊ฐ ์๋ฎฌ์ธ ์๋ฒ์์ ใ์ด๋น ๋ช ํ์
|
|
1390
|
+
* ์ฐ๋ใยทใ๋ฐ๋ฆฐ ๊ฒ ์๋ใ๋ฅผ ๋ฌผ์ ์๋ฆฌ๊ฐ **์์ ์์๋ค** โ ์ ๋ ์ฐ๊ธฐ๋ฅผ ๋ฐฐ์น๋ก ๊ณ ์น ๋ค์๋ ๊ทธ ํจ๊ณผ์
|
|
1391
|
+
* ํ๊ท๋ฅผ ์ซ์๋ก ๋ณผ ์ ์๋ค. ์ธ์ง ์๋ ๊ฐ์ ์ ๋ค์ ์ฌ๋์ด ๋๋๋ ค๋ ์๋ฌด๋ ๋ชจ๋ฅธ๋ค.
|
|
1392
|
+
*
|
|
1393
|
+
* ์ ์
(`ingested`)์ ์๋ฎฌ์ ๋ป์ด ์๋ค(์๋ณธ์์ ๋ฐ๋ ๊ฒ์ด ์๋๋ผ ์๊ธฐ๊ฐ ๋ธ๋ค) โ ๊ทธ ์นธ์ 0 ์ผ๋ก
|
|
1394
|
+
* ๋จ๊ณ , ๊ทธ๊ฒ์ด ์ฌ์ค์ด๋ค(ใ๋ฐ์ ๊ฒ์ด ์๋คใ).
|
|
1395
|
+
*/
|
|
1396
|
+
metrics: this.newMetrics(),
|
|
949
1397
|
unsub: () => {}
|
|
950
1398
|
}
|
|
951
1399
|
this.instances[key] = inst
|
|
@@ -953,44 +1401,68 @@ export class TwinEngine {
|
|
|
953
1401
|
this.stopNotes.delete(key)
|
|
954
1402
|
delete this.recovered[key] // ์์คํํธ๋ก ์ปค๋์ ์ฎ๊ฒจ ์ฌ์๋ค โ ์ด์ ๋ผ์ด๋ธ๊ฐ ์ง์ค์ด๋ค.
|
|
955
1403
|
/* ์๋ณธ์ด ์ ์ธํ ์๊ทน โ ๊ธฐ๋์ ๋ง์ง ์๋๋ค(์ถ์ ๊ธฐ์ ๊ฐ์ ๊ท์จ). ๋ชป ์ค์์ผ๋ฉด ๊ทธ ์ฌ์ค์ ๋งํ๋ค. */
|
|
956
|
-
this.installStimulus(domainId, id, inst).catch(err =>
|
|
1404
|
+
this.installStimulus(domainId, id, inst).catch(err => twinWarn(`[twin-engine] "${id}": stimulus install failed โ ${err?.message ?? err}`))
|
|
957
1405
|
|
|
958
1406
|
/* ๋ผ์ด๋ธ ๋ฐ์ธ๋ฉ(P3): data ์ฑ๋ ํํฐ๊ฐ subdomain ์ ๋ณด๋ฏ๋ก Domain ๊ฐ์ฒด๋ฅผ 1ํ ํด์ํด ๋๋ค. */
|
|
959
1407
|
getRepository(Domain).findOne({ where: { id: domainId } }).then(d => (inst.domain = d)).catch(() => {})
|
|
960
1408
|
|
|
961
|
-
/* State ์ฑ๋: runtime.subscribe(snapshotโdeltaโclock) โ pubsub
|
|
1409
|
+
/* State ์ฑ๋: runtime.subscribe(snapshotโdeltaโclock) โ pubsub ๋ธ๋ก๋์บ์คํ
(๊ตฌ๋
๋ฆฌ์กธ๋ฒ๊ฐ instanceId ํํฐ). */
|
|
962
1410
|
const sub = runtime.subscribe((msg: SubscriptionMessage) => {
|
|
963
1411
|
this.publishGuarded(
|
|
964
1412
|
'twin-state',
|
|
965
1413
|
{ twinState: { instanceId: id, kind: msg.kind, revision: (msg as any).revision, payload: msg } },
|
|
966
1414
|
`twin-state:${id}`
|
|
967
1415
|
)
|
|
968
|
-
/* ์์ + ๋ผ์ด๋ธ ๋ฐ์ธ๋ฉ ๋ธ๋ฆฌ์ง: delta
|
|
1416
|
+
/* ์์ + ๋ผ์ด๋ธ ๋ฐ์ธ๋ฉ ๋ธ๋ฆฌ์ง: delta ๋ฅผ ์ ๋ ๋ฒํผ์ ๋ด๊ณ + ์ํฐํฐ๋ณ data(tag:) publish โ ๋ณด๋ ์ปดํฌ๋ํธ ๋ผ์ด๋ธ. */
|
|
969
1417
|
if (msg.kind === 'delta') {
|
|
970
|
-
this.persist(domainId, id, msg).catch(err => console.error('twin persist fail', err))
|
|
971
1418
|
/*
|
|
972
|
-
* โโ
|
|
973
|
-
*
|
|
1419
|
+
* โโ ์ฐ๊ธฐ๋ **๋ชจ์์** ํ ๋ฒ (2026-08-20 ์ค์ธก์ผ๋ก ์ก์) โโโโโโโโโโโโโโโโโโโโ
|
|
1420
|
+
*
|
|
1421
|
+
* ์ฌ๊ธฐ์ ๋ธํ๋ง๋ค `persist()` ๋ฅผ ๋ถ๋ ๋ค โ ํ ํ๋๋น `save()` ํ๋, ๊ทธ๋ฆฌ๊ณ ๊ทธ ์์์
|
|
1422
|
+
* `structureRevOf` ๋ฅผ await(๊ตฌ์กฐ ํ์ด ์๋ ํธ์์ ์บ์ ๋ฏธ์ค๊ฐ ๊ธฐ์ต๋์ง ์์ **๋ธํ๋ง๋ค
|
|
1423
|
+
* SELECT** ์๋ค). ๋ฐ๋ชจ ์๊ทน์ ์ผ ํธ์ 23๊ฐ์์ ๊ทธ ๊ฒฐ๊ณผ๊ฐ ์ด๋ฌ๋ค: ํ๋ก์ธ์ค CPU 81~112%,
|
|
1424
|
+
* ์๋ฌด ์ผ๋ ํ์ง ์๋ ์ง์๊ฐ 8~14์ด. ํ๋กํ์ผ์ `JSON.parse`ยท๋ํ GCยท์ค๋ ๋ํ์ ๊ฐ๋ฆฌํค๊ณ
|
|
1425
|
+
* ๊ณ๊ธฐ๋ ํฑ์ ๋ชซ์ด 16% ๋ผ๊ณ ๋งํ๋ค โ **ํฑ์ด ์๋๋ผ ์ฐ๊ธฐ์๋ค.**
|
|
1426
|
+
*
|
|
1427
|
+
* ๋ฐ๋ก ์ ์ฃผ์์ด **๋ธ๋ก๋์บ์คํ
**์์ ๊ฐ์ ํจ์ ์ ์ ์ด ๋์๋ค(ํฑ ํ๋๊ฐ 35์ด). ๋ธ๋ก๋์บ์คํ
์ ๋ชจ์ผ๋๋ก
|
|
1428
|
+
* ๊ณ ์ณค๊ณ ์ฐ๊ธฐ๋ ๋จ๊ฑด์ผ๋ก ๋จ์ ์์๋ค. ๊ฐ์ ๊ท์จ๋ก ๋ค์ธ๋ค โ ADR-0030 ์ด *"๊ธฐ๋ก ๊ฒฝ๋ก๊ฐ ๋์ธ
|
|
1429
|
+
* ๊ฒ์ ์๋ฎฌ๋ง ์๋ฌธ์ผ๋ก ๋ค์ด์ค๊ธฐ ๋๋ฌธ"* ์ด๋ผ ์๊ณ ํ ๊ทธ ์๋ฆฌ๋ค.
|
|
1430
|
+
*
|
|
1431
|
+
* **๋ฆฌ๋น์ ์ ์ปค๋์ ๊ฒ์ ๊ทธ๋๋ก ๋ ๋ค**(๋ผ์ด๋ธ๋ flush ๋ ํธ์คํธ๊ฐ ๋ถ์ฌํ๋ค). ์๋ฎฌ์ ์ ๋์
|
|
1432
|
+
* ์ปค๋ ๋ฆฌ๋น์ ์ผ๋ก ์ ํ๋ฏ๋ก ์ฌ๊ธฐ์ ๋ค์ ๋ฒํธ๋ฅผ ๋งค๊ธฐ๋ฉด ์๊ฐ์ฌํ์ด ์ด๊ธ๋๋ค.
|
|
1433
|
+
*/
|
|
1434
|
+
;(inst.pendingJournal ?? (inst.pendingJournal = [])).push({ event: msg.event, revision: (msg as any).revision })
|
|
1435
|
+
/*
|
|
1436
|
+
* โโ ๋ธ๋ก๋์บ์คํ
์ **๋ชจ์์** ํ ๋ฒ (2026-08-14 ์ค์ธก์ผ๋ก ์ก์) โโโโโโโโโโโโโโโโโโโโ
|
|
1437
|
+
* ์ฌ๊ธฐ์ ๊ณง๋ฐ๋ก ๋ธ๋ก๋์บ์คํ
ํ๊ณ ์์๋ค. ๊ทธ๋ฐ๋ฐ ์ปค๋์ ํ ๋ฒ์ tick ์์ ์ฌ์ค์ **์ฌ๋ฌ ๊ฐ** ๋ธ๋ค
|
|
974
1438
|
* (์์ฝยท๋ฐฐ์นยท์๋ฃโฆ). ๊ทธ๋์ tick ํ๋๊ฐ ์ ์ํ ํฌ์์ ์๋ฐฑ ๋ฒ ๋ฐ๋ณตํ๋ค โ `order-check`
|
|
975
1439
|
* ํธ์์์ **ํฑ ํ๋๊ฐ 35์ด**๋ฅผ ๋จน๊ณ (๋จ๊ณ ํฉ์ 32ms ์๋ค: ์๊ฐ์ ๋ฐ๋ณต ํ์์ ์์๋ค) ๊ทธ
|
|
976
1440
|
* 35์ด ๋์ ์ด๋ฒคํธ ๋ฃจํ๊ฐ ๋งํ ๊ตฌ๋
์๊ฐ ์๋ฌด๊ฒ๋ ๋นผ๋ด์ง ๋ชปํ๋ค. ๋ฐ๋ฆฐ push ๊ฐ 1024๋ฅผ ๋๋
|
|
977
|
-
* ์๊ฐ pubsub ์ด
|
|
1441
|
+
* ์๊ฐ pubsub ์ด ์ค๋ฅ๋ฅผ ๋ด๊ณ , ๊ทธ ์์ธ๊ฐ ํ์ด๋จธ ์ฝ๋ฐฑ์ ํ๊ณ ์ฌ๋ผ์ **ํธ์คํธ๊ฐ ์ฃฝ์๋ค.**
|
|
978
1442
|
*
|
|
979
1443
|
* ๋ผ์ด๋ธ๋ ์ด๋ฏธ dirty ํ์ + ์ฃผ๊ธฐ flush ๋ก ์ด ๋ฌธ์ ๋ฅผ ํ์ด ๋์๋ค(BROADCAST_COALESCE_MS).
|
|
980
1444
|
* ์๋ฎฌ๋ง ๊ทธ ๊ท์จ ๋ฐ์ ์์๋ค โ ๊ฐ์ ๊ท์จ๋ก ๋ค์ธ๋ค(์ต์ -์ํ ์ฑ๋์ด๋ผ ์ค๊ฐ ์ํ๋ฅผ ๋ชจ๋
|
|
981
1445
|
* ๋ณด๋ผ ์ด์ ๊ฐ ์๋ค: 200ms ๋ง๋ค ๋ง์ง๋ง ๊ฒ ํ๋๋ฉด ํ๋ฉด์ ๊ฐ๋ค).
|
|
982
1446
|
*/
|
|
983
1447
|
inst.dirty = true
|
|
1448
|
+
/* ์ด ์ฌ์ค์ด ๊ฑด๋๋ฆฐ ๋ฌผํ๋ง ๋ค์ ๋ธ๋ก๋์บ์คํ
์์ ๋ง๋ ๋ค(๋ชจ๋ฅด๋ฉด ์ ๋ถ). */
|
|
1449
|
+
this.markItemsDirty(inst, [msg.event])
|
|
984
1450
|
this.ensureBroadcastCoalescer()
|
|
985
1451
|
}
|
|
986
1452
|
})
|
|
987
1453
|
inst.unsub = () => sub.unsubscribe()
|
|
988
1454
|
|
|
1455
|
+
/* ์์์ ์ฐ ์์คํํธ ์๊ฐ์ ๊ณ๊ธฐ์ ์ ๋๋ค โ ๊ณ๊ธฐ๋ ์ธ์คํด์ค๊ฐ ์๊ธด ๋ค์์ผ ์๋ค. */
|
|
1456
|
+
recordPhase(inst.load ?? (inst.load = newLoadMeter()), 'warmStart', warmMs)
|
|
1457
|
+
|
|
989
1458
|
/* ๋ณต๊ตฌ ์ต์ปค: ๋ ์ง์คํธ๋ฆฌ์ model/kind/status/restartPolicy ์์(์ฌ๋ถํ
์ ์ด๊ฒ ์์ด์ผ replayยท์ ์ธ ๊ฑฐ๋ ๊ฐ๋ฅ). */
|
|
990
|
-
this.register(domainId, id, kind, model, 'running', inst.restartPolicy).catch(err =>
|
|
1459
|
+
this.register(domainId, id, kind, model, 'running', inst.restartPolicy).catch(err => twinError('twin register fail', err))
|
|
991
1460
|
|
|
992
1461
|
/* ์์ปค tick โ ์ค์ผ๋ ํค์ setInterval(๋ฉ์ธ ๋ฃจํ). ๊ธด ์๋ฎฌ ์คํ-๋ฃจํ(worker thread)๋ ์ค์ผ์ผ ํ๋๋(ํฅํ, ยงhost ๊ฒฝ๊ณ). */
|
|
993
|
-
inst.timer =
|
|
1462
|
+
inst.timer = this.startTickTimer(
|
|
1463
|
+
() => this.tickGuarded(domainId, id, runtime),
|
|
1464
|
+
t => (inst.timer = t)
|
|
1465
|
+
)
|
|
994
1466
|
return inst
|
|
995
1467
|
}
|
|
996
1468
|
|
|
@@ -1054,7 +1526,7 @@ export class TwinEngine {
|
|
|
1054
1526
|
const space = await getRepository(TwinSpace).findOne({ where: { domain: { id: domainId }, spaceId: sid } }).catch(() => null)
|
|
1055
1527
|
const offset = utcOffsetOf(space?.timezone)
|
|
1056
1528
|
if (offset === undefined) {
|
|
1057
|
-
if (space?.timezone)
|
|
1529
|
+
if (space?.timezone) twinWarn(`[twin-engine] space "${sid}" declares time zone "${space.timezone}" but it is not a known IANA zone โ times will be read as UTC.`)
|
|
1058
1530
|
return model
|
|
1059
1531
|
}
|
|
1060
1532
|
return { ...model, utcOffsetMinutes: offset }
|
|
@@ -1068,7 +1540,7 @@ export class TwinEngine {
|
|
|
1068
1540
|
kernel.loadTwinModel(model)
|
|
1069
1541
|
/* **์ธ์ฐ๋ ์ชฝ์ด ์๋ ์ฌ์ค์ ์ธ์ธ ๋ ๋งํ๋ค.** ์์ ์๋ ์ฒซ ์ด๋ฒคํธ๊ฐ ๋์ฐฉํด์ผ ์ปค๋์ด ์ค์ค๋ก๋ฅผ
|
|
1070
1542
|
๊ด์ธก ๊ตฌ๋์ผ๋ก ์ฌ๊ฒผ๊ณ , ๊ทธ๋์ ์์ง ์๋ฌด๊ฒ๋ ๋ชป ๋ฐ์ ๋ฏธ๋ฌ๋ ์๋ฎฌ๋ ์ด์
์ทจ๊ธ์ ๋ฐ์๋ค. */
|
|
1071
|
-
kernel
|
|
1543
|
+
declareObserved(kernel, `live twin '${id}'`)
|
|
1072
1544
|
this.applyOperations(kernel, model, id) // ๋ช
์ธ๋ ๋ผ์ด๋ธ์๋ ์ค๋ฆฐ๋ค(์์ธก ์๊ฒฉ์ด sim ๊ณผ ๊ฐ์์ง๋ค)
|
|
1073
1545
|
/* `projector` ํ๋๋ ์ ์ด๋ฆ์ผ๋ก ๋จ๊ธด๋ค โ ์๋น์ฒ๊ฐ `snapshot()` ์ ๋ถ๋ฅด๋ฏ๋ก ์์ ์ด๋ํฐ๋ก ์๋๋ค.
|
|
1074
1546
|
* (P3 ์์ ์๋น์ฒ๋ฅผ ์ปค๋ ์ดํ๋ก ๋ฐ๊พธ๋ฉด ์ฌ๋ผ์ง๋ค.) */
|
|
@@ -1093,16 +1565,17 @@ export class TwinEngine {
|
|
|
1093
1565
|
inst.unsub = kernel.onEvent?.((e: CanonicalEnvelope) => {
|
|
1094
1566
|
if (e && typeof e === 'object' && applying.has(e as object)) return // ์ธ์
์ ์ฌ๋ฐฉ์ถ โ ์ ๋์ ์ธ์
์์ ํ ๋ฒ๋ง
|
|
1095
1567
|
;(inst.pendingJournal ?? (inst.pendingJournal = [])).push(e)
|
|
1096
|
-
/* ์ปค๋์ด ๋ธ ์ฌ์ค๋ ์ํ๋ฅผ ๋ฐ๊พผ๋ค(ํผํฌยท๋ง๊ฐ) โ ๋ค์
|
|
1568
|
+
/* ์ปค๋์ด ๋ธ ์ฌ์ค๋ ์ํ๋ฅผ ๋ฐ๊พผ๋ค(ํผํฌยท๋ง๊ฐ) โ ๋ค์ ๋ธ๋ก๋์บ์คํ
์ฃผ๊ธฐ์ ์ค๋ฆฐ๋ค. */
|
|
1097
1569
|
inst.dirty = true
|
|
1570
|
+
this.markItemsDirty(inst, [e])
|
|
1098
1571
|
this.ensureBroadcastCoalescer()
|
|
1099
1572
|
}) ?? (() => {})
|
|
1100
1573
|
/* ์ถ์ ๊ธฐ(์ค์ธกยท๊ฑฐ๋ฆฌ)๋ ๋ผ์ด๋ธ์ ๋ถ์ธ๋ค โ ์์ธก์ด ์์๋ก ๊ณ์ฐ๋์ง ์๊ฒ. ๊ธฐ๋์ ๋ง์ง ์๋๋ค. */
|
|
1101
|
-
this.installEstimators(kernel, domainId, id, model).catch(err =>
|
|
1102
|
-
inst.metrics =
|
|
1574
|
+
this.installEstimators(kernel, domainId, id, model).catch(err => twinWarn('[twin-engine] estimator install failed', err?.message))
|
|
1575
|
+
inst.metrics = this.newMetrics()
|
|
1103
1576
|
this.instances[key] = inst
|
|
1104
1577
|
/* ๋ฏธ๋ฌ์๋ ๋ถ๋ฅธ๋ค โ ์ ์ธ์ด ์์ผ๋ฉด ใ๋ฏธ๋ฌ์๋ ์ฃ์ง ์๋๋คใ๊ณ ๋งํด์ผ ํ๋ค(์กฐ์ฉํ ๋ฌด์ ๊ธ์ง). */
|
|
1105
|
-
this.installStimulus(domainId, id, inst).catch(err =>
|
|
1578
|
+
this.installStimulus(domainId, id, inst).catch(err => twinWarn(`[twin-engine] "${id}": stimulus check failed โ ${err?.message ?? err}`))
|
|
1106
1579
|
/*
|
|
1107
1580
|
* **์์ฒ์ด ๋ํ์ด ์ฃผ์ง ์๋ ๊ฒ๋ง ์๋๋ค** (2026-08-18 ์ค์ธก์ผ๋ก ๋ถ์).
|
|
1108
1581
|
*
|
|
@@ -1114,27 +1587,104 @@ export class TwinEngine {
|
|
|
1114
1587
|
* ๊ด์ธก ์ถ(์ฌ๊ณ ยท์์นยท์ค๋น)์ **์ฌ์ ํ ์ฌ์ง ์๋๋ค** โ ๋ค์ ๊ณ์ธก์ด ์ ์ ํ๊ณ , ์ฌ์ผ๋ฉด ๋ ๋ ๋ฌผ๊ฑด์ด
|
|
1115
1588
|
* ๋์ด์๋๋ค. ๋ฌด์์ ๋๊ธธ์ง๋ `planLiveContinuity` ๊ฐ ๊ณ ๋ฅด๊ณ , ์ด๋ป๊ฒ ํก์ํ ์ง๋ ์ปค๋์ด ์ ํ๋ค.
|
|
1116
1589
|
*/
|
|
1590
|
+
/*
|
|
1591
|
+
* โโ **์ฌ๊ฐ์ ์์ ๋ฏธ๋ฌ๋ฅผ ๋์ธ์ด๋ค** (2026-08-24) โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
1592
|
+
*
|
|
1593
|
+
* ์ด ์๋ฆฌ์์ ๋ฏธ๋ฌ๋ ์ค๋ซ๋์ ๋์ฐพ์ ์ํ๋ฅผ **๋ฒ๋ ธ๋ค**. ์ ์ ๋ ใ์ง์ค์ ์์ฒ์ ์์ผ๋ ๋ค์ ๊ณ์ธก์ด
|
|
1594
|
+
* ์ ์ ํ๋คใ์๊ณ ๋ผ์ด๋ธ ํผ๋์์๋ ์ณ์๋ค. ๊ทธ๋ฐ๋ฐ ์๋ณธ์ด **์ปค์ ์ฆ๋ถ**์ผ๋ก ๋งํ๋ ํ์ฅ์์๋ ๊ทธ
|
|
1595
|
+
* ์ ์ ์ด ์ค์ง ์๋๋ค: ์ปค์๊ฐ ๋ฐ๋ผ์กํ ๋ค ์๋ณธ์ด ๋ณํ์ง ์์ผ๋ฉด ๋ฏธ๋ฌ๋ ์๊ตฌํ ๋น ์ฑ๋ก ๋จ๋๋ค.
|
|
1596
|
+
* ๊ทธ๋ฆฌ๊ณ ๊ทธ ๋น ์ฑ๋ก ํ๋ฉด์ด ใ์ด์ ์์ใ์ ๋ณด์๋ค โ ์ฌ์ค์ด ์ฌ๋ผ์ง๋ ๋์ ํ๋ฉด์ด ์์ฌ์ํจ ๊ฒ์ด๋ค.
|
|
1597
|
+
*
|
|
1598
|
+
* ๋๋๋ฆฌ๋ ๊ฒ์ **์ํ๊ฐ ์๋๋ผ ์ฌ๊ฐ์ **์ด๋ค. ์ํ๋ง ์ฌ์ผ๋ฉด ๊ทธ ๋ค๋ฅผ ์ด์ด ์ ์ ๊ฒฐ๊ณผ๊ฐ 0๋ถํฐ ์ ์
|
|
1599
|
+
* ๊ฒฐ๊ณผ์ ์กฐ์ฉํ ๋ฌ๋ผ์ง๋ค(๋ฆฌ๋์๋ ๋ณด๋ฅ๋ ๋ด๊นยท์ง๊ณ ์ค์ธ ์๋๋ ๋ ๋ค). ๊ทธ ๋์น๋ ์ปค๋ ์ํ์ด
|
|
1600
|
+
* ์ฆ๋ช
ํ๋ค(`observed-checkpoint.test.ts` โ ์ฌ๊ฐ์ + ๊ผฌ๋ฆฌ == 0๋ถํฐ ์ ๊ธฐ).
|
|
1601
|
+
*
|
|
1602
|
+
* ์จ์์ **๊ทธ ๊ณต์ฅ์ด ์์ง ๊ทธ ๊ณต์ฅ์ผ ๋๋ง** ์ค๊ณ , ๋ง์ง๋ง ์ฒดํฌํฌ์ธํธ ์ดํ์ ์ฌ์ค์ ์ด๋ฏธ ์ ํ ๋ค์ด
|
|
1603
|
+
* ์๋ค(`warmSeedFor`). ์จ์์ด ์์ผ๋ฉด ์ ๊ณผ ๊ฐ์ด ๋น ์ฑ๋ก ์์ํ๋ค โ ์ง์ด๋ด์ง ์๋๋ค.
|
|
1604
|
+
*/
|
|
1605
|
+
const seed = this.recovered[key]?.fold?.reducer
|
|
1606
|
+
if (seed) {
|
|
1607
|
+
if (typeof kernel.restoreObserved !== 'function') {
|
|
1608
|
+
twinWarn(
|
|
1609
|
+
`[twin-engine] "${id}": a fold seed is stored but this kernel cannot take it (no restoreObserved) โ ` +
|
|
1610
|
+
'the mirror starts empty and waits for the source to re-tell everything. Upgrade the kernel.'
|
|
1611
|
+
)
|
|
1612
|
+
} else {
|
|
1613
|
+
try {
|
|
1614
|
+
kernel.restoreObserved(seed)
|
|
1615
|
+
const st = kernel.getSnapshot?.()
|
|
1616
|
+
twinLog(
|
|
1617
|
+
`[twin-engine] mirror "${id}" resumed from the stored fold seed at revision ${this.recovered[key]?.revision} โ ` +
|
|
1618
|
+
`items ${st?.items?.length ?? 0} ยท orders ${st?.orders?.length ?? 0} ยท tasks ${st?.tasks?.length ?? 0} ` +
|
|
1619
|
+
'(the journal is not folded from the beginning).'
|
|
1620
|
+
)
|
|
1621
|
+
if (this.recovered[key]?.fold?.oee) inst.oee?.restore(this.recovered[key].fold.oee)
|
|
1622
|
+
} catch (err: any) {
|
|
1623
|
+
/* ๋์ธ์ฐ๊ธฐ๊ฐ ์คํจํด๋ ๋ฏธ๋ฌ๋ ๋์์ผ ํ๋ค โ ๋ค๋ง ๋ฌด์์ ์์๋์ง ๋งํ๋ค. */
|
|
1624
|
+
twinWarn(`[twin-engine] "${id}": could not resume from the stored fold seed โ starting empty: ${err?.message ?? err}`)
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1117
1628
|
this.seedLiveContinuity(domainId, id, kernel)
|
|
1118
1629
|
delete this.recovered[key]
|
|
1119
1630
|
/* ๋ผ์ด๋ธ ๋ฐ์ธ๋ฉ(data ์ฑ๋) subdomain ํํฐ์ฉ Domain 1ํ ํด์(sim ๊ณผ ๋์ผ). */
|
|
1120
1631
|
getRepository(Domain).findOne({ where: { id: domainId } }).then(d => (inst.domain = d)).catch(() => {})
|
|
1121
|
-
/*
|
|
1632
|
+
/*
|
|
1633
|
+
* ์ ๋ revision ์นด์ดํฐ ์๋ โ ๊ธฐ์กด ์ ๋ ์ต๋์น์์ ์ด์ด๋ถ์(์ฌ๊ธฐ๋ ์ revision ์ถฉ๋ ๋ฐฉ์ง).
|
|
1634
|
+
*
|
|
1635
|
+
* โโ **์ปค๋์๋ ๊ฐ์ ๋ฒํธ๋ฅผ ์๋ ค ์ค๋ค** (2026-08-23 ์ค์ธก) โโโโโโโโโโโโโโโโโโโโ
|
|
1636
|
+
* ์์ ์๋ ์ด ์๋ฆฌ์์ `inst.revision`(์ ๋ ์ค ๋ฒํธ)๋ง ์ด์ด๋ฐ๊ณ **์ปค๋์ 0 ๋ถํฐ ์ธ๊ฒ ๋์๋ค.**
|
|
1637
|
+
* ๊ทธ๋์ ์ฒดํฌํฌ์ธํธ๊ฐ ๋ป์ด ๋ค๋ฅธ ๋ ์๋ฅผ ๋๋ํ ์ ์๋ค.
|
|
1638
|
+
*
|
|
1639
|
+
* ๋ฐ๊นฅ revision 12,972 ์ ๋์ ์ ํ ์ค ๋ฒํธ (์ด ์๋ฆฌ์์ ์ด์ด๋ฐ๋๋ค)
|
|
1640
|
+
* state.revision 6,478 ์ปค๋์ด ์ฒ๋ฆฌํ ๋ดํฌ ์ (0 ๋ถํฐ ์
๋ค)
|
|
1641
|
+
*
|
|
1642
|
+
* ํฌ์ฒ ๋ฏธ๋ฌ์์ ํ์ ํ๋ค: ์ฐจ์ด 6,494 ๋ ๊ทธ ํธ์์ **๋ค์ ์ธ์ด ์๊ฐ**(09:11)์ ์ ๋์ด ์ด๋ฏธ ๊ฐ๊ณ
|
|
1643
|
+
* ์๋ ์ค ์์ ์ ํํ ๊ฐ์๋ค(์ค 6494 = 08:23:59, ์ค 6495 = 09:11:13). ์ฐ์๊ฐ ๋ง์๋ค.
|
|
1644
|
+
*
|
|
1645
|
+
* ๊ฒฐํจ์ ๋ ์๊ฐ ๋ค๋ฅด๋ค๋ ๊ฒ ์์ฒด๊ฐ ์๋๋ผ **๋น๋์นญ**์ด๋ค: ์ฌ๊ธฐ๋ ๊ฒฝ๋ก(`start` โ `resumeRevision`)๋
|
|
1646
|
+
* ์ปค๋์ ๋ฒํธ๋ฅผ ์๋ ค ์ฃผ๋๋ฐ **์ด ๊ฒฝ๋ก(๋ฏธ๋ฌ)๋ ์๋ ค ์ฃผ์ง ์์๋ค.** ๊ทธ๋์ ๊ฐ์ ์ ์ฅ๋ฌผ์ ์ฝ๋ ์๋น์ฒ๊ฐ
|
|
1647
|
+
* ๊ฐ์ ์ด๋ฆ์ ๋ ์๋ฅผ ๊ฐ์ ์ถ์ผ๋ก ๊ฒฌ์ฃผ๊ฒ ๋๊ณ , ์ค์ ๋ก ๊ทธ๋ ๊ฒ ์ฝํ๋ค.
|
|
1648
|
+
*
|
|
1649
|
+
* ์ ๋์ด ๋น์ด ์์ผ๋ฉด(0) ์๋ฌด ์ผ๋ ํ์ง ์๋๋ค โ ์ ํธ์์ 0 ๋ถํฐ ์ธ๋ ๊ฒ์ด ๋ง๋ค. ๊ทธ๋ฆฌ๊ณ ์ด ์กฐํ๋
|
|
1650
|
+
* ๋น๋๊ธฐ๋ผ ๊ทธ ์ฌ์ด์ ์ด๋ฒคํธ๊ฐ ๋ช ๊ฑด ๋ค์ด์ ์์ ์ ์๋๋ฐ, ๊ทธ๋ ๋ค๋ก ๋๋๋ฆฌ๋ ๊ฒ์ ์ปค๋์ด ๊ฑฐ์ ํ๋ค
|
|
1651
|
+
* (๊ฒน์น๋ ๋ฒํธ๋ฅผ ๋ง๋ ๊ทธ ํ์ ์ด๋ค). ๊ฑฐ์ ์ ์ผํค์ง ์๊ณ ๋จ๊ธด๋ค.
|
|
1652
|
+
*/
|
|
1122
1653
|
getRepository(TwinEvent)
|
|
1123
1654
|
.findOne({ where: { domain: { id: domainId }, instanceId: id }, order: { revision: 'DESC' } })
|
|
1124
|
-
.then(top =>
|
|
1655
|
+
.then(top => {
|
|
1656
|
+
const head = top?.revision ?? 0
|
|
1657
|
+
inst.revision = head
|
|
1658
|
+
if (!head || typeof (kernel as any).resumeRevision !== 'function') return
|
|
1659
|
+
try {
|
|
1660
|
+
;(kernel as any).resumeRevision(head)
|
|
1661
|
+
} catch (err: any) {
|
|
1662
|
+
/* ๋ค๋ก ๊ฐ ์ ์๋ค๋ ๊ฑฐ์ โ ์ด ์กฐํ๊ฐ ๋์ฐฉํ๊ธฐ ์ ์ ์ด๋ฏธ ๊ทธ๋งํผ ์ฒ๋ฆฌํ๋ค๋ ๋ป์ด๋ค. */
|
|
1663
|
+
twinWarn(`[twin-engine] "${id}": journal head ${head} is behind the kernel โ ${err?.message ?? err}`)
|
|
1664
|
+
}
|
|
1665
|
+
})
|
|
1125
1666
|
.catch(() => (inst.revision = 0))
|
|
1126
|
-
this.register(domainId, id, kind, model, 'running', 'resync').catch(err =>
|
|
1667
|
+
this.register(domainId, id, kind, model, 'running', 'resync').catch(err => twinError('twin register fail', err))
|
|
1127
1668
|
return inst
|
|
1128
1669
|
}
|
|
1129
1670
|
|
|
1130
1671
|
/**
|
|
1131
|
-
* live ์ด๋ฒคํธ ์ธ์ ์คํธ โ projector ๊ตฌ๋ + data(tag)
|
|
1672
|
+
* live ์ด๋ฒคํธ ์ธ์ ์คํธ โ projector ๊ตฌ๋ + data(tag) ๋ธ๋ก๋์บ์คํ
(ํ๋ฃจํ์ ์ธ๋ฐ์ด๋ ๋์ฐฉ ์ง์ , command-routing ยง8.2).
|
|
1132
1673
|
* reference ์ด๋ํฐ๊ฐ ๋ธ records โ ์ปค๋ face2-adapter.ingest โ CanonicalEnvelope ๋ฅผ ์ฌ๊ธฐ๋ก ๋ฐ์ด๋ฃ๋๋ค.
|
|
1133
1674
|
* (State ์ฑ๋ ๋ธํ/์ ๋ ๊ฒฐ์ ์ ํ์ โ ์ค์ผ๋ ํค์ data(tag) ๋ฏธ๋ฌ ์ค์ฌ.)
|
|
1675
|
+
*
|
|
1676
|
+
* โโ ๋ฃ์ ์๋ฅผ **๋ตํ๋ค** (2026-08-20) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
1677
|
+
* ํธ์์ด ๋ผ์ด๋ธ๋ก ๋์ง ์์ผ๋ฉด ์ฌ๊ธฐ์ ๋ดํฌ๋ฅผ ๋ฒ๋ฆฐ๋ค. ๊ทธ๊ฒ ์์ฒด๋ ๋ง๋ค(๋ฃ์ ์ปค๋์ด ์๋ค). ๋ฌธ์ ๋
|
|
1678
|
+
* **์กฐ์ฉํ** ๋ฒ๋ฆฐ ๊ฒ์ด์๋ค: ํธ์์ด ๋ฉ์ถ ๋ค์๋ ํผ๋๋ ๋จ์ ๋ ์ฝ๋๋ฅผ ๋๋ฅด๊ณ , ์ ์
์ฅ๋ถ๋ ๊ทธ๊ฒ์
|
|
1679
|
+
* ใํต๊ณผใ๋ก ์
๋ค. ํ๋ฉด์ ๋ฉ์ถ ํธ์ ์์ ใ150 ํต๊ณผ ยท 100%ใ๋ผ๊ณ ์ ์๋ค โ ์ฌ์ค์ด ์ฌ๋ผ์ง๋ ๋์
|
|
1680
|
+
* ํ๋ฉด์ด ์์ฌ์ํจ ๊ฒ์ด๋ค.
|
|
1681
|
+
*
|
|
1682
|
+
* ๊ทธ๋์ **๋ฃ์ ์๋ฅผ ๋๋ ค์ค๋ค.** ๋ถ๋ฅด๋ ์ชฝ์ด ์ ์ ์์ ๊ฒฌ์ฃผ์ด ๋ฒ๋ ค์ง ์๋ฅผ ์ฅ๋ถ์ ์ ๋๋ค. ๋ฐํ์
|
|
1683
|
+
* ๋ฌด์ํ๋ ํธ์ถ๋ถ๋ ๊ทธ๋๋ก ๋์ํ๋ค(์ ๊ณผ ๊ฐ๋ค).
|
|
1134
1684
|
*/
|
|
1135
|
-
static ingestLive(domainId: string, id: string, envelopes: CanonicalEnvelope[]):
|
|
1685
|
+
static ingestLive(domainId: string, id: string, envelopes: CanonicalEnvelope[]): number {
|
|
1136
1686
|
const inst = this.instances[runtimeKey(domainId, id)]
|
|
1137
|
-
if (inst?.mode !== 'live' || !inst.projector) return
|
|
1687
|
+
if (inst?.mode !== 'live' || !inst.projector) return 0
|
|
1138
1688
|
const tIngest = performance.now()
|
|
1139
1689
|
/* ์ธ์
๋ดํฌ๋ฅผ ํ์ํด ๋๊ณ ๋ฃ๋๋ค โ ์ปค๋์ด ๊ทธ๊ฒ์ ์ฌ๋ฐฉ์ถํด๋ ์ ๋์ ๋ ๋ฒ ์ ํ์ง ์๊ฒ(์ ๊ตฌ๋
์ฃผ์). */
|
|
1140
1690
|
for (const e of envelopes) {
|
|
@@ -1144,51 +1694,151 @@ export class TwinEngine {
|
|
|
1144
1694
|
}
|
|
1145
1695
|
recordPhase(inst.load ?? (inst.load = newLoadMeter()), 'ingest', performance.now() - tIngest)
|
|
1146
1696
|
// ์ ๋ ๊ฒฐ์ (๋ผ์ด๋ธ๋ sim ์ฒ๋ผ ์ด๋ฒคํธ ์์) โ ๋จ ์ด๋ฒคํธ๋ง๋ค DB write ํ๋ฉด ๋ถํํญ๋ฐ์ด๋ผ ๋ชจ์๋๋ค๊ฐ
|
|
1147
|
-
// coalescer tick ์์ ๋ฐฐ์น ๊ธฐ๋ก(
|
|
1697
|
+
// coalescer tick ์์ ๋ฐฐ์น ๊ธฐ๋ก(๋ธ๋ก๋์บ์คํ
๊ณผ ๋์ผ ์ฃผ๊ธฐ). ํ์คํ ๋ฆฌ ๋
ธ๋ธยทํ์๋ผ์ธยท๋น์ฆ๋์ค ์์ฅ์ด ์ด ์ ๋์ ์ฝ๋๋ค.
|
|
1148
1698
|
;(inst.pendingJournal ?? (inst.pendingJournal = [])).push(...envelopes)
|
|
1149
1699
|
if (inst.metrics) { inst.metrics.ingestedTotal += envelopes.length; inst.metrics._accIngest += envelopes.length } // ๊ณ์ธก(โฃ-1)
|
|
1150
|
-
//
|
|
1151
|
-
// ์ด๋ฒคํธ๋ง๋ค
|
|
1700
|
+
// ๋ธ๋ก๋์บ์คํ
๋ณํฉ(ingest-scale ยง1.1/ยง4.4) โ apply ๋ O(1)ยท์ธ๋ค. ๊ทธ๋ฌ๋ ๋ธ๋ก๋์บ์คํ
(snapshot O(state))์ ๋น์ธ(๋๊ท๋ชจ 5ms+)
|
|
1701
|
+
// ์ด๋ฒคํธ๋ง๋ค ๋ธ๋ก๋์บ์คํ
ํ๋ฉด ํญ๋ฐ โ dirty ๋ง ์ฐ๊ณ coalescer tick ์ด ์ฃผ๊ธฐ ๋ธ๋ก๋์บ์คํ
(๋ธ๋ก๋์บ์คํ
๋ฅ โ ์ธ์ ์คํธ๋ฅ ).
|
|
1152
1702
|
inst.dirty = true
|
|
1703
|
+
this.markItemsDirty(inst, envelopes)
|
|
1153
1704
|
this.ensureBroadcastCoalescer()
|
|
1705
|
+
return envelopes.length
|
|
1154
1706
|
}
|
|
1155
1707
|
|
|
1156
1708
|
/**
|
|
1157
|
-
* ๊ตฌ๊ฐ ์ฑ๊ณผ
|
|
1709
|
+
* ๊ตฌ๊ฐ ์ฑ๊ณผ ๋ธ๋ก๋์บ์คํ
์ **์์ด๋ค**(2026-08-06). ์นด๋๊ฐ `twinKpi` ๋ฅผ ์ง์ ๋ฌป๋๋ค.
|
|
1158
1710
|
*
|
|
1159
|
-
* ์: ์นด๋๋ฅผ ์ฌ๋ฌ ๋จ๊ณ(๊ณต๊ฐยทํธ์ยท๊ตฌ์ญยท์๋ฆฌยท์ค๋น)์ ๋ถ์ด๋ ค๋ฉด
|
|
1711
|
+
* ์: ์นด๋๋ฅผ ์ฌ๋ฌ ๋จ๊ณ(๊ณต๊ฐยทํธ์ยท๊ตฌ์ญยท์๋ฆฌยท์ค๋น)์ ๋ถ์ด๋ ค๋ฉด ๋ธ๋ก๋์บ์คํ
์ผ๋ก๋ ํ๊ทธ๊ฐ ํธ์๋น 1,200๊ฐ๊ฐ ๋๊ณ ,
|
|
1160
1712
|
* **๋ชจ๋ธ์ ์นด๋๋ฅผ ํ๋๋ ์ ๋์๋** 30์ด๋ง๋ค ํธ์๋ง๋ค ์ ๋์ ์ ์๋ค. ์ง์๋ก ๋ฐ๊พธ๋ ๋ณด๊ณ ์๋ ์นด๋
|
|
1161
1713
|
* ์๋งํผ๋ง ๋ค๊ณ , ๊ฐ์ (๋์ยท์ฐฝยท์ถ) ์ ํด๋ผ์ด์ธํธ๊ฐ ํ๋๋ก ํฉ์น๋ค.
|
|
1162
1714
|
*
|
|
1163
1715
|
* ๋ค์ผ๋ก ์ง์๋ง ํ ์ ์๋ ๊ฒ์ด ๋ ์๊ฒผ๋ค โ **๊ณผ๊ฑฐ ์๊ฐ**(`toTime`)๊ณผ **๊ณต๊ฐ ๋จ์ ํฉ์ฐ**(์ฌ๋ฌ ํธ์์
|
|
1164
|
-
* ํ ๋ฒ์ ์ ๊ธฐ).
|
|
1716
|
+
* ํ ๋ฒ์ ์ ๊ธฐ). ๋ธ๋ก๋์บ์คํ
๋ฃจํ๋ ํธ์๋ณ์ด๋ผ ๋ ๋ค ๋ชป ํ๋ค.
|
|
1165
1717
|
*
|
|
1166
1718
|
* ์ถ์ ๋๋ ๋ ํด๋ ๋น์ฉ์ด ๊ฐ๋ค๋ ์ค์ธก์ด ๊ทผ๊ฑฐ๋ค(`test/kpi-query-bench.test.ts`).
|
|
1167
1719
|
*/
|
|
1168
|
-
/**
|
|
1720
|
+
/**
|
|
1721
|
+
* ๋ช ์ฐฝ๋ง๋ค ํ ๋ฒ์ **์ ๋ถ** ๋ง๋๋ โ ์ฌ๊ฑด ์์ด ๊ฐ์ด ๋ฐ๋๋ ์๋ฆฌ์ ๋ํ ๊ทธ๋ฌผ.
|
|
1722
|
+
*
|
|
1723
|
+
* 25 ์ฐฝ์ด๋ฉด ๊ธฐ๋ณธ ์ฃผ๊ธฐ์์ 5์ด๋ค. ๋ณด์ฅ์ด ์๋๋ผ ๊ทธ๋ฌผ์ด๋ค(์ `publishEntityData` ์ฃผ์).
|
|
1724
|
+
*/
|
|
1725
|
+
static FULL_BROADCAST_EVERY = 25
|
|
1726
|
+
/** ์ ๋ถ ๋ง๋ ํ์ โ ๋ฒ์๋ฅผ ์ขํ์ง ๋ชปํ ์ฐฝ์ด ์ผ๋ง๋ ๋๋์ง ๊ฐ์ผ๋ก ๋จ๋๋ค. */
|
|
1727
|
+
static broadcastFullPasses = 0
|
|
1728
|
+
/**
|
|
1729
|
+
* ๋ธ๋ก๋์บ์คํ
์ ๋ง๋ ํ์ ์ ๋ถ โ **์ ๋ถ ๋ง๋ ํ์์ ๋ถ๋ชจ.**
|
|
1730
|
+
*
|
|
1731
|
+
* ๋ถ์๋ง ๋ด๋ฉด ใ์ ๋ถ ๋ง๋ค๊ธฐ 1,200ํใ๊ฐ ๋ง์ ๊ฒ์ธ์ง ์ ์ ๊ฒ์ธ์ง ์ฝ์ ์ ์๋ค. ์ขํ๊ธฐ๊ฐ ๋ฃ๊ณ ์์ผ๋ฉด
|
|
1732
|
+
* ์ด ๊ฐ์ `1/FULL_BROADCAST_EVERY` ์ฏค์ด ์ ๋ถ ๋ง๋ ํ์์ด๊ณ , ๋ ๊ฐ์ด ๋น์ทํ๋ฉด ๋ฒ์๋ฅผ ๊ฑฐ์ ๋ชป ์ขํ
|
|
1733
|
+
* ๊ฒ์ด๋ค(์์ธ์ ๋๊ฐ ใ๋ชจ๋ฅธ๋คใ๋ก ๋จ์ด์ง๋ ์ฌ๊ฑด์ด๋ค).
|
|
1734
|
+
*/
|
|
1735
|
+
static broadcastPasses = 0
|
|
1736
|
+
|
|
1737
|
+
/**
|
|
1738
|
+
* ์ด ์ฐฝ์ ๊ฑด๋๋ฆฐ ๋ฌผํ์ ๋ชจ์๋ค โ **๋งํ ์ ์์ผ๋ฉด ๋ฒ์๋ฅผ ๋ฒ๋ฆฐ๋ค(์ ๋ถ ๋ง๋ ๋ค).**
|
|
1739
|
+
*
|
|
1740
|
+
* `events` ๋ฅผ ์ฃผ์ง ์์ผ๋ฉด ใ๋ฌด์์ด ๋ฐ๋์๋์ง ๋ชจ๋ฅธ๋คใ๋ค(๊ตฌ์กฐ ์ ํ์ฒ๋ผ ์ํ ์ ๋ฐ์ด ๋ฌ๋ผ์ง๋ ์๋ฆฌ).
|
|
1741
|
+
* ํ ์ฐฝ์์ ํ ๋ฒ ใ๋ชจ๋ฅธ๋คใ๊ฐ ๋๋ฉด ๊ทธ ์ฐฝ์ ๋๊น์ง ๋ชจ๋ฅด๋ ์ฑ๋ก ๋๋ค โ ๋ค์ ์จ ์ฌ๊ฑด์ผ๋ก ๋ฒ์๋ฅผ
|
|
1742
|
+
* ๋์ด๋ฆฌ๋ฉด ์ ์ฌ๊ฑด์ด ๊ฑด๋๋ฆฐ ๊ฒ์ ๋น ๋จ๋ฆฐ๋ค.
|
|
1743
|
+
*/
|
|
1744
|
+
static markItemsDirty(inst: InstanceRuntime, events?: any[]): void {
|
|
1745
|
+
if (inst.dirtyItems === undefined) return // ์ด ์ฐฝ์ ์ด๋ฏธ ใ๋ชจ๋ฅธ๋คใ
|
|
1746
|
+
if (!events) {
|
|
1747
|
+
inst.dirtyItems = undefined
|
|
1748
|
+
return
|
|
1749
|
+
}
|
|
1750
|
+
for (const e of events) {
|
|
1751
|
+
const touched = touchedItemKeys(e)
|
|
1752
|
+
if (!touched) {
|
|
1753
|
+
inst.dirtyItems = undefined
|
|
1754
|
+
return
|
|
1755
|
+
}
|
|
1756
|
+
for (const epc of touched) inst.dirtyItems.add(epc)
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
/** ๋ธ๋ก๋์บ์คํ
๋ณํฉ ์ฃผ๊ธฐ(ms) โ ๋ธ๋ก๋์บ์คํ
๋ฅ ์ํ. ์ธ์ ์คํธ๊ฐ ์๋ฌด๋ฆฌ ๋นจ๋ผ๋ ์ด ์ฃผ๊ธฐ๋ก๋ง ๋ธ๋ก๋์บ์คํ
. */
|
|
1169
1761
|
static BROADCAST_COALESCE_MS = 200
|
|
1762
|
+
/**
|
|
1763
|
+
* โโ ๋ธ๋ก๋์บ์คํ
์ฃผ๊ธฐ๋ **์ฌ ๋ณธ ๋น์ฉ์ ๋ง์ถ๋ค** (2026-08-21 ์ค์ธก) โโโโโโโโโโโโโโโโโโโโ
|
|
1764
|
+
* ํ ๋ฒ์ ๋ธ๋ก๋์บ์คํ
์ ์ํ ํฌ๊ธฐ์ ๋น๋กํ๋ค(์ค์ธก: ๋ฌผํ 2,400 ๊ฐ์ธ ํธ์ ํ๋๊ฐ 4.5ms โ ์ํ ํฌ์ 1.7ms,
|
|
1765
|
+
* payload ๋ง๋ค๊ธฐ 1.8ms, ์๊ทธ๋์ฒ 1.0ms). ํธ์์ด ์ค๋ฌด ๊ฐ๋ฉด 200ms ๋ง๋ค 90ms ๊ฐ ๋ธ๋ก๋์บ์คํ
์ ๋ค์ด๊ฐ๊ณ ,
|
|
1766
|
+
* ๊ทธ ์๊ฐ์๋ HTTP ๋ ๊ตฌ๋
๋ ์์ง ๋ชปํ๋ค.
|
|
1767
|
+
*
|
|
1768
|
+
* ๊ทธ๋์ ํ ์ฐฝ์์ ๋ธ๋ก๋์บ์คํ
์ ์ด ์๊ฐ์ด ์ฃผ๊ธฐ์ ์ผ์ ๋ชซ์ ๋์ผ๋ฉด **์ฃผ๊ธฐ๋ฅผ ๋๋ฆฐ๋ค**. ํ๋ฉด์ ์กฐ๊ธ ๋ฆ๊ฒ
|
|
1769
|
+
* ๊ฐฑ์ ๋๊ณ (์ต์ -์ํ ์ฑ๋์ด๋ผ ๊ฐ์ ๋ง์ง๋ง ๊ฒ ํ๋๋ฟ์ด๋ฏ๋ก ๋ด์ฉ์ ๊ฐ๋ค), ๋๋ ธ๋ค๋ ๊ฒ์ ๊ณ๊ธฐํ์ด
|
|
1770
|
+
* ๋งํ๋ค(`broadcastCoalesceMs`). ์ฌ์ ๊ฐ ์๊ธฐ๋ฉด ์๋ ์ฃผ๊ธฐ๋ก ๋๋๋ฆฐ๋ค.
|
|
1771
|
+
*
|
|
1772
|
+
* ์ด๊ฒ์ ๋ธ๋ก๋์บ์คํ
๋น์ฉ์ **์ค์ด๋ ๊ฒ์ด ์๋๋ค** โ ๋น์ฉ์ ์ค์ด๋ ๊ฒ์ ๋ณ๊ฒฝ๋ถ๋ง ๋ง๋๋ ์ผ์ด๊ณ ๊ทธ๊ฒ์ ๋ณ
|
|
1773
|
+
* ์์
์ด๋ค. ์ฌ๊ธฐ์๋ ๊ทธ๋๊น์ง ํธ์คํธ๊ฐ ๊ตถ์ง ์๊ฒ ์ํ์ ๋๋ค.
|
|
1774
|
+
*/
|
|
1775
|
+
static BROADCAST_MAX_COALESCE_MS = 1000
|
|
1776
|
+
/** ์ฃผ๊ธฐ์ ๋ช ๋ชซ๊น์ง ๋ธ๋ก๋์บ์คํ
์ ์จ๋ ๋๋๊ฐ โ ๋์ผ๋ฉด ์ฃผ๊ธฐ๋ฅผ ๋๋ฆฐ๋ค(์ ๋ฐ์ด๋ฉด ๋๋จธ์ง ์ ๋ฐ์ ๋จ๊ธด๋ค). */
|
|
1777
|
+
static BROADCAST_LOAD_RATIO = 0.3
|
|
1778
|
+
/** ์ง๊ธ ์ฐ๊ณ ์๋ ์ฃผ๊ธฐ(ms) โ ๊ณ๊ธฐํ์ด ์ด ๊ฐ์ ์ฝ๋๋ค. ๋์ด๋ ์ฑ๋ก ์์ผ๋ฉด ๊ทธ๊ฒ์ด ์ฌ์ค์ด๋ค. */
|
|
1779
|
+
static broadcastPeriodMs = 200
|
|
1780
|
+
/** ์ฃผ๊ธฐ๋ฅผ ๋๋ฆฐ ํ์ โ ์กฐ์ฉํ ๋ฆ์ถ์ง ์๋๋ค. */
|
|
1781
|
+
static broadcastBackoffs = 0
|
|
1170
1782
|
private static broadcastTimer?: any
|
|
1171
1783
|
|
|
1172
|
-
/** live
|
|
1784
|
+
/** live ๋ธ๋ก๋์บ์คํ
coalescer โ dirty ์ธ live ์ธ์คํด์ค๋ง ์ฃผ๊ธฐ์ ์ผ๋ก 1ํ ๋ธ๋ก๋์บ์คํ
(entitySigs ๋ก ๋ณ๊ฒฝ ์ํฐํฐ๋ง). */
|
|
1173
1785
|
private static ensureBroadcastCoalescer(): void {
|
|
1174
1786
|
if (this.broadcastTimer) return
|
|
1175
|
-
this.
|
|
1787
|
+
this.broadcastPeriodMs = this.BROADCAST_COALESCE_MS
|
|
1788
|
+
this.broadcastTimer = setInterval(() => this.flushLiveBroadcasts(), this.broadcastPeriodMs)
|
|
1176
1789
|
if (typeof this.broadcastTimer.unref === 'function') this.broadcastTimer.unref() // ์ข
๋ฃ ๋น์ฐจ๋จ
|
|
1177
1790
|
}
|
|
1178
1791
|
|
|
1179
1792
|
/**
|
|
1180
|
-
*
|
|
1793
|
+
* ๋ธ๋ก๋์บ์คํ
์ ์ด ์๊ฐ์ ๋ณด๊ณ ์ฃผ๊ธฐ๋ฅผ ์ ํ๋ค โ **๋๋ฆฌ๋ ๊ฒ๋ ์ค์ด๋ ๊ฒ๋ ๊ฐ์ ๊ทผ๊ฑฐํ๋ค.**
|
|
1794
|
+
*
|
|
1795
|
+
* ํ ๋ฒ์ flush ๊ฐ ์ฃผ๊ธฐ์ `BROADCAST_LOAD_RATIO` ๋ฅผ ๋๊ฒ ์ฐ๋ฉด ์ฃผ๊ธฐ๋ฅผ ๋ ๋ฐฐ๋ก(์ํ๊น์ง), ๊ทธ ๋ชซ์
|
|
1796
|
+
* ์ ๋ฐ ์๋๋ก ๋ด๋ ค์ค๋ฉด ์ ๋ฐ์ผ๋ก(์๋ ์ฃผ๊ธฐ๊น์ง) ๋๋๋ฆฐ๋ค. ๋ฌธํฑ์ ๋ ๊ฐ ๋๋ ์ด์ ๋ ํ๋๋ฉด ๊ฒฝ๊ณ์์
|
|
1797
|
+
* ๋๋ฆฌ๊ณ ์ค์ด๊ธฐ๋ฅผ ๋ฐ๋ณตํ๊ธฐ ๋๋ฌธ์ด๋ค.
|
|
1798
|
+
*/
|
|
1799
|
+
private static adjustBroadcastPeriod(flushMs: number): void {
|
|
1800
|
+
const period = this.broadcastPeriodMs
|
|
1801
|
+
const high = period * this.BROADCAST_LOAD_RATIO
|
|
1802
|
+
const low = high / 2
|
|
1803
|
+
let next = period
|
|
1804
|
+
if (flushMs > high && period < this.BROADCAST_MAX_COALESCE_MS) {
|
|
1805
|
+
next = Math.min(this.BROADCAST_MAX_COALESCE_MS, period * 2)
|
|
1806
|
+
this.broadcastBackoffs++
|
|
1807
|
+
twinWarn(
|
|
1808
|
+
`[twin-engine] broadcast period ${period}ms โ ${next}ms โ one flush took ${Math.round(flushMs)}ms across ${Object.keys(this.instances).length} twin(s)`
|
|
1809
|
+
)
|
|
1810
|
+
} else if (flushMs < low && period > this.BROADCAST_COALESCE_MS) {
|
|
1811
|
+
next = Math.max(this.BROADCAST_COALESCE_MS, Math.round(period / 2))
|
|
1812
|
+
}
|
|
1813
|
+
if (next === period) return
|
|
1814
|
+
|
|
1815
|
+
this.broadcastPeriodMs = next
|
|
1816
|
+
if (this.broadcastTimer) clearInterval(this.broadcastTimer)
|
|
1817
|
+
this.broadcastTimer = setInterval(() => this.flushLiveBroadcasts(), next)
|
|
1818
|
+
if (typeof this.broadcastTimer.unref === 'function') this.broadcastTimer.unref()
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
/**
|
|
1822
|
+
* dirty ์ธ์คํด์ค ๋ธ๋ก๋์บ์คํ
flush(์ฃผ๊ธฐ tick ๋๋ ๋ช
์ ํธ์ถ) โ **์๋ฎฌ๊ณผ ๋ผ์ด๋ธ ๋ ๋ค.**
|
|
1181
1823
|
*
|
|
1182
|
-
* ์์ ์๋ ๋ผ์ด๋ธ๋ง ๋ดค๋ค(`mode !== 'live'` ๋ฉด ๊ฑด๋๋). ์๋ฎฌ์ delta ๋ง๋ค ๊ณง๋ฐ๋ก
|
|
1183
|
-
* ํ tick ์์ ์๋ฐฑ ๋ฒ ๋ฐ๋ณต๋๋ฉฐ ์ด๋ฒคํธ ๋ฃจํ๋ฅผ ๋ง์๋ค(์ `start()` ์ฃผ์์ 35์ด ํฑ).
|
|
1824
|
+
* ์์ ์๋ ๋ผ์ด๋ธ๋ง ๋ดค๋ค(`mode !== 'live'` ๋ฉด ๊ฑด๋๋). ์๋ฎฌ์ delta ๋ง๋ค ๊ณง๋ฐ๋ก ๋ธ๋ก๋์บ์คํ
ํ๊ณ , ๊ทธ๊ฒ์ด
|
|
1825
|
+
* ํ tick ์์ ์๋ฐฑ ๋ฒ ๋ฐ๋ณต๋๋ฉฐ ์ด๋ฒคํธ ๋ฃจํ๋ฅผ ๋ง์๋ค(์ `start()` ์ฃผ์์ 35์ด ํฑ). ๋ธ๋ก๋์บ์คํ
์ ๋ชจ์ผ๋
|
|
1184
1826
|
* ๊ท์จ์ ๋ชจ๋์ ์ฑ์ง์ด ์๋๋ผ **์ฑ๋์ ์ฑ์ง**์ด๋ค โ ์ต์ -์ํ ์ฑ๋์ด๋ฉด ์ค๊ฐ ์ํ๋ ๋ณด๋ผ ๊ฐ์ด ์๋ค.
|
|
1185
1827
|
*/
|
|
1186
1828
|
static flushLiveBroadcasts(): void {
|
|
1187
1829
|
const now = Date.now()
|
|
1830
|
+
const flushStart = performance.now()
|
|
1188
1831
|
for (const inst of Object.values(this.instances)) {
|
|
1189
1832
|
const isLive = inst.mode === 'live'
|
|
1190
|
-
|
|
1191
|
-
|
|
1833
|
+
/*
|
|
1834
|
+
* ์ฒ๋ฆฌ๋ ๊ณ์ธก(โฃ-1) โ ์ฐฝ(โฅ1s)๋ง๋ค ์ ์
ยท๋ธ๋ก๋์บ์คํ
ยท์ ๋๋ฅ ๊ฐฑ์ . dirty ๋ฌด๊ด(์ ํด๋ฉด 0์ผ๋ก ์๋ ด).
|
|
1835
|
+
*
|
|
1836
|
+
* **๋ ๊ตฌ๋์ ํจ๊ป ์ผ๋ค** (2026-08-20). ์์ ์๋ `isLive ? โฆ : undefined` ๋ก ์๋ฎฌ์ ์๋ผ ๋๋ค.
|
|
1837
|
+
* ๊ทธ๋์ ๋๋ ํธ์ ๋๋ถ๋ถ์ด ์๋ฎฌ์ธ ์๋ฒ์์ ใ์ด๋น ๋ช ํ์ ์ฐ๋ใ์ ๋ตํ ์๋ฆฌ๊ฐ ์์๋ค โ ์ ๋ ์ฐ๊ธฐ๋ฅผ
|
|
1838
|
+
* ๋ฐฐ์น๋ก ๊ณ ์น ๋ค์๋ ๊ทธ ํจ๊ณผ๋ฅผ ์ซ์๋ก ๋ณผ ์ ์์๋ค. ์ ์
(`ingestRate`)์ ์๋ฎฌ์์ 0 ์ผ๋ก ์๋ ดํ๊ณ ,
|
|
1839
|
+
* ๊ทธ๊ฒ์ด ์ฌ์ค์ด๋ค(์๋ณธ์์ ๋ฐ๋ ๊ฒ์ด ์๋ค).
|
|
1840
|
+
*/
|
|
1841
|
+
const m = inst.metrics
|
|
1192
1842
|
if (m) {
|
|
1193
1843
|
const dt = (now - m._windowStartMs) / 1000
|
|
1194
1844
|
if (dt >= 1) {
|
|
@@ -1200,26 +1850,23 @@ export class TwinEngine {
|
|
|
1200
1850
|
}
|
|
1201
1851
|
if (!inst.dirty) continue
|
|
1202
1852
|
inst.dirty = false
|
|
1203
|
-
|
|
1853
|
+
/* ์ฃผ๊ธฐ๋ง๋ค ํ ๋ฒ์ ๋ฒ์๋ฅผ ๋ฒ๋ฆฌ๊ณ ์ ๋ถ ๋ง๋ ๋ค โ ์ฌ๊ฑด ์์ด ๊ฐ์ด ๋ฐ๋๋ ์๋ฆฌ์ ๋ํ ๊ทธ๋ฌผ. */
|
|
1854
|
+
const left = (inst.fullBroadcastCountdown ?? 0) - 1
|
|
1855
|
+
if (left <= 0) {
|
|
1856
|
+
inst.fullBroadcastDue = true
|
|
1857
|
+
inst.fullBroadcastCountdown = this.FULL_BROADCAST_EVERY
|
|
1858
|
+
} else {
|
|
1859
|
+
inst.fullBroadcastCountdown = left
|
|
1860
|
+
}
|
|
1861
|
+
// โ ์ํฐํฐ data(tag) ๋ธ๋ก๋์บ์คํ
โ ๋ณด๋ ์ปดํฌ๋ํธ ๋ผ์ด๋ธ ๋ ๋.
|
|
1204
1862
|
this.publishEntityData(inst)
|
|
1205
1863
|
if (m) { m.broadcastTotal++; m._accBroadcast++ }
|
|
1206
|
-
/*
|
|
1207
|
-
|
|
1864
|
+
/* โข ์ ๋ ๋ฐฐ์น ๊ธฐ๋ก โ **๋ ๊ตฌ๋์ด ๊ฐ์ ๋ฌธ์ ์ด๋ค**(์๋ฎฌ๋ ์ฌ๊ธฐ์ ํ๋ฆฐ๋ค, ยง7.1). */
|
|
1865
|
+
this.flushJournal(inst)
|
|
1866
|
+
/* ์ฌ๊ธฐ์๋ถํฐ๋ **๋ผ์ด๋ธ๋ง**์ด๋ค: ์๋ฎฌ์ state ์ฑ๋์ ์๊ธฐ ์ฝ๋ฐฑ์ด ๋ธํ๋ง๋ค ๋ณด๋ด๋ฏ๋ก(๋ฆฌ๋น์ ์ด ์ปค๋์
|
|
1867
|
+
๊ฒ์ด๋ค) ์ฌ๊ธฐ์ ๋ ๋ณด๋ด๋ฉด ๊ฐ์ ์ ํธ๊ฐ ๋ ๋ฒ ๊ฐ๋ค. ์ ๋์ ์์์ ์ด๋ฏธ ๋ ๊ตฌ๋ ๋ชซ์ ํ๋ ธ๋ค. */
|
|
1208
1868
|
if (!isLive) continue
|
|
1209
|
-
//
|
|
1210
|
-
// revision ์นด์ดํฐ๋ ์ธ๋ฉ๋ชจ๋ฆฌ(startLive ์์ ์ ๋ high-water ๋ก 1ํ ์๋) โ tick ๋ง๋ค DB ์ง์ ์์.
|
|
1211
|
-
const batch = inst.pendingJournal
|
|
1212
|
-
if (batch?.length && inst.revision != null) {
|
|
1213
|
-
inst.pendingJournal = []
|
|
1214
|
-
const start = inst.revision
|
|
1215
|
-
inst.revision = start + batch.length
|
|
1216
|
-
if (m) { m.journaledTotal += batch.length; m._accJournal += batch.length; m.backlog = batch.length }
|
|
1217
|
-
const tJournal = performance.now()
|
|
1218
|
-
this.persistBatch(inst.domainId, inst.id, batch, start)
|
|
1219
|
-
.then(() => recordPhase(inst.load ?? (inst.load = newLoadMeter()), 'journal', performance.now() - tJournal))
|
|
1220
|
-
.catch(err => console.error('twin live journal fail', err))
|
|
1221
|
-
}
|
|
1222
|
-
// โก State ์ฑ๋ ๋ฐฉ์ก โ "๋ฐ๋์๋ค"๋ ๊ฐ๋ฒผ์ด ์ ํธ๋ง(kind+revision). ๋งต ๊ตฌ๋
(subscribeTwinState)์ ์ด ์ ํธ์
|
|
1869
|
+
// โก State ์ฑ๋ ๋ธ๋ก๋์บ์คํ
โ "๋ฐ๋์๋ค"๋ ๊ฐ๋ฒผ์ด ์ ํธ๋ง(kind+revision). ๋งต ๊ตฌ๋
(subscribeTwinState)์ ์ด ์ ํธ์
|
|
1223
1870
|
// scheduleRefresh(250ms ๋๋ฐ์ด์ค)โpollLive ๋ก ๋๋ฌผ์ด๋ด. ์ค๋
์ท(O(state))์ ๋ณด๋ ์ฌ๋์ด ๋ฌผ์ ๋๋ง 1ํ ๊ณ์ฐ.
|
|
1224
1871
|
// (์ฌ๊ธฐ์ payload ๋ก ์ค๋
์ท์ ์ค์ผ๋ฉด ์๋ฌด๋ ์ ์ฝ๋๋ฐ tick ๋ง๋ค ํต์งธ๋ก ๋ ์ ์์ ๋ญ๋น โ ์ ํธ๋ง ๋ณด๋ธ๋ค.)
|
|
1225
1872
|
this.publishGuarded(
|
|
@@ -1228,12 +1875,16 @@ export class TwinEngine {
|
|
|
1228
1875
|
`twin-state:${inst.id}`
|
|
1229
1876
|
)
|
|
1230
1877
|
}
|
|
1231
|
-
/*
|
|
1232
|
-
์๋ฎฌ๋ ์ด flush ์ ๊ธฐ๋๋ฏ๋ก ๊ทธ ์กฐ๊ฑด์ด๋ฉด ์๋ฎฌ
|
|
1878
|
+
/* ๊ฐ๋ ์ค์ธ ์ธ์คํด์ค๊ฐ ํ๋๋ ์์ผ๋ฉด tick ์ ๋ฉ์ถ๋ค(์์ ์ ใ๋ผ์ด๋ธ๊ฐ ์์ผ๋ฉดใ์ด์๋๋ฐ, ์ด์
|
|
1879
|
+
์๋ฎฌ๋ ์ด flush ์ ๊ธฐ๋๋ฏ๋ก ๊ทธ ์กฐ๊ฑด์ด๋ฉด ์๋ฎฌ ๋ธ๋ก๋์บ์คํ
์ด ๋ฉ์ถ ์ฑ ๋จ๋๋ค). */
|
|
1233
1880
|
if (!Object.keys(this.instances).length && this.broadcastTimer) {
|
|
1234
1881
|
clearInterval(this.broadcastTimer)
|
|
1235
1882
|
this.broadcastTimer = undefined
|
|
1883
|
+
this.broadcastPeriodMs = this.BROADCAST_COALESCE_MS
|
|
1884
|
+
return
|
|
1236
1885
|
}
|
|
1886
|
+
/* ์ด๋ฒ flush ๊ฐ ์ผ๋ง๋ฅผ ์ผ๋์ง๋ก ๋ค์ ์ฃผ๊ธฐ๋ฅผ ์ ํ๋ค โ ๋ธ๋ก๋์บ์คํ
์ด ๋ฃจํ๋ฅผ ๋ค ์ฐ์ง ๋ชปํ๊ฒ. */
|
|
1887
|
+
this.adjustBroadcastPeriod(performance.now() - flushStart)
|
|
1237
1888
|
}
|
|
1238
1889
|
|
|
1239
1890
|
/**
|
|
@@ -1273,22 +1924,134 @@ export class TwinEngine {
|
|
|
1273
1924
|
* ์์ผ๋ฉด `undefined` ๋ค โ 0 ์ด ์๋๋ค. ๊ตฌ์กฐ ๋ฆฌ๋น์ ์ด ์๊ธฐ๊ธฐ ์ ์ ๋ง๋ค์ด์ง ํธ์์ ์์ง ๋ฆฌ๋น์ ์ด
|
|
1274
1925
|
* ์๊ณ , ๊ทธ ์ฌ์ค์ 0 ์ด๋ผ๋ **์ ํจํด ๋ณด์ด๋ ๋ฒํธ**๋ก ์์ฅํ๋ฉด ์ ๋๋ค.
|
|
1275
1926
|
*/
|
|
1276
|
-
|
|
1927
|
+
/** ๊ตฌ์กฐ ๋ฆฌ๋น์ ์บ์ โ `null` ์ **์๋ค๋ ๊ฒ์ ์๊ณ ์๋ค**๋ ๋ป์ด๋ค(๋ชจ๋ฆ๊ณผ ๊ตฌ๋ณํ๋ค, ยง7.1). */
|
|
1928
|
+
private static structureRevCache: Record<string, number | null> = {}
|
|
1277
1929
|
static async structureRevOf(domainId: string, instanceId: string): Promise<number | undefined> {
|
|
1278
1930
|
const key = runtimeKey(domainId, instanceId)
|
|
1279
1931
|
const cached = this.structureRevCache[key]
|
|
1280
|
-
|
|
1932
|
+
/*
|
|
1933
|
+
* **์๋ค๋ ๊ฒ๋ ๋ต์ด๋ค** โ ์์ ์๋ ์ฐพ์ง ๋ชปํ๋ฉด ์บ์ํ์ง ์์๋ค(`if (latest) โฆ`). ๊ทธ๋์ ๊ตฌ์กฐ ํ์ด
|
|
1934
|
+
* ์๋ ํธ์์ **๋ถ๋ฅผ ๋๋ง๋ค SELECT** ํ๊ณ , ๊ทธ ๊ฒฝ๋ก๊ฐ ๋ธํ๋ง๋ค ๋ถ๋ฆฌ๊ณ ์์๋ค(ยง7.1). `null` ๋ก ๊ธฐ์ตํ๋ค.
|
|
1935
|
+
*/
|
|
1936
|
+
if (cached !== undefined) return cached === null ? undefined : cached
|
|
1281
1937
|
const latest = await getRepository(TwinStructure).findOne({ where: { domain: { id: domainId }, instanceId }, order: { rev: 'DESC' } })
|
|
1282
|
-
|
|
1938
|
+
this.structureRevCache[key] = latest ? latest.rev : null
|
|
1283
1939
|
return latest?.rev
|
|
1284
1940
|
}
|
|
1285
1941
|
|
|
1942
|
+
/** ๊ณ๊ธฐ ํ ๋ฒ โ **๋ ๊ตฌ๋์ด ๊ฐ์ ๊ฒ์ ๋ ๋ค**(ํ์ชฝ๋ง ๋ค๋ฉด ๊ทธ ๊ตฌ๋์ ๋ฌผ์ด๋ ๋ต์ด ์๋ค). */
|
|
1943
|
+
private static newMetrics(): TwinMetrics {
|
|
1944
|
+
return { ingestedTotal: 0, broadcastTotal: 0, journaledTotal: 0, ingestRate: 0, broadcastRate: 0, journalRate: 0, backlog: 0, _accIngest: 0, _accBroadcast: 0, _accJournal: 0, _windowStartMs: Date.now() }
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
/**
|
|
1948
|
+
* ๋ชจ์ ๋ ์ ๋์ **ํ ๋ฒ์** ์ด๋ค โ ๋ ๊ตฌ๋์ด ๊ฐ์ ๋ฌธ์ ์ด๋ค (ยง7.1).
|
|
1949
|
+
*
|
|
1950
|
+
* โโ ์ ํ ํจ์์ธ๊ฐ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
1951
|
+
* ์ฐ๋ ์๋ฆฌ๊ฐ ๋์ด๋ฉด(์ฃผ๊ธฐ flush ยท ์ ์ง) ํ์ชฝ๋ง ๊ณ ์ณ์ง๊ณ , ๊ทธ ์ด๊ธ๋จ์ **์ฌ์ค์ด ์กฐ์ฉํ ์ฌ๋ผ์ง๋**
|
|
1952
|
+
* ๋ชจ์์ผ๋ก ๋ํ๋๋ค. ๊ทธ๋์ ํ๋ฆฌ๋ ๊ท์น์ ์ฌ๊ธฐ ํ ๊ณณ์ ๋๋ค.
|
|
1953
|
+
*
|
|
1954
|
+
* โโ ๋ฆฌ๋น์ ์ ๋๊ฐ ๋งค๊ธฐ๋ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
1955
|
+
* ยท ๋ผ์ด๋ธ โ ์์ฒ์ ๋ฆฌ๋น์ ์ ์ฃผ์ง ์์ผ๋ฏ๋ก **ํธ์คํธ๊ฐ** ์ด์ด ๋ถ์ธ๋ค(์ ๋ high-water ์์ ์๋).
|
|
1956
|
+
* ยท ์๋ฎฌ โ **์ปค๋์ ๋ฆฌ๋น์ **์ด ์ค๋ ค ์จ๋ค(์ ๋์ด ๊ทธ๊ฒ์ผ๋ก ์ ํ๊ณ ์๊ฐ์ฌํ์ด ๊ทธ๊ฒ์ ๋๋๋ค).
|
|
1957
|
+
* ๊ทธ๋์ ๋ฒํผ๋ ๋ ๋ชจ์์ ํจ๊ป ๋ ๋ค: ๋ดํฌ๋ง ์์ผ๋ฉด ๋ผ์ด๋ธ, `{ event, revision }` ์ด๋ฉด ์๋ฎฌ์ด๋ค.
|
|
1958
|
+
*/
|
|
1959
|
+
private static flushJournal(inst: InstanceRuntime): Promise<void> {
|
|
1960
|
+
const batch = inst.pendingJournal
|
|
1961
|
+
if (!batch?.length) return Promise.resolve()
|
|
1962
|
+
inst.pendingJournal = []
|
|
1963
|
+
const m = inst.metrics
|
|
1964
|
+
/* ์ด ๊ฑด์๋ ๋์ ์, **์ง๊ธ ๋จ์ ๊ฒ**์ backlog ์. ์ด ์๋ฆฌ์์ ๋ฐฐ์น ํฌ๊ธฐ๋ฅผ backlog ๋ก ์ ์ผ๋ฉด
|
|
1965
|
+
์ ์์ ์ธ ๋ฐฐ์น ์ ์ฅ์ด ํ๋ฉด์์ ๊ฒฝ๊ณ ๋ก ๋ณด์ธ๋ค(2026-08-21 ๊ต์ ). */
|
|
1966
|
+
if (m) { m.journaledTotal += batch.length; m._accJournal += batch.length; m.backlog = inst.pendingJournal?.length ?? 0 }
|
|
1967
|
+
|
|
1968
|
+
/* ์๋ฎฌ์ ์๊ธฐ ๋ฆฌ๋น์ ์ ๋ค๊ณ ์จ๋ค โ ๊ทธ๋๋ก ์ด๋ค. ๋ผ์ด๋ธ๋ ์ฌ๊ธฐ์ ์ด์ด ๋ถ์ธ๋ค. */
|
|
1969
|
+
const carried = batch.filter((b: any) => b && typeof b === 'object' && 'event' in b && typeof b.revision === 'number')
|
|
1970
|
+
const plain = batch.filter((b: any) => !(b && typeof b === 'object' && 'event' in b && typeof b.revision === 'number'))
|
|
1971
|
+
|
|
1972
|
+
const tJournal = performance.now()
|
|
1973
|
+
const done = () => recordPhase(inst.load ?? (inst.load = newLoadMeter()), 'journal', performance.now() - tJournal)
|
|
1974
|
+
const jobs: Promise<void>[] = []
|
|
1975
|
+
|
|
1976
|
+
if (carried.length) jobs.push(this.persistCarried(inst.domainId, inst.id, carried))
|
|
1977
|
+
if (plain.length && inst.revision != null) {
|
|
1978
|
+
const start = inst.revision
|
|
1979
|
+
inst.revision = start + plain.length
|
|
1980
|
+
jobs.push(this.persistBatch(inst.domainId, inst.id, plain, start))
|
|
1981
|
+
}
|
|
1982
|
+
if (!jobs.length) return Promise.resolve()
|
|
1983
|
+
/*
|
|
1984
|
+
* **์ฝ์์ ๋๋ ค์ค๋ค** โ ์ฃผ๊ธฐ flush ๋ ๊ธฐ๋ค๋ฆฌ์ง ์์ง๋ง(ํซ ๊ฒฝ๋ก) **์ ์ง๋ ๊ธฐ๋ค๋ ค์ผ ํ๋ค.** ๊ธฐ๋ค๋ฆฌ์ง
|
|
1985
|
+
* ์์ผ๋ฉด ๊ณง๋ฐ๋ก ์ด์ด์ง๋ ์ ๋ ์ฌ์์ด ์์ง ์ ์ฐ์ธ ๊ตฌ๊ฐ์ ๋ชป ๋ณด๊ณ , ์์คํํธ๊ฐ ใ์๋ ์ผใ๋ก ์์ํ๋ค
|
|
1986
|
+
* (์ค์ ๋ก ๊ทธ๋ ๊ฒ ๊นจ์ก๋ค: ํ๋ณด๋ถ์ ๋ ์ค๋๊ฐ ๋์ด์๋์ง ๋ชปํ๋ค).
|
|
1987
|
+
*/
|
|
1988
|
+
const written = carried.length + plain.length
|
|
1989
|
+
return Promise.all(jobs)
|
|
1990
|
+
.then(() => {
|
|
1991
|
+
done()
|
|
1992
|
+
/*
|
|
1993
|
+
* **์ ํ ๋ค์ ์ ๋๋ค** โ ์คํจํ ๋ฐฐ์น๋ฅผ ใ๋จ์๋คใ๊ณ ์ธ๋ฉด ์ถ์ด๊ฐ ์๋ ์ฌ์ค์ ์๋ค๊ณ ๋งํ๋ค.
|
|
1994
|
+
* ์ฐฝ(10๋ถ)์ ์์ด๋ฏ๋ก ์ฌ๊ธฐ๋ ๋ค์๋ ใ์ง๋ ์ฌ์ฏ ์๊ฐ ์ผ๋ง๋ ์ ์๋ใ๋ฅผ ๋ตํ ์ ์๋ค.
|
|
1995
|
+
*/
|
|
1996
|
+
this.recordJournalWrite(inst.domainId, inst.id, written)
|
|
1997
|
+
})
|
|
1998
|
+
.catch(err => twinError('twin journal flush fail', err))
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
/**
|
|
2002
|
+
* ์ปค๋์ด ๋งค๊ธด ๋ฆฌ๋น์ ์ ๊ทธ๋๋ก ๋ค๊ณ ๋ฒํฌ ์ ์ฅ โ ์๋ฎฌ ๊ฒฝ๋ก.
|
|
2003
|
+
*
|
|
2004
|
+
* ์์ ์๋ ์ด ๊ฒฝ๋ก๊ฐ **๋ธํ๋ง๋ค ํ ํ์ฉ** ์ ์ฅํ๋ค(๊ทธ๋ฆฌ๊ณ ํ๋ง๋ค ๊ตฌ์กฐ ๋ฆฌ๋น์ ์ ๋ฌผ์๋ค). ๊ท๋ชจ์์ ๊ทธ๊ฒ์ด
|
|
2005
|
+
* ํธ์คํธ๋ฅผ ๋จน์๋ค(ยง7.1 ์ค์ธก). ์ฌ๊ธฐ์ ๊ตฌ์กฐ ๋ฆฌ๋น์ ์ **ํ ๋ฒ๋ง** ๋ฌป๋๋ค.
|
|
2006
|
+
*/
|
|
2007
|
+
static async persistCarried(domainId: string, instanceId: string, items: { event: any; revision: number }[]): Promise<void> {
|
|
2008
|
+
const repo = getRepository(TwinEvent)
|
|
2009
|
+
const structureRev = await this.structureRevOf(domainId, instanceId)
|
|
2010
|
+
const rows = items.map(it => this.journalRow(repo, domainId, instanceId, it.event, it.revision, structureRev))
|
|
2011
|
+
await this.insertRows(repo, rows)
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
/**
|
|
2015
|
+
* ์ ๋ ํ์ ๋ฃ๋๋ค โ **๋ฃ๊ธฐ๋ง ํ๋ค**(2026-08-20).
|
|
2016
|
+
*
|
|
2017
|
+
* ์์ ์๋ `save()` ์๋ค. ๊ทธ๋ฐ๋ฐ `save` ๋ ๋ฃ์ ๋ค ์์ฑ ์ปฌ๋ผ์ ์ฝ์ผ๋ ค๊ณ **ํ๋ง๋ค SELECT ๋ฅผ ํ ๋ฒ ๋**
|
|
2018
|
+
* ํ๋ค(์ํ ๋ก๊ทธ์์ ๊ทธ ์ง์๊ฐ ๊ทธ๋๋ก ๋ณด์๋ค: `SELECT โฆ FROM twin_events WHERE id = ?`). ์ ๋์
|
|
2019
|
+
* append-only ์ด๊ณ ๋ถ๋ฅด๋ ์ชฝ์ ๋๋ ค๋ฐ์ ์ํฐํฐ๋ฅผ ์ฐ์ง ์์ผ๋ฏ๋ก ๊ทธ ์๋ณต์ด ์์ ๋ญ๋น๋ค.
|
|
2020
|
+
*
|
|
2021
|
+
* ๋ฌถ์์ **๋๋ ์** ๋ฃ๋๋ค: ํ ๋ฌธ์ ์ด์ด ์ด๋ค์ฏ์ธ ํ์ ์์ฒ ๊ฐ ์ค์ผ๋ฉด ๋๋ผ์ด๋ฒ์ ํ๋ผ๋ฏธํฐ ํ๊ณ์
|
|
2022
|
+
* ๊ฑธ๋ฆฐ๋ค(pg ๋ 65,535๊ฐ). 500ํ์ด๋ฉด ์ด๋ ๋๋ผ์ด๋ฒ์์๋ ์์ ํ๋ค.
|
|
2023
|
+
*/
|
|
2024
|
+
private static async insertRows(repo: any, rows: any[]): Promise<void> {
|
|
2025
|
+
const CHUNK = 500
|
|
2026
|
+
/*
|
|
2027
|
+
* โโ ํ ํธ๋์ญ์
์ผ๋ก ๊ฐ์ธ ๋ณด์๊ณ , **๋๋๋ ธ๋ค** (2026-08-22 ์ค์ธก) โโโโโโโโโโโโโ
|
|
2028
|
+
* ใ์ฒญํฌ๋ง๋ค ์ปค๋ฐํ๋ฉด fsync ๊ฐ ๊ทธ๋งํผ ๋์ด๋๋คใ๋ ์ด์ ๋ก ์ ์ฒด๋ฅผ ํ ํธ๋์ญ์
์ ๊ฐ์๋ค. ์ฐ๊ธฐ ์์ฒด๋
|
|
2029
|
+
* ์ค์ ๋ก ๋นจ๋ผ์ก๋ค โ ๋๋ฆฐ ์ง์ ๋ชฉ๋ก์์ ์ด INSERT ๊ฐ ์ฌ๋ผ์ก๋ค. **๊ทธ๋ฐ๋ฐ ์๋ฒ๊ฐ ๋ ๋๋ ค์ก๋ค.**
|
|
2030
|
+
*
|
|
2031
|
+
* ๊ฐ์ธ๊ธฐ ์ ๋๋ฆฐ ์ง์ ์ต๋ 32.3์ด ยท ROLLBACK ์์
|
|
2032
|
+
* ๊ฐ์ผ ๋ค ๋๋ฆฐ ์ง์ ์ต๋ 50.2์ด ยท ROLLBACK 36.3์ด ๋ฑ์ฅ
|
|
2033
|
+
*
|
|
2034
|
+
* ๊ธฐ์ ์ ๋๋ผ์ด๋ฒ๋ค. TypeORM ์ sqlite ๋๋ผ์ด๋ฒ๋ ์ฐ๊ฒฐ์ **ํ๋**๋ง ๋ ๋ค(ํ ์์). ํธ๋์ญ์
์ด
|
|
2035
|
+
* ์ด๋ ค ์๋ ๋์ ๊ทธ ์ ์ผํ ์ฐ๊ฒฐ์ ์ด ๋ฌถ์์ ๊ฒ์ด๋ฏ๋ก, **๋ฌถ์์ด ๋๋ ๋๊น์ง ์ฑ์ ๋ชจ๋ ์ง์๊ฐ
|
|
2036
|
+
* ๊ธฐ๋ค๋ฆฐ๋ค.** ์ฒญํฌ๋ง๋ค ์ปค๋ฐํ๋ฉด ๊ทธ ์ฌ์ด์ ๋ค๋ฅธ ์ง์๊ฐ ๋ผ์ด๋ค ์๋ฆฌ๊ฐ ์๊ธด๋ค โ fsync ๋ฅผ ๋ ์น๋ฅด๋
|
|
2037
|
+
* ๋์ **๋จธ๋ฆฌ ๋งํ์ด ์งง์์ง๋ค.**
|
|
2038
|
+
*
|
|
2039
|
+
* ์ฆ ์ฌ๊ธฐ์๋ ใ์ปค๋ฐ ์๋ฅผ ์ค์ด๋ ๊ฒใ์ด ๋ชฉ์ ์ด ์๋๋ค. ๋ชฉ์ ์ **ํ ๋ฒ์ ์ค๋ ๋ถ์ก์ง ์๋ ๊ฒ**์ด๋ค.
|
|
2040
|
+
* ์ ๋ ๋ฌถ์์ ์์์ฑ์ ๊ทธ ๋๊ฐ๋ฅผ ์น๋ฅผ ๋งํผ์ ๊ฐ์ด ์๋๋ค: ์ ๋์ append-only ์ด๊ณ ๋ฆฌ๋น์ ์ด
|
|
2041
|
+
* ์ด์ด์ง๋ฏ๋ก, ์ ๋ฐ๋ง ๋ค์ด๊ฐ ๋ฌถ์์ ๋ค์ ๊ธฐ๋์ replay ๊ฐ ๊ทธ ์ง์ ๋ถํฐ ์ด์ด๋ฐ๋๋ค.
|
|
2042
|
+
*
|
|
2043
|
+
* ์ฐ๊ฒฐ ํ์ด ์๋ ๋๋ผ์ด๋ฒ(postgresยทmysql)์์๋ ํ๋จ์ด ๋ฌ๋ผ์ง ์ ์๋ค โ ๊ทธ๋๋ ์ด ์ฃผ์์ ๊ทผ๊ฑฐ๋ก
|
|
2044
|
+
* ๋ค์ ์ฌ๊ณ ์ ํ๋ผ. **๋๋ผ์ด๋ฒ๋ง๋ค ๋ค๋ฅธ ๊ฒฐ๋ก ์ด ๋๋ ์๋ฆฌ๋ค.**
|
|
2045
|
+
*/
|
|
2046
|
+
for (let i = 0; i < rows.length; i += CHUNK) await repo.insert(rows.slice(i, i + CHUNK))
|
|
2047
|
+
}
|
|
2048
|
+
|
|
1286
2049
|
/** live ์ ๋ ๋ฐฐ์น ๊ธฐ๋ก โ ๋ชจ์๋ CanonicalEnvelope ๋ค์ startRevision+1.. ์ ๋ถ์ฌํด ๋ฒํฌ ์ ์ฅ(coalescer tick ๋น 1ํ). */
|
|
1287
2050
|
static async persistBatch(domainId: string, instanceId: string, envelopes: any[], startRevision: number): Promise<void> {
|
|
1288
2051
|
const repo = getRepository(TwinEvent)
|
|
1289
2052
|
const structureRev = await this.structureRevOf(domainId, instanceId)
|
|
1290
2053
|
const rows = envelopes.map((e, i) => this.journalRow(repo, domainId, instanceId, e, startRevision + i + 1, structureRev))
|
|
1291
|
-
await
|
|
2054
|
+
await this.insertRows(repo, rows)
|
|
1292
2055
|
}
|
|
1293
2056
|
|
|
1294
2057
|
/** ๋ ์ง์คํธ๋ฆฌ upsert(๋๋ฉ์ธ+instanceId ์ ๋ํฌ). status ์ธ์๋ก provision(stopped)/start(running) ๊ณต์ฉ. */
|
|
@@ -1320,7 +2083,7 @@ export class TwinEngine {
|
|
|
1320
2083
|
spaceId: (model as any)?.spaceId ?? existing?.spaceId,
|
|
1321
2084
|
areaId: (model as any)?.areaId ?? existing?.areaId,
|
|
1322
2085
|
/* ์ฌ๊ธฐ๋ ์ ์ฑ
(ADR-0029 ยง2) โ ๋ช
์๊ฐ ์ฐ์ , ์์ผ๋ฉด ๊ธฐ์กด๊ฐ ๋ณด์กด(์ฌํ๋ก๋น์ ์ด ์ ์ธ์ ์ง์ฐ์ง ์๊ฒ).
|
|
1323
|
-
**๋ ๋ค ์์ผ๋ฉด
|
|
2086
|
+
**๋ ๋ค ์์ผ๋ฉด ์ค๋ฅ๋ฅผ ๋ธ๋ค**: ์์ ์๋ ์ฌ๊ธฐ์ ๊ธฐ๋ณธ๊ฐ์ ๊ฐ์ธํ๋๋ฐ, ๊ทธ ๊ธฐ๋ณธ๊ฐ์ด ใ์ ๋ ์ด๊ธฐํใ๋ผ์
|
|
1324
2087
|
์ ์ธ์ ๋น ๋จ๋ฆฐ ํ๋ก๋น์ ๋์ด ์กฐ์ฉํ ์ด๋ ฅ์ ์ง์ฐ๋ ํธ์์ ๋ง๋ค์๋ค. ์ ์ธ์ ๋ถ๋ฅด๋ ์ชฝ์ ๋ชซ์ด๋ค. */
|
|
1325
2088
|
restartPolicy:
|
|
1326
2089
|
restartPolicy ??
|
|
@@ -1385,7 +2148,7 @@ export class TwinEngine {
|
|
|
1385
2148
|
private static assertNotRunning(domainId: string, instanceId: string): void {
|
|
1386
2149
|
if (!this.instances[runtimeKey(domainId, instanceId)]) return
|
|
1387
2150
|
/*
|
|
1388
|
-
* **๊ฑฐ์ ๋ ๋ฒ์ญ๋ผ์ผ ํ๋ค.** ์ด ๋ฌธ์ฅ์
|
|
2151
|
+
* **๊ฑฐ์ ๋ ๋ฒ์ญ๋ผ์ผ ํ๋ค.** ์ด ๋ฌธ์ฅ์ ์ ๋ฌ๋์ด ๋ฆฌ์กธ๋ฒ์ `catch` ๋ฅผ ์ง๋ ํ๋ฉด ํ ์คํธ์ ๊ทธ๋๋ก
|
|
1389
2152
|
* ๋ด๋ค โ ๋ค์ฏ ์ธ์ด ์ ํ์์ ์์ด ํ ์ค์ด ์ฌ์ฉ์์๊ฒ ๋ณด์๋ค(2026-08-14 ์ค์ธก).
|
|
1390
2153
|
*
|
|
1391
2154
|
* ๊ทธ๋์ ์ฝ๋์ ํ๋ผ๋ฏธํฐ๋ฅผ ์์ธ์ ์ค์ด ๋ณด๋ธ๋ค(`ImportSpaceRefusal` ๊ณผ ๊ฐ์ ๊ท์ฝ: ์์ด ๋ฌธ์ฅ์
|
|
@@ -1449,22 +2212,24 @@ export class TwinEngine {
|
|
|
1449
2212
|
* ๋๋๋ฆฌ์ง ์๋๋ค โ ๋ค๋ง ์กฐ์ฉํ ๋๊ธฐ์ง ์๊ณ ๋งํ๋ค.
|
|
1450
2213
|
*/
|
|
1451
2214
|
await projectStructure(domainId, instanceId, model, instanceId).catch((err: any) =>
|
|
1452
|
-
|
|
2215
|
+
twinWarn(`[twin-engine] "${instanceId}" adopted a new structure but its projected rows were not refreshed โ ${err?.message ?? err}`)
|
|
1453
2216
|
)
|
|
1454
2217
|
|
|
1455
2218
|
/*
|
|
1456
|
-
* **๊ตฌ์กฐ๊ฐ ๋ฐ๋ ์๊ฐ์ด ์ํ๊ฐ ๋ฐ๋ ์๊ฐ์ด๋ค** โ ๊ทธ๋ฌ๋
|
|
2219
|
+
* **๊ตฌ์กฐ๊ฐ ๋ฐ๋ ์๊ฐ์ด ์ํ๊ฐ ๋ฐ๋ ์๊ฐ์ด๋ค** โ ๊ทธ๋ฌ๋ ๋ธ๋ก๋์บ์คํ
ํ๋ค.
|
|
1457
2220
|
*
|
|
1458
2221
|
* โโ ๋ฌด์์ด ๋ฌ๋ (2026-08-18) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
1459
2222
|
* ๊ตฌ์กฐ ์ ํ์ ์ปค๋๋ง ๊ฐ๊ณ ์กฐ์ฉํ ๋๋ฌ๋ค. ๊ทธ๋ฐ๋ฐ ํ๋ฉด์ด ๋ณด๋ ๊ฒ ์๋น์๊ฐ ๊ตฌ์กฐ์์ ํ์๋๋ค โ
|
|
1460
2223
|
* ๊ณ์ฝ ๋๋น ํ์ , ์ฃผ๋ชฉ ์ ํธ, ์๋ฆฌ ์. ํ์ฅ์ด ๊ณ์ฝ์ ๊ณ ์ณ ์ ์ธํ ์๊ฐ ์กฐ๊ฑด์ด ์ฑ๋ฆฝํ๋๋ฐ๋, ์ํ
|
|
1461
|
-
*
|
|
2224
|
+
* ๋ธ๋ก๋์บ์คํ
์ด ์์ด์ ์ง๋ ๋ ์ผ์ **๋ค์ ๊ณ์ธก ํ๋ณธ์ด ์ฌ ๋๊น์ง** ์ ํ๋ฉด์ ๋ค๊ณ ์์๋ค(๊ทธ ์ฌ์ด ํค๋
|
|
1462
2225
|
* ๋ฐฐ์ง๋ 4์ด ํด๋ง์ผ๋ก ๋จผ์ ์์, ใ๋ฐฐ์ง์ ์๊ณ ๋ชฉ๋ก์ ์๋ใ ์ด๊ธ๋ ํ๋ฉด์ด ์ค์ ๋ก ๋ณด์๋ค).
|
|
1463
2226
|
*
|
|
1464
|
-
* ์
|
|
1465
|
-
* ๋๋ฌผ์ง๋ง, ์ฌ๋ฌ ํธ์์ ์๋ฌ์ ๋ค์ด์ฌ ์ ์๊ณ (ํ์ฅ ์ผ๊ด ์ ์ธ) ๊ทธ๋๋
|
|
2227
|
+
* ์ ๋ธ๋ก๋์บ์คํ
๊ฒฝ๋ก๋ฅผ ๋ง๋ค์ง ์๋๋ค: dirty ๋ฅผ ์ธ์ **์ด๋ฏธ ์๋ ๋ณํฉ ๊ท์จ**(200ms)์ ์น๋๋ค. ๊ตฌ์กฐ ์ ํ์
|
|
2228
|
+
* ๋๋ฌผ์ง๋ง, ์ฌ๋ฌ ํธ์์ ์๋ฌ์ ๋ค์ด์ฌ ์ ์๊ณ (ํ์ฅ ์ผ๊ด ์ ์ธ) ๊ทธ๋๋ ๋ธ๋ก๋์บ์คํ
๋ฅ ์ํ์ ์ง์ผ์ผ ํ๋ค.
|
|
1466
2229
|
*/
|
|
1467
2230
|
inst.dirty = true
|
|
2231
|
+
/* ๊ตฌ์กฐ๊ฐ ๊ฐ๋ ธ์ผ๋ฉด ๋ฌด์์ด ๋ฌ๋ผ์ก๋์ง ์ฌ๊ฑด์ผ๋ก ๋งํ ์ ์๋ค โ ์ ๋ถ ๋ค์ ๋ง๋ ๋ค. */
|
|
2232
|
+
this.markItemsDirty(inst)
|
|
1468
2233
|
this.ensureBroadcastCoalescer()
|
|
1469
2234
|
|
|
1470
2235
|
return { rev, ...shift }
|
|
@@ -1626,6 +2391,43 @@ export class TwinEngine {
|
|
|
1626
2391
|
const reg = await getRepository(TwinInstance).findOne({ where: { domain: { id: domainId }, instanceId } })
|
|
1627
2392
|
if (!reg?.model) throw new Error(`instance "${instanceId}" not provisioned (no model)`)
|
|
1628
2393
|
|
|
2394
|
+
/*
|
|
2395
|
+
* โโ ์ ์ธ๋ `restartPolicy` ๊ฐ **๊ธฐ๋ ๋ฐฉ์์ ์ ํ๋ค** (2026-08-20) โโโโโโโโโโ
|
|
2396
|
+
*
|
|
2397
|
+
* ์์ ์๋ ์ด ํจ์๊ฐ ์ ์ฑ
์ **์ฝ์ด์ ๋๊ธฐ๊ธฐ๋ง** ํ๊ณ , ๊ธฐ๋์ ์ธ์ ๋ ์๋ฎฌ ๊ฒฝ๋ก์๋ค. ๊ทธ๋์ ๋ฏธ๋ฌ๋ก
|
|
2398
|
+
* ์ ์ธํ ํธ์์ ํ๋ฉด์์ ์์ํ๋ฉด **์๋ฎฌ๋ก ๋ด๋ค** โ ๊ทธ ํธ์์ ์ ์
์ ๋ฐ์ง ๋ชปํ๋ค(`ingestLive` ๋
|
|
2399
|
+
* ๋ผ์ด๋ธ๊ฐ ์๋๋ฉด 0 ์ ๋๋ ค์ค๋ค). ์ค๋ฅ๋ ๋์ง ์๊ณ , ํ๋ฉด์๋ ใrunningใ์ด๋ผ ์ ํ๋ค.
|
|
2400
|
+
*
|
|
2401
|
+
* ๋ถํ
๊ฒฝ๋ก(`resumeRow`)์๋ ๊ทธ ๊ฐ๋ฆผ์ด ์์๋๋ฐ ์ฌ๊ธฐ์๋ ์์๋ค. ์ฆ **๋ถํ
์ผ๋ก ์ด์๋ ๋ฏธ๋ฌ๋
|
|
2402
|
+
* ๋ฐ๊ณ , ์ฌ๋์ด ์์ํ ๋ฏธ๋ฌ๋ ๋ฐ์ง ๋ชปํ๋ค.** ๊ฐ์ ์ ์ธ์ด ๋ ๊ฒฐ๊ณผ๋ฅผ ๋ด๋ ๊ฒ์ ๊ฒฐํจ์ด๋ค.
|
|
2403
|
+
*
|
|
2404
|
+
* ๊ฐ๋ฆผ์ ์ฌ๊ธฐ ํ ๊ณณ์ ๋๋ค โ ๋ถํ
๋ ์ด ๋ฌธ์ ์ง๋๋ค.
|
|
2405
|
+
*
|
|
2406
|
+
* โโ `reset` ์ ์ ์ธ๋๋ก **์ ๋์ ๋น์ด๋ค** (2026-08-20) โโโโโโโโโโโโโโโโโโโ
|
|
2407
|
+
* ใseed ์ฌํใ์ ๋ฐฑ์ง์์ ๋ค์ ์์ํ๋ค๋ ๋ป์ด๋ค. ์์ ์๋ ๊ทธ ์ ์ธ์ด ๊ตฌํ๋์ง ์์ `reset` ํธ์์ด
|
|
2408
|
+
* ์ฌ์ค์ ์ด์ด์ก๊ณ , ๊ทธ๋์ ใ์ด ํธ์์ ์ด๋ ฅ์ ์ ์ฌ๊ธฐ๋์ ๋๊ฒจ ๋จ์ ์๋ใ๋ฅผ ์๋ฌด๋ ์ค๋ช
ํ ์ ์์๋ค.
|
|
2409
|
+
*
|
|
2410
|
+
* ์ด๊ฒ์ **์ฌ์ค์ ์ง์ฐ๋ ๋์**์ด๋ผ ์์๋ฅผ ์ง์ผ ์ผฐ๋ค: ๋จผ์ ์ฌ๋์ด ๊ฐ ํธ์์ ์ ์ฑ
์ ๋ค์ ์ ์ธํ ๋ฌธ์
|
|
2411
|
+
* ๋ง๋ค๊ณ (`setTwinRestartPolicy`), ์ด๋ ฅ์ ๋๊ณ ๋ณผ ํธ์์ `resume` ์ผ๋ก ์ฎ๊ธด ๋ค์ ์ผ ๋ค. ๊ทธ ์์๋ฅผ
|
|
2412
|
+
* ๋ค์ง์ผ๋ฉด ์ ์ธ์ ์งํจ ๋๊ฐ๋ก ๋จ์ ์ด๋ ฅ์ ์ง์ด๋ค.
|
|
2413
|
+
*/
|
|
2414
|
+
const policy = restartPolicy ?? readRestartPolicy(reg.restartPolicy, `twin "${instanceId}"`)
|
|
2415
|
+
if (policy === 'resync') {
|
|
2416
|
+
/* ๋ฏธ๋ฌ๋ **๊ด์ธก ๊ตฌ๋**์ผ๋ก ์ธ์ด๋ค โ ์๋ฎฌ๋ก ์ธ์ฐ๋ฉด ์๋ ์์ง์์ ์ค์ค๋ก ๋ง๋ ๋ค.
|
|
2417
|
+
์๊ฐ ๊ธฐ์ค์ ๊ณต๊ฐ์ด ๊ฐ๋๋ค(๋ถํ
๊ฒฝ๋ก์ ๊ฐ์ ๊ท์น). */
|
|
2418
|
+
return this.startLive(instanceId, domainId, reg.kind, await this.withSpaceTimeBase(reg.model as TwinModelDef, domainId))
|
|
2419
|
+
}
|
|
2420
|
+
if (policy === 'reset') {
|
|
2421
|
+
/*
|
|
2422
|
+
* ์จ์ ์ฌํ โ ์ ๋์ ๋น์ฐ๊ณ ๋ฆฌ๋น์ 0 ๋ถํฐ ๋ค์ ์ผ๋ค. ๋ณต๊ตฌ๋ณธ๋ ํจ๊ป ๋ฒ๋ฆฐ๋ค(`resetJournal` ์ด ํ๋ค):
|
|
2423
|
+
* ๋จ๊ฒจ ๋๋ฉด ๋ฐฑ์ง๋ก ์์ํ ํธ์์ ์ ์ํ๊ฐ ๋์ด์๋ ใ์จ์ ์ฌํใ์ด ์๋๊ฒ ๋๋ค.
|
|
2424
|
+
*
|
|
2425
|
+
* **๋ฒค์น ์ฌ๋ณธ์ ์์ธ๋ค.** ๊ทธ๊ฒ์ ๋๊ตฐ๊ฐ ์ง์ผ๋ณด๋ ์คํ์ด๊ณ , ๊ทธ ์คํ์ ๊ธฐ๋ก์ ๊ธฐ๋์ด ์ง์ฐ๋ฉด ์คํ์ด
|
|
2426
|
+
* ์ฌ๋ผ์ง๋ค(๋ถํ
์ ๋ฒค์น๋ฅผ ๋์ด๋ฆฌ์ง๋ ์๋๋ค โ `resumeRow`).
|
|
2427
|
+
*/
|
|
2428
|
+
if (reg.purpose !== 'bench') await this.resetJournal(domainId, instanceId)
|
|
2429
|
+
}
|
|
2430
|
+
|
|
1629
2431
|
/*
|
|
1630
2432
|
* ์์คํํธ ์ฌ๋ฃ๋ฅผ **์ฌ๊ธฐ์ ํ์คํ ํ๋ณดํ๋ค.**
|
|
1631
2433
|
* `start()` ๋ ๋๊ธฐ๋ผ ์ค์ค๋ก ์บ์๋ฅผ ์ฝ์ ์ ์์ด์ `recovered` ์ ๋ฏธ๋ฆฌ ๋ด๊ฒจ ์๊ธฐ๋ฅผ ๊ธฐ๋ํ๋๋ฐ,
|
|
@@ -1634,9 +2436,9 @@ export class TwinEngine {
|
|
|
1634
2436
|
* ์ฒดํฌํฌ์ธํธ ์บ์ ์ฐ์ (O(1) + ๋ผ์ด๋ธ ํ์์ํ ๋ณด์กด), ์์ผ๋ฉด ์ ๋ replay ํด๋ฐฑ(๋ถํ
๊ณผ ๊ฐ์ ์์).
|
|
1635
2437
|
*/
|
|
1636
2438
|
if (!this.recovered[key] && reg.purpose !== 'bench') {
|
|
1637
|
-
const cached = await this.
|
|
2439
|
+
const cached = await this.warmSeedFor(domainId, instanceId).catch(() => null)
|
|
1638
2440
|
if (cached?.state) {
|
|
1639
|
-
this.recovered[key] =
|
|
2441
|
+
this.recovered[key] = cached
|
|
1640
2442
|
} else {
|
|
1641
2443
|
const state = await this.recover(domainId, instanceId).catch(() => null)
|
|
1642
2444
|
if (state) this.recovered[key] = { revision: state.revision, state }
|
|
@@ -1664,8 +2466,8 @@ export class TwinEngine {
|
|
|
1664
2466
|
๋ฉ์ ๋๋ฐ, ๊ทธ๊ฑด YMS/MES ํธ์์ **์กฐ์ฉํ WMS ๋ก ๋ถํ
**์ํค๋ ๊ธธ์ด์๋ค โ ์ค๋ฅ ์์ด ๋ค๋ฅธ ๊ณต์ฅ์ด ๋ฌ๋ค. */
|
|
1665
2467
|
reg.kind,
|
|
1666
2468
|
reg.model as TwinModelDef,
|
|
1667
|
-
/* ์ ์ฅ๋ ๊ฐ์ **์๊ฒฉํ**
|
|
1668
|
-
|
|
2469
|
+
/* ์ ์ฅ๋ ๊ฐ์ ์์์ **์๊ฒฉํ** ์ฝ์๋ค โ ๋ชจ๋ฅด๋ ๊ฐ์ ๊ธฐ๋ณธ๊ฐ์ผ๋ก ๋ฉ์ฐ๋ฉด ๊ทธ ํธ์์ด ์กฐ์ฉํ ๋ค๋ฅด๊ฒ ์ฌ๊ธฐ๋ํ๋ค. */
|
|
2470
|
+
policy,
|
|
1669
2471
|
reg.purpose,
|
|
1670
2472
|
Number(last?.max ?? 0) || 0
|
|
1671
2473
|
)
|
|
@@ -1685,6 +2487,23 @@ export class TwinEngine {
|
|
|
1685
2487
|
if (mode === 'resync') {
|
|
1686
2488
|
const reg = await getRepository(TwinInstance).findOne({ where: { domain: { id: domainId }, instanceId } })
|
|
1687
2489
|
if (!reg?.model) throw new Error(`instance "${instanceId}" not provisioned (no model)`)
|
|
2490
|
+
/*
|
|
2491
|
+
* โโ **๋ฏธ๋ฌ๋ ์จ์์ ์ฌ๊ธฐ์ ํ๋ณดํ๋ค** (2026-08-24) โโโโโโโโโโโโโโโโโโโโโโโโ
|
|
2492
|
+
*
|
|
2493
|
+
* ์ด ์ค์ด ์์ด์ ๋ฏธ๋ฌ๋ ์ ์ฅ๋ ์ฒดํฌํฌ์ธํธ๋ฅผ **ํ ๋ฒ๋ ์ฝ์ง ์์๋ค.** `startLive` ๋ ๋๊ธฐ๋ผ ์ค์ค๋ก
|
|
2494
|
+
* ์บ์๋ฅผ ์ฝ์ ์ ์๊ณ `recovered` ์ ๋ด๊ฒจ ์๊ธฐ๋ฅผ ๊ธฐ๋ํ๋๋ฐ, ๊ทธ๊ฒ์ ๋ด๋ ๊ณณ์ `bootstrap` ๊ณผ
|
|
2495
|
+
* `start` ๋ฟ์ด์๋ค โ ๊ทธ๋ฆฌ๊ณ ๋ฏธ๋ฌ์ ์ค์ ๊ธฐ๋ ๊ฒฝ๋ก๋ **์ฌ๊ธฐ**๋ค. ๊ทธ๋์ ใ์ฝ๋ ์ชฝยท์ฐ๋ ์ชฝ์ด ๋ค
|
|
2496
|
+
* ์๋๋ฐ ์๋ฌด ์ผ๋ ์ผ์ด๋์ง ์๋ใ ์ํ๊ฐ ๋๋ค.
|
|
2497
|
+
*
|
|
2498
|
+
* ์ด๊ฒ์ `start` ๊ฐ ์ด๋ฏธ ๋ฐฐ์ด ๊ตํ๊ณผ ๊ฐ์ ์๋ฆฌ๋ค: ๋ถํ
์์์ ๊ธฐ๋๋ฉด ์ด๋ค ๋ ์ ์ํ๊ฐ ์ด์๋๊ณ
|
|
2499
|
+
* ์ด๋ค ๋ ์ ์กฐ์ฉํ ๋น ์ฑ๋ก ๋ฌ๋ค โ **์ฌํ๋์ง ์๋ ๊ฒฐํจ์ด ๊ฐ์ฅ ๋์๋ค.** ๊ทธ๋์ ์์์ ๊ธฐ๋์ง ์๊ณ
|
|
2500
|
+
* ์ด ์๋ฆฌ์์ ํ๋ณดํ๋ค(์ด๋ฏธ ๋ด๊ฒจ ์์ผ๋ฉด ๊ทธ๊ฒ์ ์ด๋ค).
|
|
2501
|
+
*/
|
|
2502
|
+
const key = runtimeKey(domainId, instanceId)
|
|
2503
|
+
if (!this.recovered[key]) {
|
|
2504
|
+
const seed = await this.warmSeedFor(domainId, instanceId).catch(() => null)
|
|
2505
|
+
if (seed?.state) this.recovered[key] = seed
|
|
2506
|
+
}
|
|
1688
2507
|
return this.startLive(instanceId, domainId, reg.kind, reg.model as TwinModelDef)
|
|
1689
2508
|
}
|
|
1690
2509
|
if (mode === 'resume') {
|
|
@@ -1715,6 +2534,23 @@ export class TwinEngine {
|
|
|
1715
2534
|
static async resetJournal(domainId: string, instanceId: string): Promise<void> {
|
|
1716
2535
|
await getRepository(TwinEvent).delete({ domain: { id: domainId } as any, instanceId })
|
|
1717
2536
|
delete this.recovered[runtimeKey(domainId, instanceId)]
|
|
2537
|
+
/*
|
|
2538
|
+
* โโ **์ฒดํฌํฌ์ธํธ๋ ํจ๊ป ๋ฌดํจ๋ก ๋ง๋ ๋ค** (2026-08-20) โโโโโโโโโโโโโโโโโโโโโโ
|
|
2539
|
+
*
|
|
2540
|
+
* ์ ๋๋ง ์ง์ฐ๋ฉด ์จ์ ์ฌํ์ด ๋์ง ์๋๋ค: ์์คํํธ๋ ์ฒดํฌํฌ์ธํธ๋ฅผ **๋จผ์ ** ๋ณด๊ณ , ๊ทธ๊ฒ์ด ๋จ์ ์์ผ๋ฉด
|
|
2541
|
+
* ๋ฐฑ์ง๋ก ์์ํ ํธ์์ ์ ์ํ๊ฐ ๋์ด์๋๋ค(์ ๋์ ๋น์๋๋ฐ ์ฌ๊ณ ๊ฐ ์๋ ํธ์์ด ๋๋ค).
|
|
2542
|
+
* ์๊ฐ์ฌํ์ด ๋๋ ์ฌ์ฌ ์บ์๋ ๊ฐ์ ์ด์ ๋ก ๋ฌดํจ๋ค โ ์ง์์ง ๋ฆฌ๋น์ ์ ๊ฐ๋ฆฌํค๊ฒ ๋๋ค.
|
|
2543
|
+
*
|
|
2544
|
+
* ๊ณตํต ์บ์ ์๋น์ค์๋ ์ญ์ ๊ฐ ์๋ค(get/set/clearStale ๋ฟ). ๊ทธ๋์ **ใ์์ใ์ ์ ๋๋ค** โ ๊ฐ์
|
|
2545
|
+
* ์ง์ด๋ด๋ ๊ฒ์ด ์๋๋ผ ใ์ฒดํฌํฌ์ธํธ๊ฐ ์๋คใ๋ ์ฌ์ค์ ์ ๋ ๊ฒ์ด๊ณ , ์ฝ๋ ์ชฝ์ ์ด๋ฏธ `cached?.state` ๋ก
|
|
2546
|
+
* ๊ทธ๊ฒ์ ๊ฐ๋ ค๋ธ๋ค. ๊ณตํต ๋ชจ๋์ ์ญ์ ๋ฅผ ์๋ก ๋ซ๋ ๊ฒ์ ์ด ํ ์๋ฆฌ๋ฅผ ์ํด ํ๊ธฐ์๋ ํฐ ๋ณ๊ฒฝ์ด๋ค.
|
|
2547
|
+
*/
|
|
2548
|
+
await cacheService
|
|
2549
|
+
.setInCache(this.SNAPSHOT_CACHE_ID, { domainId, instanceId }, { revision: 0, state: null }, this.SNAPSHOT_TTL_S)
|
|
2550
|
+
.catch(err => twinWarn(`[twin-engine] "${instanceId}" checkpoint not invalidated โ a seed run may inherit old state:`, err?.message ?? err))
|
|
2551
|
+
await cacheService
|
|
2552
|
+
.setInCache(this.CHAIN_INDEX_CACHE_ID, { domainId, instanceId }, { revisions: [] }, this.SNAPSHOT_TTL_S)
|
|
2553
|
+
.catch(err => twinWarn(`[twin-engine] "${instanceId}" chain index not cleared โ time travel may point at deleted revisions:`, err?.message ?? err))
|
|
1718
2554
|
}
|
|
1719
2555
|
|
|
1720
2556
|
/** ์ญ์ โ ์ ์ง + ๋ ์ง์คํธ๋ฆฌ ์ญ์ + ์ ๋ purge(domain ์ค์ฝํ). */
|
|
@@ -1755,27 +2591,60 @@ export class TwinEngine {
|
|
|
1755
2591
|
(await getRepository(TwinSpace).find({ where: { domain: { id: domainId } } })).map(s => [s.spaceId, s.name])
|
|
1756
2592
|
)
|
|
1757
2593
|
/*
|
|
1758
|
-
* **์ต์ ๋ฆฌ๋น์ ์ ํ
|
|
2594
|
+
* **์ต์ ๋ฆฌ๋น์ ์ ํธ์๋ง๋ค ์ธ๋ฑ์ค ๋์์ ํ ํ์ฉ ์ฝ๋๋ค.**
|
|
2595
|
+
*
|
|
2596
|
+
* โโ ๊ทธ๋ฃน ์ง์๋ก ๋ฐ๊พธ์๋ค๊ฐ ๋๋๋ ธ๋ค (2026-08-22 ์ค์ธก) โโโโโโโโโโโโโโโโโโโโโ
|
|
2597
|
+
* ์์ ์ฃผ์์ ์ด๋ฌ๋ค: ใํธ์๋ง๋ค `findOne(order revision DESC)` ์ ๋๊ณ ์์๋ค โ 13๊ฐ๋ฉด ์ง์
|
|
2598
|
+
* 13๋ฒ์ด๊ณ , ํธ์์ด ๋๋ฉด ๊ทธ๋๋ก ์๋๋ค. ํ ๋ฒ์ ๊ทธ๋ฃน ์ง์๋ก ๋ฐ๊พผ๋ค.ใ
|
|
2599
|
+
*
|
|
2600
|
+
* **์ง์ ๊ฐ์๋ฅผ ์ค์์ง๋ง ์ผ์ ์์ ๋๋ ธ๋ค.**
|
|
1759
2601
|
*
|
|
1760
|
-
*
|
|
1761
|
-
*
|
|
1762
|
-
*
|
|
2602
|
+
* findOne ร 13 `(domain, instance, revision)` ์ธ๋ฑ์ค **๋์์ ํ ํ** โ ๊ฐ๊ฐ O(1)
|
|
2603
|
+
* GROUP BY ํ ๋ฒ ๊ทธ ๋๋ฉ์ธ์ ์ ๋์ **์ ์ ์ง๊ณ** โ O(์ ์ฒด)
|
|
2604
|
+
*
|
|
2605
|
+
* ์ ๋์ด ์์ ๋๋ ์ด๋์ด์๊ณ , ์ปค์ง๋ฉด์ ์ํด๊ฐ ๋๋ค. ์ค์ธก(์ ๋ 1,134๋ง ํ): ์ด ๊ทธ๋ฃน ์ง์๊ฐ
|
|
2606
|
+
* **44.7์ด**์๋ค. ๊ทธ๋ฆฌ๊ณ sqlite ๋๋ผ์ด๋ฒ๋ ์ฐ๊ฒฐ์ด ํ๋์ด๋ฏ๋ก ๊ทธ๋์ ์ฑ์ ๋ชจ๋ ์ง์๊ฐ ๊ทธ ๋ค์ ์ฐ๋ค โ
|
|
2607
|
+
* ๊ฐ์ ์๊ฐ 29ํ์ง๋ฆฌ `twin_instances` ์กฐํ๋ 44.7์ด๋ก ์ฐํ๋ค. ์ด ๋ชฉ๋ก์ ใํธ์ ๊ด๋ฆฌยทํ์ฅ ๊ตฌ์ฑยท
|
|
2608
|
+
* ์ํฐํฐ ํจ๋์ด ๋ชจ๋ ์ฝ๋ ์๋ฆฌใ์ฌ์, ํ๋ฉด์ ์ด ๋๋ง๋ค ์๋ฒ ์ ์ฒด๊ฐ ๊ทธ๋งํผ ๋ฉ์ท๋ค.
|
|
2609
|
+
*
|
|
2610
|
+
* ํธ์ ์๋ ์์ญ์ด๊ณ ์ ๋์ ์ฒ๋ง์ด๋ค. **์์ ๊ฒ์ ์ฌ๋ฌ ๋ฒ ์ฝ๋ ํธ์ด ํฐ ๊ฒ์ ํ ๋ฒ ํ๋ ๊ฒ๋ณด๋ค ์ธ๋ค.**
|
|
2611
|
+
* ์ง์ ๊ฐ์๊ฐ ํธ์ ์์ ๋น๋กํด ์๋ผ๋ ๊ฒ์ ์ฌ์ค์ด์ง๋ง, ๊ฐ ์ง์๊ฐ ์ธ๋ฑ์ค ๋ ํ ํ์ด๋ฏ๋ก ๊ทธ ์ฑ์ฅ์
|
|
2612
|
+
* ๊ฐ๋น๋๋ค โ ๊ทธ๋ฆฌ๊ณ ๋ณ๋ ฌ๋ก ๋ฌป๋๋ค.
|
|
2613
|
+
*
|
|
2614
|
+
* **๋ค์ ๊ทธ๋ฃน ์ง์๋ก ๋ฐ๊พธ์ง ๋ง ๊ฒ.** ๋ฐ๊พธ๋ ค๋ฉด ์ด ์ซ์๋ฅผ ๋จผ์ ๋ค์ ์ฌ๋ผ.
|
|
1763
2615
|
*/
|
|
1764
2616
|
const tipOfInstance = new Map<string, number>()
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
2617
|
+
await Promise.all(
|
|
2618
|
+
rows.map(async r => {
|
|
2619
|
+
try {
|
|
2620
|
+
/*
|
|
2621
|
+
* **์ง๊ณ ํ๋๋ก ๋ฌป๋๋ค โ ํ์ ์ค์ด ์ค์ง ์๋๋ค.**
|
|
2622
|
+
*
|
|
2623
|
+
* `findOne(order revision DESC)` ๋ก ๋์๋๋ ๋ ๊ฐ์ง๊ฐ ํ๋ ธ๋ค. โ ๊ด๊ณ๋ฅผ ๊ฐ์ง ์ํฐํฐ์ ์ ๋ ฌ
|
|
2624
|
+
* ์กฐํ๋ TypeORM ์ด `DISTINCT` ๋ก ๊ฐ์ธ๋ฏ๋ก `select` ๋ก ์ปฌ๋ผ์ ์ขํ๋ฉด `distinctAlias.
|
|
2625
|
+
* TwinEvent_id` ๊ฐ ์๋ค๊ณ ํฐ์ง๋ค(์ค์ธก: ๋ชจ๋ ํธ์์์ SQLITE_ERROR). โก `select` ๋ฅผ ๋ผ๋ฉด
|
|
2626
|
+
* **`payload` ๊น์ง ์ค์ด ์จ๋ค** โ ์ด ๊ฐ ํ๋๋ฅผ ์๋ ค๊ณ ํฐ TEXT ๋ฅผ ์ฝ๋๋ค.
|
|
2627
|
+
*
|
|
2628
|
+
* ๊ทธ๋์ ๋จ์ผ ์ง๊ณ๋ฅผ ์ด๋ค. `(domain, instance, revision)` ์ธ๋ฑ์ค์์ instance ๊ฐ ์ ํด์ง๋ฉด
|
|
2629
|
+
* `MAX` ๋ ๊ทธ ๊ตฌ๊ฐ์ ๋์ด๋ฏ๋ก sqlite ๊ฐ ๋์ ์ง๋๋ค(์ค์ธก 0.007์ด ยท ๋๋ฉ์ธ ์ ์ฒด GROUP BY ๋
|
|
2630
|
+
* 14.5์ด). ์ด ํ์ผ์ ๋ค๋ฅธ ์๋ฆฌ๋ ๊ฐ์ ๊ท์จ์ด๋ค(ยง`timeRange` ยท ๊ธฐ๋ ์ ์ต์ข
๋ฆฌ๋น์ ).
|
|
2631
|
+
*/
|
|
2632
|
+
const row = await getRepository(TwinEvent)
|
|
2633
|
+
.createQueryBuilder('e')
|
|
2634
|
+
.select('MAX(e.revision)', 'revision')
|
|
2635
|
+
.where('e.domain = :domainId', { domainId })
|
|
2636
|
+
.andWhere('e.instanceId = :instanceId', { instanceId: r.instanceId })
|
|
2637
|
+
.getRawOne<{ revision: unknown }>()
|
|
2638
|
+
/* ํ์ด ์์ผ๋ฉด `null` ์ด๋ค โ ๊ทธ๋๋ ๋น์ ๋๋ค(0 ์ "์๋ฌด ์ผ๋ ์์๋ค" ๋ ์ฃผ์ฅ์ด๋ค). */
|
|
2639
|
+
if (row?.revision != null) tipOfInstance.set(String(r.instanceId), Number(row.revision) || 0)
|
|
2640
|
+
} catch (err: any) {
|
|
2641
|
+
/* ํ ํธ์์ ์กฐํ๊ฐ ์คํจํด๋ **0 ์ผ๋ก ๋ฉ์ฐ์ง ์๋๋ค** โ ๋ฆฌ๋น์ 0 ์ "์์ง ์๋ฌด ์ผ๋ ์์๋ค" ๋
|
|
2642
|
+
์ฌ์ค ์ฃผ์ฅ์ด๋ค. ๋น์ ๋๋ฉด ์๋์์ undefined ๋ก ๋จ๊ณ , ํ๋ฉด์ ๊ทธ๊ฒ์ "๋ชจ๋ฆ" ์ผ๋ก ๊ทธ๋ฆฐ๋ค.
|
|
2643
|
+
๊ทธ๋ฆฌ๊ณ ๊ทธ ํธ์๋ง ๋ชจ๋ฆ์ด ๋๋ค โ ์์ ์๋ ์ง๊ณ ํ๋๊ฐ ์คํจํ๋ฉด ์ ๋ถ ๋ชจ๋ฆ์ด์๋ค. */
|
|
2644
|
+
twinError(`[twin-engine] latest revision lookup failed "${r.instanceId}"`, err?.message ?? err)
|
|
2645
|
+
}
|
|
2646
|
+
})
|
|
2647
|
+
)
|
|
1779
2648
|
|
|
1780
2649
|
const out: any[] = []
|
|
1781
2650
|
for (const r of rows) {
|
|
@@ -2102,11 +2971,11 @@ export class TwinEngine {
|
|
|
2102
2971
|
}
|
|
2103
2972
|
}
|
|
2104
2973
|
} catch (err: any) {
|
|
2105
|
-
|
|
2974
|
+
twinError(`[twin-engine] structure projection failed for "${master.source}":`, err?.message)
|
|
2106
2975
|
warnings.push(projectionFailed(err?.message ?? 'unknown'))
|
|
2107
2976
|
}
|
|
2108
2977
|
|
|
2109
|
-
if (warnings.length)
|
|
2978
|
+
if (warnings.length) twinWarn(`[twin-engine] ingest "${master.source}" warnings: ${describeWarnings(warnings)}`)
|
|
2110
2979
|
return { instanceId: master.source, spaceId, warnings }
|
|
2111
2980
|
}
|
|
2112
2981
|
|
|
@@ -2129,15 +2998,15 @@ export class TwinEngine {
|
|
|
2129
2998
|
* ๋ณด๋ ์ปดํฌ๋ํธ๊ฐ tag ๋ก ๊ตฌ๋
(board-ui provider)ํด `component.data` ๋ก ๋ผ์ด๋ธ ๊ฐฑ์ . delta ์์๋ง(ํฌ์).
|
|
2130
2999
|
* tag=์ํฐํฐ id(๋ฐ๋ชจ=๋จ์ผ ์ธ์คํด์ค). ๋ฉํฐ ์ธ์คํด์ค/๋ณด๋ ์ฌ์ฌ์ฉ ์ tag ๋ค์์คํ์ด์ค๋ ํ์.
|
|
2131
3000
|
*/
|
|
2132
|
-
/**
|
|
3001
|
+
/** ๋ธ๋ก๋์บ์คํ
์คํจ ๋ก๊ทธ ์กฐ์ โ ์ฐฝ๋ง๋ค ํ ์ค(๋งํ ๊ตฌ๋
์๋ ์ด๋น ์์ญ ๋ฒ ์คํจํ๋ค). */
|
|
2133
3002
|
private static publishDrops = new Map<string, { count: number; lastLogMs: number }>()
|
|
2134
3003
|
private static readonly PUBLISH_DROP_LOG_MS = 10_000
|
|
2135
3004
|
|
|
2136
3005
|
/**
|
|
2137
|
-
* ํ ๋ฒ์
|
|
3006
|
+
* ํ ๋ฒ์ ๋ธ๋ก๋์บ์คํ
โ **๊ตฌ๋
์ ํ๋๊ฐ ํธ์คํธ๋ฅผ ์ฃฝ์ด์ง ๋ชปํ๊ฒ.**
|
|
2138
3007
|
*
|
|
2139
3008
|
* โโ ๋ฌด์์ด ์ฃฝ์๋ (2026-08-14) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
2140
|
-
* ๋ฐ๋ฆฐ push ๊ฐ 1024๋ฅผ ๋์ผ๋ฉด pubsub ์ด
|
|
3009
|
+
* ๋ฐ๋ฆฐ push ๊ฐ 1024๋ฅผ ๋์ผ๋ฉด pubsub ์ด ์ค๋ฅ๋ฅผ ๋ธ๋ค(`RepeaterOverflowError`). ๊ทธ ๋ธ๋ก๋์บ์คํ
์ ํ์ด๋จธ ์ฝ๋ฐฑ
|
|
2141
3010
|
* ์์์ ์ผ์ด๋๋ฏ๋ก ์์ธ๊ฐ ์กํ๋ ๊ณณ ์์ด ์ฌ๋ผ๊ฐ **ํ๋ก์ธ์ค๊ฐ ๋๋ฌ๋ค** โ ํธ์ 14๊ฐ๊ฐ ๋๋ ํธ์คํธ๊ฐ
|
|
2142
3011
|
* ์๋น๋ฅผ ๋ฉ์ถ ๊ตฌ๋
์ ํ๋ ๋๋ฌธ์ ํต์งธ๋ก.
|
|
2143
3012
|
*
|
|
@@ -2154,7 +3023,7 @@ export class TwinEngine {
|
|
|
2154
3023
|
const now = Date.now()
|
|
2155
3024
|
if (now - d.lastLogMs >= this.PUBLISH_DROP_LOG_MS) {
|
|
2156
3025
|
d.lastLogMs = now
|
|
2157
|
-
|
|
3026
|
+
twinWarn(`[twin-engine] broadcast dropped on "${what}" (${d.count} so far) โ ${err?.message ?? err}`)
|
|
2158
3027
|
}
|
|
2159
3028
|
this.publishDrops.set(what, d)
|
|
2160
3029
|
return false
|
|
@@ -2179,16 +3048,34 @@ export class TwinEngine {
|
|
|
2179
3048
|
if (!st) return
|
|
2180
3049
|
/*
|
|
2181
3050
|
* ๋ณํํ ์ํฐํฐ๋ง ๋ฐํ โ ์ด์ ์ delta ๋ง๋ค ์ ์ํฐํฐ(๋
ธ๋+๋ฌด๋ฒ+์ค๋)๋ฅผ ๊ฐ ๋ณํ์ ๋ฌด๊ดํ๊ฒ ์ ๋ ์ฌ๋ฐํํด
|
|
2182
|
-
* ๊ฐ์ ๋ฐ์ดํฐ๋ฅผ ๋งค์ด ๋ฐ๋ณต push ํ๊ณ ์์๋ค(์ค์ฉ). ์ต์ -์ํ ์ฑ๋์ด๋ฏ๋ก ๋ฌด๋ณํ
|
|
3051
|
+
* ๊ฐ์ ๋ฐ์ดํฐ๋ฅผ ๋งค์ด ๋ฐ๋ณต push ํ๊ณ ์์๋ค(์ค์ฉ). ์ต์ -์ํ ์ฑ๋์ด๋ฏ๋ก ๋ฌด๋ณํ ์ฌ๋ธ๋ก๋์บ์คํ
์ ๋ฌด์๋ฏธํ๋ค.
|
|
2183
3052
|
* ์ํฐํฐ๋ณ ์๊ทธ๋์ฒ๋ฅผ ๋น๊ตํด ๋ฐ๋ ๊ฒ๋ง push.
|
|
2184
3053
|
*/
|
|
2185
3054
|
const sigs = inst.entitySigs ?? (inst.entitySigs = new Map())
|
|
2186
3055
|
const seen = new Set<string>()
|
|
2187
3056
|
/* payload ๋งคํ์ ์์ ํจ์(buildEntityDeltas)๋ก ๋ถ๋ฆฌ โ ์ฌ๊ธฐ์ ์๊ทธ๋์ฒ dedup + ๋ฐํ๋ง.
|
|
2188
|
-
* ๋ณํํ ์ํฐํฐ๋ง ๋ฐํ(์ต์ -์ํ ์ฑ๋์ด๋ผ ๋ฌด๋ณํ
|
|
3057
|
+
* ๋ณํํ ์ํฐํฐ๋ง ๋ฐํ(์ต์ -์ํ ์ฑ๋์ด๋ผ ๋ฌด๋ณํ ์ฌ๋ธ๋ก๋์บ์คํ
๋ฌด์๋ฏธ). */
|
|
2189
3058
|
/* โก payload ๋ง๋ค๊ธฐ โ ์ํฐํฐ ์์ ๋น๋ก. โข ์๊ทธ๋์ฒ ๋น๊ต + ๋ฐํ โ ๋ฐ๋ ๊ฒ ์์ ๋น๋ก. */
|
|
2190
3059
|
const tDelta = performance.now()
|
|
2191
|
-
|
|
3060
|
+
/*
|
|
3061
|
+
* โโ ๋ฌผํ์ **๊ฑด๋๋ฆฐ ๊ฒ๋ง** ๋ง๋ ๋ค (2026-08-21 ์ค์ธก) โโโโโโโโโโโโโโโโโโโโโโ
|
|
3062
|
+
* ๋ง๋๋ payload ์ ๋๋ถ๋ถ์ด ๋ฌผํ์ด๋ค(์ํฐํฐ 3,611 ์ค 2,400). ๋ฐ๋ ๊ฒ์ด ํ๋์ฌ๋ ์ ๋ถ ๋ง๋ค์ด
|
|
3063
|
+
* ๋ฌธ์์ด๋ก ๋ฐ๊พผ ๋ค ใ๊ฐ๋คใ๋ฅผ ํ์ธํ๊ณ ๋ฒ๋ ธ๋ค.
|
|
3064
|
+
*
|
|
3065
|
+
* ๋ฒ์๋ ์ด ์ฐฝ์ ๋ค์ด์จ ์ฌ๊ฑด์์ ๋ชจ์๋ค(`touchedItemKeys`). **๋งํ ์ ์๋ ์ฌ๊ฑด์ด ํ๋๋ผ๋ ์์ผ๋ฉด
|
|
3066
|
+
* ๋ฒ์๋ ์๊ณ ์ ๋ถ ๋ง๋ ๋ค** โ ๋ฏ์ ์ดํ๊ฐ ์ค๋ฉด ์กฐ์ฉํ ๋น ๋จ๋ฆฌ๋ ๋์ ๋น์ธ๊ฒ ์์ ํ ์ชฝ์ผ๋ก ๋จ์ด์ง๋ค.
|
|
3067
|
+
*
|
|
3068
|
+
* ๊ทธ๋ฆฌ๊ณ ์ฃผ๊ธฐ๋ง๋ค ํ ๋ฒ์ **๋ฌด์กฐ๊ฑด ์ ๋ถ** ๋ง๋ ๋ค(`FULL_BROADCAST_EVERY`). ์ปค๋์ด ์ฌ๊ฑด ์์ด ๋ฌผํ์
|
|
3069
|
+
* ๋ฐ๊พธ๋ ์๋ฆฌ๊ฐ ์๊ธฐ๋ฉด ๊ทธ ๊ฐ์ด ํ๋ฉด์ ๋จ์ ์ ์๋๋ฐ, ๊ทธ ์ฐฝ์ ๋ช ์ด๋ก ๋ฌถ๋ ๊ทธ๋ฌผ์ด๋ค. **๋ณด์ฅ์ด ์๋๋ผ
|
|
3070
|
+
* ๊ทธ๋ฌผ์ด๋ค** โ ์ฌ๊ฑด ์์ด ๋ฐ๋๋ ์๋ฆฌ๋ฅผ ์ฐพ์ผ๋ฉด ๊ทธ๊ฒ์ ๊ณ ์น๋ ๊ฒ์ด ๋ต์ด๊ณ ์ด ๊ทธ๋ฌผ์ ์๊ฐ์ ๋ฒ ๋ฟ์ด๋ค.
|
|
3071
|
+
*/
|
|
3072
|
+
const full = inst.fullBroadcastDue === true || !inst.dirtyItems
|
|
3073
|
+
const deltas = buildEntityDeltas(st, inst.id, full ? undefined : { items: inst.dirtyItems! })
|
|
3074
|
+
this.broadcastPasses++
|
|
3075
|
+
if (full) this.broadcastFullPasses++
|
|
3076
|
+
/* ์ด๋ฒ ์ฐฝ์ ๋ฒ์๋ ์ฌ๊ธฐ์ ๋ซ๋๋ค โ ๋ค์ ์ฐฝ์ ๋ค์ ๋ชจ์๋ค. */
|
|
3077
|
+
inst.dirtyItems = new Set()
|
|
3078
|
+
inst.fullBroadcastDue = false
|
|
2192
3079
|
recordPhase(load, 'deltas', performance.now() - tDelta)
|
|
2193
3080
|
|
|
2194
3081
|
const tPub = performance.now()
|
|
@@ -2204,14 +3091,21 @@ export class TwinEngine {
|
|
|
2204
3091
|
if (!this.publishGuarded('data', { data: { domain, tag, data } }, `data:${inst.id}`)) sigs.delete(tag)
|
|
2205
3092
|
}
|
|
2206
3093
|
recordPhase(load, 'publish', performance.now() - tPub)
|
|
2207
|
-
/*
|
|
2208
|
-
|
|
3094
|
+
/*
|
|
3095
|
+
* ์ฌ๋ผ์ง ์ํฐํฐ์ ์๊ทธ๋์ฒ ์ ๋ฆฌ(๋งต ๋ฌดํ ์ฑ์ฅ ๋ฐฉ์ง) โ **์ ๋ถ ๋ง๋ ์ฐฝ์์๋ง.**
|
|
3096
|
+
* ๋ฒ์๋ฅผ ์ขํ ์ฐฝ์ `seen` ์๋ ๋ง๋ค์ง ์์ ์ํฐํฐ๊ฐ ์์ผ๋ฏ๋ก, ๊ทธ๋ ์ ๋ฆฌํ๋ฉด ์ด์ ์๋ ํ๊ทธ์
|
|
3097
|
+
* ์๊ทธ๋์ฒ๋ฅผ ์ง์ด๋ค. ๋ก์ ๊ฐ์ด ๋๊ฐ๋ ๊ฒ์ ์๋์ง๋ง(๋ค์ ์ฐฝ์ ๋ค์ ๋ง๋ค์ด ๋ณด๋ธ๋ค) ๊ฐ์ ๊ฐ์
|
|
3098
|
+
* ๋ํ์ด ๋ณด๋ด๊ฒ ๋์ด, ์ค์ด๋ ค๋ ๊ฒ์ ๋๋๋ฆฐ๋ค.
|
|
3099
|
+
*/
|
|
3100
|
+
if (full && sigs.size > seen.size) for (const tag of sigs.keys()) if (!seen.has(tag)) sigs.delete(tag)
|
|
2209
3101
|
}
|
|
2210
3102
|
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
3103
|
+
/*
|
|
3104
|
+
* ๋จ๊ฑด ์ ์ฅ(`persist`)์ **์์ด๋ค** (2026-08-20, ยง7.1).
|
|
3105
|
+
*
|
|
3106
|
+
* ์๋ฎฌ์ด ๋ธํ๋ง๋ค ์ด๊ฒ์ ๋ถ๋ฌ ํ ํ๋์ฉ ์ผ๊ณ , ๊ทธ๊ฒ์ด ๊ท๋ชจ์์ ํธ์คํธ๋ฅผ ๋จน์๋ค. ๋จ๊ฒจ ๋๋ฉด ๋ค์ ์ฌ๋์ด
|
|
3107
|
+
* ๋ค์ ๋ถ๋ฅผ ์๋ฆฌ๊ฐ ๋๋ฏ๋ก ์ง์ด๋ค โ ์ฐ๋ ๋ฌธ์ `flushJournal` ํ๋๋ค(`persistBatch`ยท`persistCarried`).
|
|
3108
|
+
*/
|
|
2215
3109
|
|
|
2216
3110
|
/**
|
|
2217
3111
|
* ์ฌ๋ถํ
๋ณต๊ตฌ / ์๊ฐ์ฌํ โ DB ์ ๋์ replay ํด ์ํ ์ฌ๊ตฌ์ฑ.
|
|
@@ -2303,6 +3197,10 @@ export class TwinEngine {
|
|
|
2303
3197
|
const baseWhere = { domain: { id: domainId }, instanceId }
|
|
2304
3198
|
/* ์ด์ด ์ ์ ๋๋ **๊ทธ ๋ค๋ง** ์ฝ๋๋ค โ ์ฌ๊ฐ์ ๊น์ง์ ์ฌ์ค์ ์ด๋ฏธ ์จ์ ์์ ์๋ค. */
|
|
2305
3199
|
const from = resume ? (resume.revision ?? 0) : undefined
|
|
3200
|
+
/*
|
|
3201
|
+
* journal-fold: ์ฌ์์ ์ฌ์ค์ ํ๋์ฉ ์ ๋ ๊ฒ์ด๋ฏ๋ก ๊ทธ ๊ตฌ๊ฐ์ ํ์ด ํ์ํ๋ค. ์ปค์(`from`)์ ์๊ฐ
|
|
3202
|
+
* ์ํ์ด ๊ตฌ๊ฐ์ ์๋ฅด๊ณ , ์ฒดํฌํฌ์ธํธ๊ฐ ์์ชฝ์ ์จ์์ผ๋ก ๋์ ํ๋ค โ ํ ์ ์ฒด๋ฅผ ์ฝ์ง ์๋๋ค.
|
|
3203
|
+
*/
|
|
2306
3204
|
const rows = await getRepository(TwinEvent).find({
|
|
2307
3205
|
where: useTime
|
|
2308
3206
|
? from
|
|
@@ -2483,22 +3381,71 @@ export class TwinEngine {
|
|
|
2483
3381
|
/**
|
|
2484
3382
|
* ๊ณต๊ฐ(๊ณต๋๋ฐฐ์น) ์๊ฐ ๋ฒ์ โ ์คํฌ๋ฌ๋ฒ ์ต์ปค(runtime-state-model ยง4ยทยง6). ๊ทธ ๊ณต๊ฐ ์ ์ธ์คํด์ค ์ ๋์ min/max eventTime.
|
|
2485
3383
|
* ๋ฐํ {minTime, maxTime}(ISO) โ ์ด๋ฒคํธ/์๊ฐ ์์ผ๋ฉด null. ํด๋ผ ํ์คํ ๋ฆฌ ์คํฌ๋ฌ๋ฒ๊ฐ ์ด ๋ฒ์๋ฅผ ์๊ฐ์ถ์ผ๋ก ๊ทธ๋ฆฐ๋ค.
|
|
3384
|
+
*
|
|
3385
|
+
* โโ ๋ ์๋ฅผ ๊ตฌํ๋ ค๊ณ ์ ๋์ ๋ค ์ฝ์ง ์๋๋ค (2026-08-20 ์ค์ธก์ผ๋ก ์ก์) โโโโโโโโ
|
|
3386
|
+
* ์ฌ๊ธฐ์ ์ธ์คํด์ค๋ง๋ค `find()` ๋ก **ํ์ ์ ๋ถ ์ํฐํฐ๋ก ํ์ด๋๋ ์ด์
**ํ ๋ค์ JS ์์ min/max ๋ฅผ
|
|
3387
|
+
* ๊ณจ๋๋ค. ๊ทธ๋ฐ๋ฐ ์ด ํจ์๋ ํ๋ฉด ์๋จ์ ์ปจํ
์คํธ ๋ ๊ฐ **4์ด๋ง๋ค** ๋ถ๋ฅธ๋ค. ์ค์ธกํ ๊ฐ๋ฐ ์๋ฒ์์
|
|
3388
|
+
* `order-check` ํ ํธ์์ด 275,882ํ ยท `payload` 131MB ์๋ค โ 4์ด๋ง๋ค ๊ทธ JSON ์ ์ ๋ถ ํ์ฑํ ๊ฒ์ด๋ค.
|
|
3389
|
+
*
|
|
3390
|
+
* ๊ทธ ๊ฒฐ๊ณผ๊ฐ ์ด๋ฌ๋ค: ํ๋ก์ธ์ค CPU 81~152%, ์๋ฌด ์ผ๋ ํ์ง ์๋ ์ง์๊ฐ 8~14์ด. JS ํ๋กํ์ผ์ ์์๊ฐ
|
|
3391
|
+
* TypeORM ์ `RelationIdLoader`ยท`RawSqlResultsToEntityTransformer`ยท`stringToSimpleJson`(= `payload`
|
|
3392
|
+
* ํ์ฑ)์ด๊ณ GC ๊ฐ 16.7% ์๋ค. **ํธ์ ํฑ๋ ์ ๋ ์ฐ๊ธฐ๋ ์๋๋ผ ์ด ์ฝ๊ธฐ์๋ค.**
|
|
3393
|
+
*
|
|
3394
|
+
* ์ง๊ณ๋ DB ๊ฐ ํ๋ค. `(domain, instanceId, eventTime)` ์ธ๋ฑ์ค๊ฐ ์ด๋ฏธ ์์ด(`ix_twin_event_1`)
|
|
3395
|
+
* ์ธ๋ฑ์ค์ **์ ๋์ ์ง๋๋ค** โ ํ์ ํ๋๋ ์ค์ด ์ค์ง ์๋๋ค.
|
|
3396
|
+
*
|
|
3397
|
+
* โโ ์ MIN ๊ณผ MAX ๋ฅผ ํ ๋ฌธ์ฅ์ ๋ฃ์ง ์๋ (์ค์ธก) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
3398
|
+
* ์ฒ์์ `SELECT MIN(...), MAX(...) ... WHERE instanceId IN (...)` ํ ๋ฐฉ์ผ๋ก ๋์๋๋ **21ms** ์๋ค.
|
|
3399
|
+
* ์ง๊ณ๊ฐ **๋์ด๋ฉด** ์ตํฐ๋ง์ด์ ์ ใ์ธ๋ฑ์ค ๋์ ์ง๋ใ ์ต์ ํ๊ฐ ๊ฑธ๋ฆฌ์ง ์์ ์ธ๋ฑ์ค ๊ตฌ๊ฐ์ ํ๋๋ค โ
|
|
3400
|
+
* ์ฆ ๋น์ฉ์ด ์ฌ์ ํ **ํ ์์ ๋น๋ก**ํ๋ค. ํธ์๋ง๋ค ๋จ์ผ ์ง๊ณ๋ก ๋๋ ๋ฌผ์ผ๋ฉด **0.32ms**(ํธ์ 3๊ฐ ยท 6์๋ณต)
|
|
3401
|
+
* ์ด๊ณ , ๋น์ฉ์ด **ํธ์ ์์ ๋น๋ก**ํ๋ค. ๊ท๋ชจ ๊ธฐ์ค(์ํฐํฐ 10๋ง)์์๋ ์ด ์ฐจ์ด๊ฐ ๋ณธ์ง์ด๋ค.
|
|
3402
|
+
*
|
|
3403
|
+
* ์๋ณต์ ํธ์๋น ๋์ด์ง๋ง **ํจ๊ป ๋์ด๋ค** โ ์๊ฒฉ DB ์์ ์ง๋ ฌ๋ก ๋๋ฉด ์๋ณต ์ง์ฐ์ด ๊ทธ๋๋ก ์์ธ๋ค.
|
|
3404
|
+
*
|
|
3405
|
+
* ๋๋ผ์ด๋ฒ ๋ค์ฏ์ ๋ค ์ง๋๊ฐ์ผ ํ๋ฏ๋ก raw SQL ์ ์ฐ์ง ์๋๋ค(์ฟผ๋ฆฌ๋น๋์ MIN/MAX ๋ ์ด์๋๋ค).
|
|
3406
|
+
* ๋๋ ค์ฃผ๋ ๊ฐ์ **๋ชจ์์ ๋๋ผ์ด๋ฒ๋ง๋ค ๋ค๋ฅด๋ค**(๋ฌธ์์ดยทDate) โ ๋ฐ์ ๋ค์ ํ ๋ฒ ์ ๊ทํํ๋ค(`parseTime`).
|
|
2486
3407
|
*/
|
|
2487
3408
|
static async timeRange(domainId: string, spaceId: string): Promise<{ minTime: string | null; maxTime: string | null }> {
|
|
2488
3409
|
const regs = await getRepository(TwinInstance).find({ where: { domain: { id: domainId }, spaceId } })
|
|
2489
3410
|
const ids = regs.map(r => r.instanceId)
|
|
2490
3411
|
if (!ids.length) return { minTime: null, maxTime: null }
|
|
3412
|
+
|
|
3413
|
+
/**
|
|
3414
|
+
* ํ ํธ์ ์ ๋์ ์๊ฐ ์ ๋ ํ๋ โ ๋จ์ผ ์ง๊ณ๋ผ ์ธ๋ฑ์ค ๋์ ์ง๋๋ค.
|
|
3415
|
+
*
|
|
3416
|
+
* ์ง๊ณ์์ **์กฐ๋ฆฝํ์ง ์๊ณ ํ์ ๋ฆฌํฐ๋ด๋ก ์ ๋๋ค.** `` `${agg}(e.eventTime)` `` ๋ก ๋ง๋ค๋ฉด ์ด ์๋ฆฌ๊ฐ
|
|
3417
|
+
* ์ง๊ณ๋ฅผ ์ฐ๋์ง ํ๋ ๊ฒ์ฌ(`journal-read-discipline`)๊ฐ ์ฐพ์ง ๋ชปํ๋ค โ ์ค์ ๋ก ๊ทธ๋ ๊ฒ ๋นจ๊ฐ์ก๋ค.
|
|
3418
|
+
* ๋ฒ์ญ ํค์์ ๊ฒช์ ๊ฒ๊ณผ ๊ฐ์ ๊ท์จ์ด๋ค: ๊ฒ์ฌ๊ฐ ์ผํ๊ฒ ๋๋ ํธ์ด ๋ซ๋ค.
|
|
3419
|
+
*/
|
|
3420
|
+
const AGG_SELECT = { MIN: 'MIN(e.eventTime)', MAX: 'MAX(e.eventTime)' } as const
|
|
3421
|
+
const edge = async (instanceId: string, agg: 'MIN' | 'MAX'): Promise<number | null> => {
|
|
3422
|
+
const row = await getRepository(TwinEvent)
|
|
3423
|
+
.createQueryBuilder('e')
|
|
3424
|
+
.select(AGG_SELECT[agg], 't')
|
|
3425
|
+
.where('e.domain = :domainId', { domainId })
|
|
3426
|
+
.andWhere('e.instanceId = :instanceId', { instanceId })
|
|
3427
|
+
.getRawOne<{ t: unknown }>()
|
|
3428
|
+
return parseTime(row?.t)
|
|
3429
|
+
}
|
|
3430
|
+
|
|
2491
3431
|
let min = Infinity
|
|
2492
3432
|
let max = -Infinity
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
3433
|
+
|
|
3434
|
+
/*
|
|
3435
|
+
* ํ ๋ฒ์ ๋์ฐ๋ ์๋ฅผ ๋ฌถ๋๋ค โ ๊ณต๊ฐ์ ํธ์์ด ์๋ฐฑ์ด๋ฉด ์๋ณต ์๋ฐฑ ๊ฐ๋ฅผ ๋์์ ๋ณด๋ด ์ปค๋ฅ์
ํ์
|
|
3436
|
+
* ๋ง๋ ค ๋ฒ๋ฆฐ๋ค(์ด ํจ์ ํ๋ ๋๋ฌธ์ ๋ค๋ฅธ ์ง์๊ฐ ๊ธฐ๋ค๋ฆฌ๊ฒ ๋๋ค).
|
|
3437
|
+
*/
|
|
3438
|
+
for (let i = 0; i < ids.length; i += TIME_RANGE_FANOUT) {
|
|
3439
|
+
const batch = ids.slice(i, i + TIME_RANGE_FANOUT)
|
|
3440
|
+
const edges = await Promise.all(batch.flatMap(id => [edge(id, 'MIN'), edge(id, 'MAX')]))
|
|
3441
|
+
for (let k = 0; k < edges.length; k += 2) {
|
|
3442
|
+
const lo = edges[k]
|
|
3443
|
+
const hi = edges[k + 1]
|
|
3444
|
+
if (lo !== null && lo < min) min = lo
|
|
3445
|
+
if (hi !== null && hi > max) max = hi
|
|
2500
3446
|
}
|
|
2501
3447
|
}
|
|
3448
|
+
|
|
2502
3449
|
if (min === Infinity) return { minTime: null, maxTime: null }
|
|
2503
3450
|
return { minTime: new Date(min).toISOString(), maxTime: new Date(max).toISOString() }
|
|
2504
3451
|
}
|
|
@@ -2519,14 +3466,20 @@ export class TwinEngine {
|
|
|
2519
3466
|
* ์ฒดํฌํฌ์ธํธ ๋ฃจํ๊ฐ 20์ด๋ง๋ค ๋จ์ง๋ง ๊ทธ ์ฌ์ด์ ๋ฉ์ถ๋ฉด ๊ทธ ๊ตฌ๊ฐ์ด ํต์งธ๋ก ๋ค์ ์ ํ๋ค.
|
|
2520
3467
|
*/
|
|
2521
3468
|
await this.persistSnapshot(domainId, id).catch(err =>
|
|
2522
|
-
|
|
3469
|
+
twinError(`[twin-engine] snapshot on stop fail "${id}"`, err?.message ?? err)
|
|
2523
3470
|
)
|
|
3471
|
+
/*
|
|
3472
|
+
* **๋ชจ์ ๋ ์ ๋๋ ์ง๊ธ ํ๋ฆฐ๋ค** โ ์ฃผ๊ธฐ flush ์ฌ์ด(์ต๋ `BROADCAST_COALESCE_MS`)์ ๋ฉ์ถ๋ฉด ๊ทธ ๊ตฌ๊ฐ์
|
|
3473
|
+
* ์ฌ์ค์ด ์ฌ๋ผ์ง๋ค. ์ค๋
์ท๋ง ๋จ๊ธฐ๋ฉด ์ํ๋ ์ด์๋ **์ ๊ทธ๋ ๊ฒ ๋๋์ง**๊ฐ ๋น๋ค(์ ๋์ด ๋ตํ๋ ๊ฒ์ด๋ค).
|
|
3474
|
+
* ์ฐ๊ธฐ๋ ์ด ํจ์๋ฅผ ๊ธฐ๋ค๋ฆฌ์ง ์์ง๋ง(๋น์ฐจ๋จ) ๋ฒํผ๋ ์ฌ๊ธฐ์ ๋น์์ง๋ฏ๋ก ๋ค์ ๊ธฐ๋์ด ๋ ๋ฒ ์ ์ง ์๋๋ค.
|
|
3475
|
+
*/
|
|
3476
|
+
await this.flushJournal(i)
|
|
2524
3477
|
clearInterval(i.timer)
|
|
2525
3478
|
i.unsub()
|
|
2526
3479
|
delete this.instances[runtimeKey(domainId, id)]
|
|
2527
3480
|
await getRepository(TwinInstance)
|
|
2528
3481
|
.update({ domain: { id: i.domainId }, instanceId: id }, { status: 'stopped' })
|
|
2529
|
-
.catch(err =>
|
|
3482
|
+
.catch(err => twinError('twin deregister fail', err))
|
|
2530
3483
|
}
|
|
2531
3484
|
for (const hook of this.stopHooks) { try { hook(id) } catch { /* ํ
๊ฒฉ๋ฆฌ */ } }
|
|
2532
3485
|
}
|
|
@@ -2585,7 +3538,7 @@ export class TwinEngine {
|
|
|
2585
3538
|
const took = performance.now() - t0
|
|
2586
3539
|
recordPhase(load, 'tick', took)
|
|
2587
3540
|
const j = judgeCycle(load, took, this.TICK_MS, Date.now())
|
|
2588
|
-
if (j.warn)
|
|
3541
|
+
if (j.warn) twinWarn(slowTickMessage(id, took, j.budgetMs, load))
|
|
2589
3542
|
this.guardStarvation(domainId, id, took)
|
|
2590
3543
|
}
|
|
2591
3544
|
} catch (err: any) {
|
|
@@ -2635,9 +3588,9 @@ export class TwinEngine {
|
|
|
2635
3588
|
|
|
2636
3589
|
/** ๋ฉ์ถ๊ณ **์ด์ ๋ฅผ ๋จ๊ธด๋ค** โ ์ด์ ์๋ ใ์ ์งใ๋ ์ฌ๋์ด ์๊ธฐ๊ฐ ๋ฉ์ถ ๊ฒ์ผ๋ก ์ฝ๋๋ค. */
|
|
2637
3590
|
private static stopWithNote(domainId: string, id: string, note: StopNote, log: string): void {
|
|
2638
|
-
|
|
3591
|
+
twinError(log)
|
|
2639
3592
|
this.stopNotes.set(runtimeKey(domainId, id), note)
|
|
2640
|
-
this.stop(domainId, id).catch(err =>
|
|
3593
|
+
this.stop(domainId, id).catch(err => twinError(`[twin-engine] stop after guard failed "${id}"`, err?.message ?? err))
|
|
2641
3594
|
}
|
|
2642
3595
|
|
|
2643
3596
|
/** ์ด ํธ์์ด ์ค์ค๋ก ๋ฉ์ถ ์ด์ (์์ผ๋ฉด) โ ํ๋ฉด์ด ์ฎ๊ฒจ ๋งํ๋ค. */
|
|
@@ -2699,7 +3652,7 @@ export class TwinEngine {
|
|
|
2699
3652
|
}
|
|
2700
3653
|
if (ack?.accepted && emitted.length) {
|
|
2701
3654
|
inst!.pendingJournal = [...(inst!.pendingJournal ?? []), ...emitted]
|
|
2702
|
-
inst!.dirty = true // ์ฝ์ผ๋ ์๊ฐ ์ด๋ฒ ์ฃผ๊ธฐ์ ๋น์ฐ๊ณ
|
|
3655
|
+
inst!.dirty = true // ์ฝ์ผ๋ ์๊ฐ ์ด๋ฒ ์ฃผ๊ธฐ์ ๋น์ฐ๊ณ ๋ธ๋ก๋์บ์คํ
๊น์ง ํ๊ฒ ํ๋ค
|
|
2703
3656
|
}
|
|
2704
3657
|
return ack ?? { accepted: false, errorCode: 'unknown-command', error: 'unknown-command' }
|
|
2705
3658
|
}
|
|
@@ -2790,7 +3743,7 @@ export class TwinEngine {
|
|
|
2790
3743
|
instanceId: id,
|
|
2791
3744
|
ingestedTotal: m.ingestedTotal, broadcastTotal: m.broadcastTotal, journaledTotal: m.journaledTotal,
|
|
2792
3745
|
ingestRate: m.ingestRate, broadcastRate: m.broadcastRate, journalRate: m.journalRate,
|
|
2793
|
-
backlog: m.backlog, broadcastCoalesceMs: this.
|
|
3746
|
+
backlog: m.backlog, broadcastCoalesceMs: this.broadcastPeriodMs,
|
|
2794
3747
|
/* ์์
๋ณ ๋ถํ โ ๋ฌด๊ฑฐ์ด ๊ฒ๋ถํฐ. ์ฐ ์ ์ด ์์ผ๋ฉด null(0 ์ผ๋ก ์ฑ์ฐ๋ฉด "๋น ๋ฅด๋ค" ๋ก ์ฝํ๋ค). */
|
|
2795
3748
|
load: loadSummary(this.instances[runtimeKey(domainId, id)]?.load, this.TICK_MS)
|
|
2796
3749
|
}
|
|
@@ -2841,7 +3794,15 @@ export class TwinEngine {
|
|
|
2841
3794
|
|
|
2842
3795
|
return {
|
|
2843
3796
|
tickMs: this.TICK_MS,
|
|
2844
|
-
broadcastCoalesceMs: this.
|
|
3797
|
+
broadcastCoalesceMs: this.broadcastPeriodMs,
|
|
3798
|
+
/* ๋ธ๋ก๋์บ์คํ
์ฃผ๊ธฐ๋ฅผ ๋๋ฆฐ ํ์ โ ๋์ด๋ ์ฃผ๊ธฐ๋ง ๋ณด๋ฉด ใ์๋ ๊ทธ๋ฐ ๊ฐใ์ผ๋ก ์ฝํ๋ค. */
|
|
3799
|
+
broadcastBackoffs: this.broadcastBackoffs,
|
|
3800
|
+
/* ๋ฌผํ ๋ฒ์๋ฅผ ์ขํ์ง ๋ชปํด ์ ๋ถ ๋ง๋ ํ์ โ ์ขํ๊ธฐ๊ฐ ์ค์ ๋ก ๋ฃ๊ณ ์๋์ง๋ฅผ ์ด ๊ฐ์ผ๋ก ๋ณธ๋ค. */
|
|
3801
|
+
broadcastPasses: this.broadcastPasses,
|
|
3802
|
+
broadcastFullPasses: this.broadcastFullPasses,
|
|
3803
|
+
broadcastFullEvery: this.FULL_BROADCAST_EVERY,
|
|
3804
|
+
/* **๋ฃจํ๊ฐ ์ค์ ๋ก ์ผ๋ง๋ ๋งํ๋** โ ํธ์ ๋ถํ์ ๋๋ํ ๋๊ณ ์์ธ์ ๊ฐ๋ฆฐ๋ค(loop-lag.ts ์ฃผ์). */
|
|
3805
|
+
loopLag: loopLag.view(),
|
|
2845
3806
|
...fleetLoad(rows, this.TICK_MS),
|
|
2846
3807
|
/* ์ค๋ง๋ค ์์ธ โ ํ๋ฉด์ด ํผ์ณ ๋ณผ ์ ์๊ฒ. ์ต๊ทผ ๋ถํ ์์๋ fleetLoad ๊ฐ ์ ํ๋ค. */
|
|
2847
3808
|
details: keys.map(key => { const at = parseRuntimeKey(key); return this.load(at.domainId, at.instanceId) }).filter(Boolean)
|
|
@@ -2860,7 +3821,204 @@ export class TwinEngine {
|
|
|
2860
3821
|
if (inst) recordPhase(inst.load ?? (inst.load = newLoadMeter()), phase, tookMs)
|
|
2861
3822
|
}
|
|
2862
3823
|
|
|
2863
|
-
|
|
3824
|
+
/*
|
|
3825
|
+
* โโ ๋๊ธฐํ ๊ฑด๊ฐ ์ฅ๋ถ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
3826
|
+
*
|
|
3827
|
+
* **๋ฐํ์์ด ์๋๋ผ ์์ง์ด ๋ ๋ค.** ์ธ์คํด์ค์ ๋ถ์ด๋ฉด ํธ์์ ์ฌ๊ธฐ๋ํ ๋ ์ฌ๋ผ์ง๋๋ฐ, ์ฌ๋์ด ์๊ณ ์ถ์
|
|
3828
|
+
* ๊ฒ์ ๋ฐ๋ก ๊ทธ ์๊ฐ์ด๋ค โ ใ๊ณ ์ณ์ ๋ค์ ๋์ ๋๋ฐ ์ด์ ํต๊ณผํ๋ใ. ํ๋ก์ธ์ค๊ฐ ์ฌ๋ ๋์ ์ด์ด์ง๋ค.
|
|
3829
|
+
*
|
|
3830
|
+
* ์ง๊ธ์ ๋ฉ๋ชจ๋ฆฌ๋ง์ด๋ค. ์ฐฝ์ด ๋ซํ ๋ ํ ํ์ฉ ์์ํ๋ ๊ฒ์ `onWindowClosed` ํ ์๋ฆฌ์ ๋ถ๋๋ค.
|
|
3831
|
+
*/
|
|
3832
|
+
private static ingestLedgers: Record<string, IngestLedger> = {}
|
|
3833
|
+
|
|
3834
|
+
/** ์ฐฝ์ด ๋ซํ ๋ ๋ถ๋ฅผ ๊ณณ โ ์์ ๊ณ์ธต์ด ๋ถํ
์์ ํ ๋ฒ ๋ฑ๋กํ๋ค(์ปค๋ ์ธต์ ๊ทธ ๊ณ์ธต์ ๋ชจ๋ฅธ๋ค). */
|
|
3835
|
+
private static onWindowClosed?: (at: { domainId: string; instanceId: string }, closed: IngestWindow) => void
|
|
3836
|
+
|
|
3837
|
+
/**
|
|
3838
|
+
* ๋ซํ ์ฐฝ์ ๋ฐ์ ๊ณณ์ ๋ฑ๋กํ๋ค โ **์์(B)์ด ๋ถ๋ ์ ์ผํ ๋ฌธ.**
|
|
3839
|
+
*
|
|
3840
|
+
* ์ด ํจํค์ง๋ ์ ์ฅ ๊ณ์ธต์ ๋ชจ๋ฅธ๋ค. ๋ฑ๋กํ๋ ์ชฝ์ด ์๊ธฐ ๋ฐฉ์์ผ๋ก ์ด๋ค. ํ
์ด ์ค๋ฅ๋ฅผ ๋ด๋ ์ฅ๋ถ๋ ๊ณ์ ๊ตด๋ฌ๊ฐ๋ค
|
|
3841
|
+
* (`rollIngestWindow` ๊ฐ ๊ฐ์ผ๋ค) โ ์์์ ์งํค๋ ค๊ณ ๊ด์ธก์ ๋ฉ์ถ์ง ์๋๋ค.
|
|
3842
|
+
*/
|
|
3843
|
+
static onIngestWindowClosed(
|
|
3844
|
+
fn: (at: { domainId: string; instanceId: string }, closed: IngestWindow) => void
|
|
3845
|
+
): void {
|
|
3846
|
+
this.onWindowClosed = fn
|
|
3847
|
+
}
|
|
3848
|
+
|
|
3849
|
+
/**
|
|
3850
|
+
* ํ ๋ฒ์ ์ธ์ ์คํธ ๊ฒฐ๊ณผ๋ฅผ ์ฅ๋ถ์ ์ ๋๋ค.
|
|
3851
|
+
*
|
|
3852
|
+
* `offered` ๋ **์ ์๋ ๋ ์ฝ๋ ์**๋ค(๊ฑฐ๋ถ ์ฌ๋ถ ๋ฌด๊ด). ํต๊ณผ์จ์ ์๋ก ๋ค๋ฅธ ๋ ๊ณ์๊ธฐ์์ ๋๋ ๊ณ์ฐํ๋ฉด
|
|
3853
|
+
* ๋ถ๋ชจ์ ๋ถ์๊ฐ ๋ค๋ฅธ ๊ฒ์ ์ธ๊ฒ ๋๋ฏ๋ก, ํ์๋ฆฌ์์ ๋ณธ ์๋ฅผ ๊ทธ๋๋ก ๋๊ธด๋ค.
|
|
3854
|
+
*/
|
|
3855
|
+
static recordIngestResult(
|
|
3856
|
+
domainId: string,
|
|
3857
|
+
instanceId: string,
|
|
3858
|
+
offered: number,
|
|
3859
|
+
rejected: { record?: unknown; errors?: string[] }[] | undefined,
|
|
3860
|
+
nowMs = Date.now(),
|
|
3861
|
+
/**
|
|
3862
|
+
* ๋งคํ์ ํต๊ณผํ๋๋ฐ **์ปค๋์ด ๋ฐ์ง ์์** ์(`ingestLive` ์ ๋ฐํ๊ณผ ๊ฒฌ์ฃผ์ด ์ป๋๋ค).
|
|
3863
|
+
*
|
|
3864
|
+
* ใํต๊ณผใ๋ก ์ธ์ง ์๋๋ค โ ํธ์์ด ๋ฉ์ถ ์ฌ์ด ์ฌ์ค์ด ์ฌ๋ผ์ง๋๋ฐ ํ๋ฉด์ด 100% ๋ผ๊ณ ๋งํ๊ฒ ๋๋ค.
|
|
3865
|
+
*/
|
|
3866
|
+
undelivered = 0
|
|
3867
|
+
): void {
|
|
3868
|
+
const key = runtimeKey(domainId, instanceId)
|
|
3869
|
+
const ledger = this.ingestLedgers[key] ?? (this.ingestLedgers[key] = newIngestLedger())
|
|
3870
|
+
recordIngest(
|
|
3871
|
+
ledger,
|
|
3872
|
+
offered,
|
|
3873
|
+
rejected,
|
|
3874
|
+
nowMs,
|
|
3875
|
+
closed => this.onWindowClosed?.({ domainId, instanceId }, closed),
|
|
3876
|
+
undelivered
|
|
3877
|
+
)
|
|
3878
|
+
}
|
|
3879
|
+
|
|
3880
|
+
/**
|
|
3881
|
+
* **์๋ณธ์ ๋ฟ์ง ๋ชปํ๋ค**๋ฅผ ์ ๋๋ค โ ใ๋ฐ์ ๊ฒ์ด ์๋คใ์ ๊ฐ๋ฅธ๋ค(ยง`recordReadFailure`).
|
|
3882
|
+
*
|
|
3883
|
+
* ์ด ๋ฌธ์ด ์๋ ๋์ ์ค ์๋ณธ์ด ๋๊ฒจ๋ ํธ์์ ์กฐํ ๊ฐ๋ฅํ ์ํ์ ๊ทธ ์ฌ์ค์ด ์์๋ค. ํ๋ฉด์ด ๋ณผ ์ ์๋
|
|
3884
|
+
* ๊ฒ์ ใ์ ์ฌ์ค์ด ์๋คใ๋ฟ์ด์๊ณ ๊ทธ๊ฒ์ ใ์๋ณธ์ด ์กฐ์ฉํ๋คใ์ ๊ตฌ๋ณ๋์ง ์๋๋ค โ ์ค์ฆ ์ค์ ์๋ณธ์ด
|
|
3885
|
+
* ๋๊ธฐ๋ฉด ์ฌ์ฉ์๊ฐ ์์ธ์ ์ฐพ์ ์ ์๋ค.
|
|
3886
|
+
*
|
|
3887
|
+
* ๋ก๊ทธ๋ก๋ ๋งํ๊ณ ์์๋ค(์ด๋ํฐ๊ฐ ์ฌ์๋๋ฅผ ๊ฒฝ๊ณ ํ๋ค). ๊ทธ๋ฌ๋ **๋ก๊ทธ๋ ์ฌ๋์ด ๋ณผ ๋๋ง ๊ฐ์ด ์๋ค** โ
|
|
3888
|
+
* ํ๋ฉด์ด ๋งํ๋ ค๋ฉด ์ํ์ ์์ด์ผ ํ๋ค.
|
|
3889
|
+
*/
|
|
3890
|
+
static recordIngestReadFailure(domainId: string, instanceId: string, reason: string, nowMs = Date.now(), stream?: string): void {
|
|
3891
|
+
const key = runtimeKey(domainId, instanceId)
|
|
3892
|
+
const ledger = this.ingestLedgers[key] ?? (this.ingestLedgers[key] = newIngestLedger())
|
|
3893
|
+
recordReadFailure(ledger, reason, nowMs, stream)
|
|
3894
|
+
}
|
|
3895
|
+
|
|
3896
|
+
/**
|
|
3897
|
+
* ์ฝ๊ธฐ๊ฐ ์ฑ๊ณตํ๋ค โ ๋จ์ ๊ธฐ๋ก์ ์ง์ด๋ค.
|
|
3898
|
+
*
|
|
3899
|
+
* **๋น ์ฝ๊ธฐ๋ ์ฑ๊ณต์ด๋ค.** ์๋ณธ์ด ใ์ ๊ฒ์ด ์๋คใ๊ณ ๋ตํ ๊ฒ์ ๋ฟ์๋ค๋ ๋ป์ด๋ฏ๋ก, ๊ทธ๋๋ ๋ถ๋ฅธ๋ค.
|
|
3900
|
+
* ๊ทธ ๋์ ๊ฐ๊ฒ ๋๋ฉด ์กฐ์ฉํ ์๋ณธ์ด ๋๊ธด ์๋ณธ์ผ๋ก ๋ณด์ธ๋ค.
|
|
3901
|
+
*/
|
|
3902
|
+
static clearIngestReadFailure(domainId: string, instanceId: string): void {
|
|
3903
|
+
const ledger = this.ingestLedgers[runtimeKey(domainId, instanceId)]
|
|
3904
|
+
if (ledger) clearReadFailure(ledger)
|
|
3905
|
+
}
|
|
3906
|
+
|
|
3907
|
+
/**
|
|
3908
|
+
* **์ฝ์๋๋ฐ ์ฐฝ์ ๋๊ธธ ์ ์๋ค**๋ฅผ ์ ๋๋ค โ ์์ **์กฐ์น๊ฐ ๋ฐ๋์ธ** ์ฌ์ค์ด๋ค(ยง`recordCursorStall`).
|
|
3909
|
+
*
|
|
3910
|
+
* ์ด ๋ฌธ์ด ์๋ ๋์ ์ด ์ฌ์ค์ด `recordIngestReadFailure` ๋ก ๋๊ฐ๋ค. ๊ทธ๋์ ํ๋ฉด์ด ใ์๋ณธ์ ๋ฟ์ง
|
|
3911
|
+
* ๋ชปํ๋คใ๊ณ ๋งํ๋๋ฐ ์๋ณธ์ **๋ตํ** ์ํ์๊ณ , ๊ทธ ๋ต์ ๋ชจ์์ด ์ปค์๋ฅผ ์ด๊ธด ๊ฒ์ด์๋ค(ํ ์๊ฐ์ ํ
|
|
3912
|
+
* ํ์ด์ง๋ณด๋ค ๋ง์ ํ). ์ฌ๋์ ์๋ณธ์ ์์ฌํ๊ณ ๊ธฐ๋ค๋ฆฌ๋๋ฐ **๊ธฐ๋ค๋ฆผ์ผ๋ก๋ ์์ํ ํ๋ฆฌ์ง ์๋๋ค.**
|
|
3913
|
+
*
|
|
3914
|
+
* ์กฐ์น๊ฐ ๋ฐ๋์ธ ๋ ์ฌ์ค์ ํ ์ด๋ฆ์ผ๋ก ๋ถ๋ฅด๋ฉด ๊ทธ ์ด๋ฆ์ ์ ๋ณด๊ฐ ์๋๋ผ ์คํด๋ค.
|
|
3915
|
+
*/
|
|
3916
|
+
static recordIngestCursorStall(domainId: string, instanceId: string, reason: string, nowMs = Date.now(), stream?: string): void {
|
|
3917
|
+
const key = runtimeKey(domainId, instanceId)
|
|
3918
|
+
const ledger = this.ingestLedgers[key] ?? (this.ingestLedgers[key] = newIngestLedger())
|
|
3919
|
+
recordCursorStall(ledger, reason, nowMs, stream)
|
|
3920
|
+
}
|
|
3921
|
+
|
|
3922
|
+
/** ์ฐฝ์ ๋๊ฒผ๋ค โ ์ ์ฒด ๊ธฐ๋ก์ ์ง์ด๋ค(ํ๋ฆฐ ์ ์ฒด๊ฐ ํ๋ฉด์ ๋จ์ ์์ผ๋ฉด ๊ทธ๊ฒ๋ ๊ฑฐ์ง์ด๋ค). */
|
|
3923
|
+
static clearIngestCursorStall(domainId: string, instanceId: string): void {
|
|
3924
|
+
const ledger = this.ingestLedgers[runtimeKey(domainId, instanceId)]
|
|
3925
|
+
if (ledger) clearCursorStall(ledger)
|
|
3926
|
+
}
|
|
3927
|
+
|
|
3928
|
+
/**
|
|
3929
|
+
* ์ ๋์ **์ ์ ๊ฒ**์ ๊ฐ์ ์ฅ๋ถ์ ๋จ๊ธด๋ค โ ์ ์
๊ณผ ๊ฐ์ 10๋ถ ์ฐฝ์.
|
|
3930
|
+
*
|
|
3931
|
+
* โโ ์ ์ ์
์ฅ๋ถ์ ๋ฃ๋ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
3932
|
+
* ์ ์ฅ๋ถ๋ฅผ ๋ง๋ค๋ฉด ใ๋ซํ ์ฐฝ๋ง ์ต๊ทผใยทใ0 ๊ณผ ์์์ ๊ฐ๋ฅธ๋คใยท์์์ ๋ ๋ฒ ์ง์ผ์ผ ํ๊ณ , ๊ทธ์ค ํ ๋ฒ๋ง
|
|
3933
|
+
* ๊ณ ์ณ์ง๋ ๊ฒ์ด ๋ณดํต์ด๋ค. ์ ์
์ฐฝ์ ๊ทธ ๊ท์จ์ด ์ด๋ฏธ ๋ค์ด ์๊ณ ๋ซํ ๋ ํ์ผ๋ก ๋จ๋๋ค.
|
|
3934
|
+
*
|
|
3935
|
+
* โโ ์ด ๊ฐ์ด ์์ผ๋ฉด ๋ฌด์์ ๋ชป ๋ณด๋ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
3936
|
+
* ๊ณ๊ธฐ(`journalRate`)๋ **์ง๊ธ**๋ง ๋ตํ๋ค. ํ๋ก์ธ์ค๋ฅผ ๋ค์ ๋์ฐ๋ฉด 0 ์์ ์์ํ๋ฏ๋ก ใ์ด์ ์ด ์๊ฐ์๋
|
|
3937
|
+
* ์ด๋ฌ๋ใยทใ๊ณ ์น ๋ค๋ก ์ค์๋ใ๋ฅผ ๋ฌผ์ ์๋ฆฌ๊ฐ ์๋ค. 2026-08-20 ์ ๊ณ ์น ๊ฒ์ด ๋ฐ๋ก ์ฐ๊ธฐ ๊ฒฝ๋ก์ด๋ฏ๋ก,
|
|
3938
|
+
* ๋๋์๊ฐ๋ ๊ฒ์ ๋ณผ ์ ์์ด์ผ ํ๋ค.
|
|
3939
|
+
*/
|
|
3940
|
+
static recordJournalWrite(domainId: string, instanceId: string, rows: number, nowMs = Date.now()): void {
|
|
3941
|
+
const key = runtimeKey(domainId, instanceId)
|
|
3942
|
+
const ledger = this.ingestLedgers[key] ?? (this.ingestLedgers[key] = newIngestLedger())
|
|
3943
|
+
recordLedgerWrite(ledger, rows, nowMs, closed => this.onWindowClosed?.({ domainId, instanceId }, closed))
|
|
3944
|
+
}
|
|
3945
|
+
|
|
3946
|
+
/**
|
|
3947
|
+
* ใ์ด ํธ์์ด ํ์ฅ๊ณผ ๋ง์ถฐ์ง๊ณ ์๋ใ โ ํ๋ ํ์ + ์ถ์ด + ์ฌ์ + ํ๋ณธ.
|
|
3948
|
+
*
|
|
3949
|
+
* ์กฐํํ ๋ ์ฐฝ์ ํ ๋ฒ ๊ตด๋ฆฐ๋ค: ์ ์
์ด ๋ฉ์ถ๋ฉด ๋ค์ ์ธ์ ์คํธ๊ฐ ์์ด ์ฐฝ์ด ์์ํ ๋ซํ์ง ์๋๋ฐ, ๊ทธ๋ฌ๋ฉด
|
|
3950
|
+
* ใ์ต๊ทผใ์ด ์๊ฒ์ผ๋ก ๋จ๋๋ค. ํ์ด๋จธ๋ฅผ ๋์ง ์๋ ์ด์ ๋ ํธ์๋ง๋ค ํ์ด๋จธ๋ฅผ ๊ฑธ๋ฉด ๊ทธ ํ์ด๋จธ๋ค์ด ๋ค์
|
|
3951
|
+
* ๋ฉ์ธ ๋ฃจํ์ ์นํ๊ธฐ ๋๋ฌธ์ด๋ค(์ค๋ ํ์ธํ ๊ทธ ๋ถํ๋ฅผ ์ด ๊ธฐ๋ฅ์ด ๋ค์ ๋ง๋ค ์ด์ ๊ฐ ์๋ค).
|
|
3952
|
+
*/
|
|
3953
|
+
/**
|
|
3954
|
+
* ๋๋ฉ์ธ์ ํธ์๋ง๋ค **ํ ์ค ์์ฝ** โ ๋ชฉ๋ก ํ๋ฉด๊ณผ ๋ฏธ๋ ์ถ์ด์ฉ.
|
|
3955
|
+
*
|
|
3956
|
+
* โโ ์ ์์ธ์ ๋ฐ๋ก์ธ๊ฐ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
3957
|
+
* `ingestHealthOf` ๋ ์ฌ์ ๋ฌธ๊ตฌ์ **๋ ์ฝ๋ ์๋ฌธ ํ๋ณธ**๊น์ง ๋ธ๋ค. ๋ชฉ๋ก์ ํธ์์ด ์ค๋ฌด ๊ฐ๋ฉด ๊ทธ ์๋ฌธ์ด
|
|
3958
|
+
* ์ค๋ฌด ๋ฒ ์ค๋ ค ์กฐํ๊ฐ ๋ฌด๊ฑฐ์์ง๊ณ , ํ๋ฉด์ ์ด์ฐจํผ ๊ทธ๊ฒ์ ๊ทธ๋ฆฌ์ง ์๋๋ค. ๊ทธ๋์ ์ฌ๊ธฐ์๋ **์ซ์๋ง** ๋ธ๋ค.
|
|
3959
|
+
*
|
|
3960
|
+
* ์ถ์ด๋ ์ฐฝ๋น ์ซ์ ๋ท์ด๋ผ ์คํํฌ๋ผ์ธ ํ๋์ ์ถฉ๋ถํ๊ณ ๊ฐ๋ณ๋ค.
|
|
3961
|
+
*
|
|
3962
|
+
* โโ ๋ฑ๋ก๋ ํธ์์ ๊ธฐ์ค์ผ๋ก ํ๋๋ค โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
3963
|
+
* ๋๋ ํธ์๋ง ํ์ผ๋ฉด ใ๊ณ ์น๋ ค๊ณ ๋ฉ์ถฐ ๋ ํธ์ใ์ด ๋ชฉ๋ก์์ ์ฌ๋ผ์ง๋ค โ ์ฌ๋์ด ๋ฐฉ๊ธ ๋ฉ์ถ ๊ทธ๊ฒ์ ๋ณด๋ ค๊ณ
|
|
3964
|
+
* ๋ชฉ๋ก์ ์ฌ๋๋ฐ ์์ผ๋ฉด ๊ฒฐํจ์ผ๋ก ์ฝ๋๋ค. ๊ทธ๋์ ํธ์ถ๋ถ(๋ชฉ๋ก ํ๋ฉด)๊ฐ ์๋ instanceId ๋ค์ ๋ฐ๋๋ค.
|
|
3965
|
+
*/
|
|
3966
|
+
static ingestHealthBrief(
|
|
3967
|
+
domainId: string,
|
|
3968
|
+
instanceIds: string[]
|
|
3969
|
+
): { instanceId: string; verdict: string; acceptedRatio: number | null; trend: { offered: number; rejected: number; undelivered: number; rows: number }[]; lastAt: string | null }[] {
|
|
3970
|
+
return instanceIds.map(instanceId => {
|
|
3971
|
+
const full = this.ingestHealthOf(domainId, instanceId)
|
|
3972
|
+
return {
|
|
3973
|
+
instanceId,
|
|
3974
|
+
verdict: full.verdict,
|
|
3975
|
+
/* ์ต๊ทผ **๋ซํ** ์ฐฝ์ ํต๊ณผ์จ. ์ฐฝ์ด ์ ๋ซํ์ผ๋ฉด ๋น์ด๋ค โ 0 ์ด๋ 1 ๋ก ์ฑ์ฐ์ง ์๋๋ค. */
|
|
3976
|
+
acceptedRatio: full.recent?.acceptedRatio ?? null,
|
|
3977
|
+
/*
|
|
3978
|
+
* ์คํํฌ๋ผ์ธ์ฉ โ ์ฐฝ๋ง๋ค ์
. **๋ฏธ์ ๋ฌ์ ๋นผ๋์ผ๋ฉด ์คํํฌ๋ผ์ธ์ด ๊ฑฐ์ง์ ๊ทธ๋ฆฐ๋ค**: ๋ฒ๋ ค์ง ๊ฒ์
|
|
3979
|
+
* ํต๊ณผ๋ก ์ธ๋ฉด ํธ์์ด ๋ฉ์ถ ๊ตฌ๊ฐ์์๋ ์ ์ด 100% ์ ๋ถ๋๋ค.
|
|
3980
|
+
*/
|
|
3981
|
+
/* ์ ํ ํ๋ ํจ๊ป โ ์๋ฎฌ ํธ์์ ์ ์
์ด 0 ์ด๋ผ ์ด ๊ฐ ์์ด๋ ์ถ์ด๊ฐ ๋น ์ ์ผ๋ก ๋ณด์ธ๋ค. */
|
|
3982
|
+
trend: full.trend.map(w => ({ offered: w.offered, rejected: w.rejected, undelivered: w.undelivered, rows: w.rows })),
|
|
3983
|
+
lastAt: full.lastAt
|
|
3984
|
+
}
|
|
3985
|
+
})
|
|
3986
|
+
}
|
|
3987
|
+
|
|
3988
|
+
/**
|
|
3989
|
+
* ํ ํธ์์ ๋๊ธฐํ ๊ฑด๊ฐ.
|
|
3990
|
+
*
|
|
3991
|
+
* โโ ์กฐํ๊ฐ ์ฅ๋ถ๋ฅผ ์ ์ง์ํจ๋ค(์๋) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
3992
|
+
* ์ฐฝ์ **๋ค์ ์ ์
์ด ์์ ๋** ๋ซํ๋ค. ๊ทธ๋์ ํผ๋๊ฐ ์ฃฝ์ผ๋ฉด ๋ง์ง๋ง ์ฐฝ์ด ์ด๋ฆฐ ์ฑ๋ก ๋จ์ ์์ํ ์์๋์ง
|
|
3993
|
+
* ์๊ณ , ์ถ์ด์๋ ๋ค์ด๊ฐ์ง ์๋๋ค. ์กฐํ ์์ ์ ํ ๋ฒ ๊ตด๋ ค ์ฃผ๋ฉด ๊ทธ ๋ง์ง๋ง ์ฐฝ์ด ๋ซํ๋ฉด์ ์์ ํ
๋ ๋ถ๋ฆฐ๋ค
|
|
3994
|
+
* โ ์ฆ **์ฃฝ์ ํผ๋์ ๋ง์ง๋ง ๊ตฌ๊ฐ์ ์์ง ์๊ธฐ ์ํด** ์ฝ๊ธฐ๊ฐ ๊ตด๋ฆฐ๋ค. ๋ถ์ํจ๊ณผ์ง๋ง ํ์ํ ๋ถ์ํจ๊ณผ๋ค.
|
|
3995
|
+
*
|
|
3996
|
+
* ์๊ฐ์ **ํ ๋ฒ๋ง ์ฝ์ด** ๊ตด๋ฆฌ๊ธฐ์ ํ์ ์ ๊ฐ์ ๊ฐ์ ์ด๋ค. ๋ ๋ฒ ์ฝ์ผ๋ฉด ๊ทธ ์ฌ์ด์ ์ฐฝ์ด ๋ซํ ํ์ ์ด
|
|
3997
|
+
* ๊ตด๋ฆฌ๊ธฐ ์ ์ํ๋ฅผ ๋ณด๋ ์ผ์ด ์๊ธด๋ค.
|
|
3998
|
+
*/
|
|
3999
|
+
static ingestHealthOf(domainId: string, instanceId: string): IngestHealthView {
|
|
4000
|
+
const key = runtimeKey(domainId, instanceId)
|
|
4001
|
+
const nowMs = Date.now()
|
|
4002
|
+
const ledger = this.ingestLedgers[key]
|
|
4003
|
+
if (ledger) {
|
|
4004
|
+
rollIngestWindow(ledger, nowMs, undefined, closed => this.onWindowClosed?.({ domainId, instanceId }, closed))
|
|
4005
|
+
}
|
|
4006
|
+
|
|
4007
|
+
const inst = this.instances[key]
|
|
4008
|
+
const feedState = liveFeedStateOf({
|
|
4009
|
+
restartPolicy: inst?.restartPolicy,
|
|
4010
|
+
running: !!inst,
|
|
4011
|
+
instanceId
|
|
4012
|
+
})
|
|
4013
|
+
return ingestHealth(ledger, feedState, undefined, nowMs)
|
|
4014
|
+
}
|
|
4015
|
+
|
|
4016
|
+
/**
|
|
4017
|
+
* ๋๋ ์ธ์คํด์ค ์ ์ฒด์ ๊ณ์ธก(๋ชจ๋ํฐ ๋์๋ณด๋์ฉ) โ **์๋ฎฌ๊ณผ ๋ฏธ๋ฌ๋ฅผ ํจ๊ป**.
|
|
4018
|
+
*
|
|
4019
|
+
* ์์ ์๋ ์๋ฎฌ์ ๊ณ๊ธฐ๊ฐ ์์ด ์ด ๋ชฉ๋ก์์ ์กฐ์ฉํ ๋น ์ก๋ค(๊ณ๊ธฐ๊ฐ `null` ์ด๋ผ ๊ฑธ๋ฌ์ก๋ค). ๋๋ ํธ์
|
|
4020
|
+
* ๋๋ถ๋ถ์ด ์๋ฎฌ์ธ ์๋ฒ์์ ๊ทธ ๋ชฉ๋ก์ ใ๋ถํ๊ฐ ๊ฑฐ์ ์๋คใ๋ก ๋ณด์๋ค.
|
|
4021
|
+
*/
|
|
2864
4022
|
static async allMetrics(domainId?: string): Promise<any[]> {
|
|
2865
4023
|
/* ๋๋ฉ์ธ ์์ด ๋ถ๋ฅด๋ฉด ์ ํ
๋ํธ๋ฅผ ํ๋๋ค(๋ด๋ถ ๋ชจ๋ํฐ์ฉ) โ ํค์์ ๋๋ฉ์ธ์ ๋๋๋ ค ๊ฐ์์๊ฒ ๋ฌป๋๋ค. */
|
|
2866
4024
|
const rows = Object.keys(this.instances)
|
|
@@ -2913,6 +4071,14 @@ export class TwinEngine {
|
|
|
2913
4071
|
const state = await this.recover(domainId, instanceId, undefined, untilTime).catch(() => null)
|
|
2914
4072
|
if (!state) return null
|
|
2915
4073
|
const cutoff = untilTime != null ? Date.parse(untilTime) : Infinity
|
|
4074
|
+
/*
|
|
4075
|
+
* journal-fold: ์ค๋์ ๋ง์ง๋ง ์ํ๋ ๊ทธ ์ค๋์ ์ผ์ด๋ ์ฌ์ค๋ค์ ์ ์ด์ผ ๋์จ๋ค.
|
|
4076
|
+
*
|
|
4077
|
+
* **์์ด์ง ์กฐ๊ฑด** โ ์ง๊ธ ์ด ์ฝ๊ธฐ์๋ ์ํ์ด ์๊ณ `eventType` ์ ์ธ๋ฑ์ค๋ ์๋ค. ๊ทธ๋์ ์ ๋์ด ํฐ
|
|
4078
|
+
* ํธ์์์๋ ์ด ํ ๋ฒ์ด ๊ทธ ํธ์์ ์ ๋ ์ ์ฒด ์ฃผ์ฌ๋ค. ์ค๋ ์ํ๋ฅผ ๋ฐ๋ก ์ ์ด ๋ ํ(ํฌ์)๋ฅผ ๋๊ฑฐ๋,
|
|
4079
|
+
* `(domain, instanceId, eventType, revision)` ์ธ๋ฑ์ค๋ฅผ ๋ถ์ด๊ณ ์ปค์๋ฅผ ๋ฃ์ผ๋ฉด ์ด ์์ธ๋ ์ฌ๋ผ์ง๋ค.
|
|
4080
|
+
* ์๊ฐ ์ํ(`cutoff`)์ SQL ๋ก ๋ด๋ ค๋ ์ ๋ฐ์ ์ค๋ค.
|
|
4081
|
+
*/
|
|
2916
4082
|
const rows = await getRepository(TwinEvent).find({ where: { domain: { id: domainId }, instanceId, eventType: OP_EVENT.order }, order: { revision: 'ASC' } })
|
|
2917
4083
|
const latest = new Map<string, any>()
|
|
2918
4084
|
for (const r of rows) {
|