@things-factory/headless-twin 10.0.15 → 10.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-server/engine/attention-digest.d.ts +4 -4
- package/dist-server/engine/attention-digest.js +7 -7
- package/dist-server/engine/attention-digest.js.map +1 -1
- package/dist-server/engine/canonical-ingest.d.ts +18 -82
- package/dist-server/engine/canonical-ingest.js +183 -18
- 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/declared-stimulus.js +1 -1
- package/dist-server/engine/declared-stimulus.js.map +1 -1
- package/dist-server/engine/energy-topology.js +4 -4
- package/dist-server/engine/energy-topology.js.map +1 -1
- package/dist-server/engine/ingest-dedupe.d.ts +38 -0
- package/dist-server/engine/ingest-dedupe.js +145 -0
- package/dist-server/engine/ingest-dedupe.js.map +1 -0
- package/dist-server/engine/ingest-health.d.ts +90 -10
- package/dist-server/engine/ingest-health.js +125 -16
- package/dist-server/engine/ingest-health.js.map +1 -1
- package/dist-server/engine/integration-probes.d.ts +3 -3
- package/dist-server/engine/integration-probes.js +6 -6
- package/dist-server/engine/integration-probes.js.map +1 -1
- package/dist-server/engine/kpi-baseline.js +4 -4
- package/dist-server/engine/kpi-baseline.js.map +1 -1
- package/dist-server/engine/kpi-fold.d.ts +212 -14
- package/dist-server/engine/kpi-fold.js +374 -21
- package/dist-server/engine/kpi-fold.js.map +1 -1
- package/dist-server/engine/kpi-query.d.ts +11 -11
- package/dist-server/engine/kpi-query.js +281 -36
- package/dist-server/engine/kpi-query.js.map +1 -1
- package/dist-server/engine/live-feed-registry.d.ts +1 -1
- package/dist-server/engine/live-feed-registry.js +1 -1
- 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.map +1 -1
- package/dist-server/engine/local-declarations.d.ts +7 -7
- package/dist-server/engine/local-declarations.js +15 -14
- package/dist-server/engine/local-declarations.js.map +1 -1
- package/dist-server/engine/log.d.ts +1 -1
- package/dist-server/engine/log.js +1 -1
- package/dist-server/engine/log.js.map +1 -1
- package/dist-server/engine/measured-estimator.js +1 -1
- package/dist-server/engine/measured-estimator.js.map +1 -1
- package/dist-server/engine/measured-yield.js +1 -1
- package/dist-server/engine/measured-yield.js.map +1 -1
- package/dist-server/engine/model-basis.d.ts +1 -1
- package/dist-server/engine/model-basis.js +1 -1
- package/dist-server/engine/model-basis.js.map +1 -1
- package/dist-server/engine/model-gap.d.ts +3 -3
- package/dist-server/engine/model-gap.js +1 -1
- package/dist-server/engine/model-gap.js.map +1 -1
- package/dist-server/engine/model-vocabulary.js +3 -3
- package/dist-server/engine/model-vocabulary.js.map +1 -1
- package/dist-server/engine/oee-accumulator.d.ts +6 -5
- package/dist-server/engine/oee-accumulator.js +6 -5
- package/dist-server/engine/oee-accumulator.js.map +1 -1
- package/dist-server/engine/property-effects.js +2 -2
- package/dist-server/engine/property-effects.js.map +1 -1
- package/dist-server/engine/read-time.d.ts +22 -0
- package/dist-server/engine/read-time.js +81 -0
- package/dist-server/engine/read-time.js.map +1 -0
- package/dist-server/engine/restart-policy.js +1 -1
- 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/spec-coverage.d.ts +2 -2
- package/dist-server/engine/spec-coverage.js +1 -1
- package/dist-server/engine/spec-coverage.js.map +1 -1
- package/dist-server/engine/stage-path.d.ts +4 -4
- package/dist-server/engine/stage-path.js +1 -1
- package/dist-server/engine/stage-path.js.map +1 -1
- package/dist-server/engine/state-axes.d.ts +1 -1
- package/dist-server/engine/state-axes.js +2 -2
- package/dist-server/engine/state-axes.js.map +1 -1
- package/dist-server/engine/travel-estimator.d.ts +2 -2
- package/dist-server/engine/travel-estimator.js +1 -1
- package/dist-server/engine/travel-estimator.js.map +1 -1
- package/dist-server/engine/twin-engine.d.ts +307 -54
- package/dist-server/engine/twin-engine.js +822 -196
- package/dist-server/engine/twin-engine.js.map +1 -1
- package/dist-server/engine/warm-start.d.ts +2 -2
- package/dist-server/engine/warm-start.js +5 -5
- package/dist-server/engine/warm-start.js.map +1 -1
- package/dist-server/index.js +1 -1
- package/dist-server/index.js.map +1 -1
- package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.js +1 -1
- package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.js.map +1 -1
- package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.js +3 -3
- package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.js.map +1 -1
- package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.js +2 -2
- package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.js.map +1 -1
- package/dist-server/migrations/1786300000000-IndexEventTypeByTime.d.ts +5 -0
- package/dist-server/migrations/1786300000000-IndexEventTypeByTime.js +54 -0
- package/dist-server/migrations/1786300000000-IndexEventTypeByTime.js.map +1 -0
- package/dist-server/migrations/1786400000000-IndexEventCreatedAt.d.ts +5 -0
- package/dist-server/migrations/1786400000000-IndexEventCreatedAt.js +44 -0
- package/dist-server/migrations/1786400000000-IndexEventCreatedAt.js.map +1 -0
- package/dist-server/migrations/1786500000000-CreateTwinSubjectEvents.d.ts +6 -0
- package/dist-server/migrations/1786500000000-CreateTwinSubjectEvents.js +84 -0
- package/dist-server/migrations/1786500000000-CreateTwinSubjectEvents.js.map +1 -0
- package/dist-server/migrations/index.js +7 -1
- package/dist-server/migrations/index.js.map +1 -1
- package/dist-server/routes.d.ts +1 -0
- package/dist-server/routes.js +69 -5
- package/dist-server/routes.js.map +1 -1
- package/dist-server/service/index.d.ts +2 -2
- package/dist-server/service/index.js +30 -24
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/reference/control-routing.d.ts +1 -1
- package/dist-server/service/reference/control-routing.js +1 -1
- package/dist-server/service/reference/control-routing.js.map +1 -1
- package/dist-server/service/reference/discovery-result.d.ts +2 -2
- package/dist-server/service/reference/discovery-result.js +3 -3
- package/dist-server/service/reference/discovery-result.js.map +1 -1
- package/dist-server/service/reference/hook-contract.d.ts +43 -0
- package/dist-server/service/reference/hook-contract.js +59 -0
- package/dist-server/service/reference/hook-contract.js.map +1 -0
- package/dist-server/service/reference/ingest-space.d.ts +1 -1
- package/dist-server/service/reference/ingest-space.js +4 -4
- package/dist-server/service/reference/ingest-space.js.map +1 -1
- package/dist-server/service/reference/knob-defaults.d.ts +1 -1
- package/dist-server/service/reference/knob-defaults.js +4 -4
- package/dist-server/service/reference/knob-defaults.js.map +1 -1
- package/dist-server/service/reference/reference-adapter.d.ts +116 -9
- package/dist-server/service/reference/reference-adapter.js +27 -0
- package/dist-server/service/reference/reference-adapter.js.map +1 -1
- package/dist-server/service/reference/reference-hook.d.ts +20 -0
- package/dist-server/service/reference/reference-hook.js +133 -0
- package/dist-server/service/reference/reference-hook.js.map +1 -0
- package/dist-server/service/reference/reference-live.d.ts +2 -2
- package/dist-server/service/reference/reference-live.js +76 -13
- package/dist-server/service/reference/reference-live.js.map +1 -1
- package/dist-server/service/reference/reference-master.d.ts +25 -16
- package/dist-server/service/reference/reference-master.js +35 -13
- package/dist-server/service/reference/reference-master.js.map +1 -1
- package/dist-server/service/reference/reference-resolver.d.ts +1 -1
- package/dist-server/service/reference/reference-resolver.js +12 -12
- package/dist-server/service/reference/reference-resolver.js.map +1 -1
- package/dist-server/service/reference/template-registry.d.ts +1 -1
- package/dist-server/service/reference/template-registry.js.map +1 -1
- package/dist-server/service/reference/twin-reference.d.ts +3 -3
- package/dist-server/service/reference/twin-reference.js.map +1 -1
- package/dist-server/service/twin-attention/twin-attention-query.d.ts +1 -1
- package/dist-server/service/twin-attention/twin-attention-query.js +1 -1
- package/dist-server/service/twin-attention/twin-attention-query.js.map +1 -1
- package/dist-server/service/twin-audit/command-audit.d.ts +1 -1
- package/dist-server/service/twin-audit/command-audit.js.map +1 -1
- package/dist-server/service/twin-audit/twin-audit-event.d.ts +1 -1
- package/dist-server/service/twin-audit/twin-audit-event.js.map +1 -1
- package/dist-server/service/twin-audit/twin-audit-query.js +1 -1
- package/dist-server/service/twin-audit/twin-audit-query.js.map +1 -1
- package/dist-server/service/twin-backfill/backfill-period-facts.d.ts +24 -0
- package/dist-server/service/twin-backfill/backfill-period-facts.js +96 -0
- package/dist-server/service/twin-backfill/backfill-period-facts.js.map +1 -0
- package/dist-server/service/twin-backfill/backfill-shape.d.ts +34 -0
- package/dist-server/service/twin-backfill/backfill-shape.js +75 -0
- package/dist-server/service/twin-backfill/backfill-shape.js.map +1 -0
- package/dist-server/service/twin-backfill/index.d.ts +2 -0
- package/dist-server/service/twin-backfill/index.js +6 -0
- package/dist-server/service/twin-backfill/index.js.map +1 -0
- package/dist-server/service/twin-backfill/twin-backfill-resolver.d.ts +10 -0
- package/dist-server/service/twin-backfill/twin-backfill-resolver.js +44 -0
- package/dist-server/service/twin-backfill/twin-backfill-resolver.js.map +1 -0
- package/dist-server/service/twin-control/twin-control-mutation.js +3 -3
- package/dist-server/service/twin-control/twin-control-mutation.js.map +1 -1
- package/dist-server/service/twin-event/twin-event-keys.d.ts +1 -1
- package/dist-server/service/twin-event/twin-event-keys.js +2 -2
- package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
- package/dist-server/service/twin-event/twin-event-type.d.ts +1 -0
- package/dist-server/service/twin-event/twin-event-type.js +8 -1
- package/dist-server/service/twin-event/twin-event-type.js.map +1 -1
- package/dist-server/service/twin-event/twin-event.d.ts +2 -2
- package/dist-server/service/twin-event/twin-event.js +32 -4
- package/dist-server/service/twin-event/twin-event.js.map +1 -1
- package/dist-server/service/twin-forecast/forecast-metrics.js +1 -1
- package/dist-server/service/twin-forecast/forecast-metrics.js.map +1 -1
- package/dist-server/service/twin-forecast/gap-analytics.d.ts +1 -1
- package/dist-server/service/twin-forecast/gap-analytics.js.map +1 -1
- package/dist-server/service/twin-forecast/twin-forecast-query.js +29 -11
- package/dist-server/service/twin-forecast/twin-forecast-query.js.map +1 -1
- package/dist-server/service/twin-ingest-window/twin-ingest-window-query.d.ts +1 -1
- package/dist-server/service/twin-ingest-window/twin-ingest-window-query.js +1 -1
- package/dist-server/service/twin-ingest-window/twin-ingest-window-query.js.map +1 -1
- package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.js +1 -1
- package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.js.map +1 -1
- package/dist-server/service/twin-instance/twin-instance.js.map +1 -1
- package/dist-server/service/twin-journal/event-type-split-shape.d.ts +50 -0
- package/dist-server/service/twin-journal/event-type-split-shape.js +111 -0
- package/dist-server/service/twin-journal/event-type-split-shape.js.map +1 -0
- package/dist-server/service/twin-journal/event-type-split.d.ts +15 -0
- package/dist-server/service/twin-journal/event-type-split.js +102 -0
- package/dist-server/service/twin-journal/event-type-split.js.map +1 -0
- package/dist-server/service/twin-journal/journal-order.d.ts +15 -0
- package/dist-server/service/twin-journal/journal-order.js +17 -0
- package/dist-server/service/twin-journal/journal-order.js.map +1 -0
- package/dist-server/service/twin-journal/twin-journal-query.d.ts +5 -5
- package/dist-server/service/twin-journal/twin-journal-query.js +232 -21
- package/dist-server/service/twin-journal/twin-journal-query.js.map +1 -1
- package/dist-server/service/twin-lifecycle/domain-catalog.js +2 -1
- package/dist-server/service/twin-lifecycle/domain-catalog.js.map +1 -1
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.d.ts +27 -1
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +59 -3
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js.map +1 -1
- package/dist-server/service/twin-metrics/twin-metrics-query.js +14 -1
- package/dist-server/service/twin-metrics/twin-metrics-query.js.map +1 -1
- package/dist-server/service/twin-model/axis-journal-evidence.js +1 -1
- package/dist-server/service/twin-model/axis-journal-evidence.js.map +1 -1
- package/dist-server/service/twin-model/epcis-coverage.js +34 -3
- package/dist-server/service/twin-model/epcis-coverage.js.map +1 -1
- package/dist-server/service/twin-model/iec61850-coverage.js +1 -1
- package/dist-server/service/twin-model/iec61850-coverage.js.map +1 -1
- package/dist-server/service/twin-model/isa95-coverage.d.ts +12 -9
- package/dist-server/service/twin-model/isa95-coverage.js +33 -9
- package/dist-server/service/twin-model/isa95-coverage.js.map +1 -1
- package/dist-server/service/twin-model/item-ref.js +2 -2
- package/dist-server/service/twin-model/item-ref.js.map +1 -1
- package/dist-server/service/twin-model/name-index.d.ts +1 -1
- package/dist-server/service/twin-model/name-index.js +2 -2
- package/dist-server/service/twin-model/name-index.js.map +1 -1
- package/dist-server/service/twin-model/project-structure.d.ts +1 -1
- package/dist-server/service/twin-model/project-structure.js +7 -6
- package/dist-server/service/twin-model/project-structure.js.map +1 -1
- package/dist-server/service/twin-model/twin-equipment.d.ts +2 -2
- package/dist-server/service/twin-model/twin-equipment.js +3 -3
- package/dist-server/service/twin-model/twin-equipment.js.map +1 -1
- package/dist-server/service/twin-model/twin-lineage-query.js +16 -5
- package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -1
- package/dist-server/service/twin-model/twin-location.d.ts +1 -1
- package/dist-server/service/twin-model/twin-location.js.map +1 -1
- package/dist-server/service/twin-model/twin-model-item-query.d.ts +3 -3
- package/dist-server/service/twin-model/twin-model-item-query.js +15 -15
- package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -1
- package/dist-server/service/twin-model/twin-model-mutation.d.ts +2 -2
- package/dist-server/service/twin-model/twin-model-mutation.js +4 -4
- package/dist-server/service/twin-model/twin-model-mutation.js.map +1 -1
- package/dist-server/service/twin-model/twin-model-query.js +51 -30
- package/dist-server/service/twin-model/twin-model-query.js.map +1 -1
- package/dist-server/service/twin-model/twin-model-tree-query.js +23 -1
- package/dist-server/service/twin-model/twin-model-tree-query.js.map +1 -1
- package/dist-server/service/twin-model/twin-operation.d.ts +1 -1
- package/dist-server/service/twin-model/twin-operation.js +1 -1
- package/dist-server/service/twin-model/twin-operation.js.map +1 -1
- package/dist-server/service/twin-space/move-space.js +2 -2
- package/dist-server/service/twin-space/move-space.js.map +1 -1
- package/dist-server/service/twin-space/representation-role.d.ts +2 -0
- package/dist-server/service/twin-space/representation-role.js +18 -0
- package/dist-server/service/twin-space/representation-role.js.map +1 -0
- package/dist-server/service/twin-space/space-integrity.d.ts +1 -1
- package/dist-server/service/twin-space/space-integrity.js.map +1 -1
- package/dist-server/service/twin-space/twin-space-representation.d.ts +2 -0
- package/dist-server/service/twin-space/twin-space-representation.js +16 -0
- package/dist-server/service/twin-space/twin-space-representation.js.map +1 -1
- package/dist-server/service/twin-space/twin-space-resolver.d.ts +2 -2
- package/dist-server/service/twin-space/twin-space-resolver.js +37 -8
- package/dist-server/service/twin-space/twin-space-resolver.js.map +1 -1
- package/dist-server/service/twin-structure/twin-structure.js +3 -3
- package/dist-server/service/twin-structure/twin-structure.js.map +1 -1
- package/dist-server/service/twin-subject/event-subjects.d.ts +43 -0
- package/dist-server/service/twin-subject/event-subjects.js +137 -0
- package/dist-server/service/twin-subject/event-subjects.js.map +1 -0
- package/dist-server/service/twin-subject/index.d.ts +9 -0
- package/dist-server/service/twin-subject/index.js +16 -0
- package/dist-server/service/twin-subject/index.js.map +1 -0
- package/dist-server/service/twin-subject/item-resolver.d.ts +10 -0
- package/dist-server/service/twin-subject/item-resolver.js +93 -0
- package/dist-server/service/twin-subject/item-resolver.js.map +1 -0
- package/dist-server/service/twin-subject/subject-query-shape.d.ts +22 -0
- package/dist-server/service/twin-subject/subject-query-shape.js +88 -0
- package/dist-server/service/twin-subject/subject-query-shape.js.map +1 -0
- package/dist-server/service/twin-subject/twin-subject-backfill.d.ts +23 -0
- package/dist-server/service/twin-subject/twin-subject-backfill.js +159 -0
- package/dist-server/service/twin-subject/twin-subject-backfill.js.map +1 -0
- package/dist-server/service/twin-subject/twin-subject-event.d.ts +14 -0
- package/dist-server/service/twin-subject/twin-subject-event.js +111 -0
- package/dist-server/service/twin-subject/twin-subject-event.js.map +1 -0
- package/dist-server/service/twin-subject/twin-subject-fast-path.d.ts +39 -0
- package/dist-server/service/twin-subject/twin-subject-fast-path.js +295 -0
- package/dist-server/service/twin-subject/twin-subject-fast-path.js.map +1 -0
- package/dist-server/service/twin-subject/twin-subject-query.d.ts +27 -0
- package/dist-server/service/twin-subject/twin-subject-query.js +232 -0
- package/dist-server/service/twin-subject/twin-subject-query.js.map +1 -0
- package/dist-server/service/twin-subject/twin-subject-writer.d.ts +27 -0
- package/dist-server/service/twin-subject/twin-subject-writer.js +166 -0
- package/dist-server/service/twin-subject/twin-subject-writer.js.map +1 -0
- package/dist-server/service/twin-target/twin-target-resolver.js +1 -1
- package/dist-server/service/twin-target/twin-target-resolver.js.map +1 -1
- package/dist-shared/entity-delta.d.ts +1 -1
- package/dist-shared/entity-delta.js +4 -4
- package/dist-shared/entity-delta.js.map +1 -1
- package/dist-shared/touched-items.js +2 -2
- package/dist-shared/touched-items.js.map +1 -1
- package/dist-shared/twin-level.js +1 -1
- package/dist-shared/twin-level.js.map +1 -1
- package/package.json +7 -6
- package/server/engine/attention-digest.ts +9 -9
- package/server/engine/canonical-ingest.ts +202 -87
- package/server/engine/command-routing.ts +1 -1
- package/server/engine/declared-stimulus.ts +1 -1
- package/server/engine/energy-topology.ts +2 -2
- package/server/engine/ingest-dedupe.ts +149 -0
- package/server/engine/ingest-health.ts +171 -22
- package/server/engine/integration-probes.ts +7 -7
- package/server/engine/kpi-baseline.ts +4 -4
- package/server/engine/kpi-fold.ts +592 -41
- package/server/engine/kpi-query.ts +307 -48
- package/server/engine/live-feed-registry.ts +1 -1
- package/server/engine/load-meter.ts +1 -1
- package/server/engine/local-declarations.ts +15 -14
- package/server/engine/log.ts +1 -1
- package/server/engine/measured-estimator.ts +1 -1
- package/server/engine/measured-yield.ts +1 -1
- package/server/engine/model-basis.ts +2 -2
- package/server/engine/model-gap.ts +3 -3
- package/server/engine/model-vocabulary.ts +2 -2
- package/server/engine/oee-accumulator.ts +8 -6
- package/server/engine/property-effects.ts +2 -2
- package/server/engine/read-time.ts +77 -0
- package/server/engine/restart-policy.ts +1 -1
- package/server/engine/runtime-key.ts +1 -1
- package/server/engine/spec-coverage.ts +3 -3
- package/server/engine/stage-path.ts +5 -5
- package/server/engine/state-axes.ts +2 -2
- package/server/engine/travel-estimator.ts +2 -2
- package/server/engine/twin-engine.ts +906 -205
- package/server/engine/warm-start.ts +6 -6
- package/server/index.ts +1 -1
- package/server/migrations/1786000000000-RenameTwinInstanceBoardToModel.ts +1 -1
- package/server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.ts +3 -3
- package/server/migrations/1786200000000-CarryLiveFeedCursor.ts +2 -2
- package/server/migrations/1786300000000-IndexEventTypeByTime.ts +48 -0
- package/server/migrations/1786400000000-IndexEventCreatedAt.ts +38 -0
- package/server/migrations/1786500000000-CreateTwinSubjectEvents.ts +93 -0
- package/server/migrations/index.ts +7 -1
- package/server/routes.ts +73 -5
- package/server/service/index.ts +7 -1
- package/server/service/reference/control-routing.ts +1 -1
- package/server/service/reference/discovery-result.ts +3 -3
- package/server/service/reference/hook-contract.ts +103 -0
- package/server/service/reference/ingest-space.ts +4 -4
- package/server/service/reference/knob-defaults.ts +4 -4
- package/server/service/reference/reference-adapter.ts +145 -10
- package/server/service/reference/reference-hook.ts +145 -0
- package/server/service/reference/reference-live.ts +87 -15
- package/server/service/reference/reference-master.ts +51 -20
- package/server/service/reference/reference-resolver.ts +11 -11
- package/server/service/reference/template-registry.ts +1 -1
- package/server/service/reference/twin-reference.ts +3 -3
- package/server/service/twin-attention/twin-attention-query.ts +1 -1
- package/server/service/twin-audit/command-audit.ts +1 -1
- package/server/service/twin-audit/twin-audit-event.ts +2 -2
- package/server/service/twin-audit/twin-audit-query.ts +1 -1
- package/server/service/twin-backfill/backfill-period-facts.ts +168 -0
- package/server/service/twin-backfill/backfill-shape.ts +86 -0
- package/server/service/twin-backfill/index.ts +3 -0
- package/server/service/twin-backfill/twin-backfill-resolver.ts +35 -0
- package/server/service/twin-control/twin-control-mutation.ts +2 -2
- package/server/service/twin-event/twin-event-keys.ts +2 -2
- package/server/service/twin-event/twin-event-type.ts +18 -1
- package/server/service/twin-event/twin-event.ts +32 -6
- package/server/service/twin-forecast/forecast-metrics.ts +1 -1
- package/server/service/twin-forecast/gap-analytics.ts +1 -1
- package/server/service/twin-forecast/twin-forecast-query.ts +30 -11
- package/server/service/twin-ingest-window/twin-ingest-window-query.ts +1 -1
- package/server/service/twin-ingest-window/twin-ingest-window-writer.ts +1 -1
- package/server/service/twin-instance/twin-instance.ts +4 -4
- package/server/service/twin-journal/event-type-split-shape.ts +112 -0
- package/server/service/twin-journal/event-type-split.ts +144 -0
- package/server/service/twin-journal/journal-order.ts +20 -0
- package/server/service/twin-journal/twin-journal-query.ts +224 -20
- package/server/service/twin-lifecycle/domain-catalog.ts +2 -1
- package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +58 -4
- package/server/service/twin-metrics/twin-metrics-query.ts +14 -1
- package/server/service/twin-model/axis-journal-evidence.ts +1 -1
- package/server/service/twin-model/epcis-coverage.ts +34 -3
- package/server/service/twin-model/iec61850-coverage.ts +1 -1
- package/server/service/twin-model/isa95-coverage.ts +43 -16
- package/server/service/twin-model/item-ref.ts +2 -2
- package/server/service/twin-model/name-index.ts +2 -2
- package/server/service/twin-model/project-structure.ts +7 -6
- package/server/service/twin-model/twin-equipment.ts +16 -4
- package/server/service/twin-model/twin-lineage-query.ts +16 -5
- package/server/service/twin-model/twin-location.ts +1 -1
- package/server/service/twin-model/twin-model-item-query.ts +15 -15
- package/server/service/twin-model/twin-model-mutation.ts +4 -4
- package/server/service/twin-model/twin-model-query.ts +49 -28
- package/server/service/twin-model/twin-model-tree-query.ts +26 -2
- package/server/service/twin-model/twin-operation.ts +2 -2
- package/server/service/twin-space/move-space.ts +3 -3
- package/server/service/twin-space/representation-role.ts +15 -0
- package/server/service/twin-space/space-integrity.ts +1 -1
- package/server/service/twin-space/twin-space-representation.ts +37 -0
- package/server/service/twin-space/twin-space-resolver.ts +40 -8
- package/server/service/twin-structure/twin-structure.ts +3 -3
- package/server/service/twin-subject/event-subjects.ts +155 -0
- package/server/service/twin-subject/index.ts +10 -0
- package/server/service/twin-subject/item-resolver.ts +92 -0
- package/server/service/twin-subject/subject-query-shape.ts +84 -0
- package/server/service/twin-subject/twin-subject-backfill.ts +196 -0
- package/server/service/twin-subject/twin-subject-event.ts +101 -0
- package/server/service/twin-subject/twin-subject-fast-path.ts +346 -0
- package/server/service/twin-subject/twin-subject-query.ts +210 -0
- package/server/service/twin-subject/twin-subject-writer.ts +189 -0
- package/server/service/twin-target/twin-target-resolver.ts +1 -1
- package/shared/entity-delta.ts +4 -4
- package/shared/touched-items.ts +2 -2
- package/shared/twin-level.ts +1 -1
- package/test/adopt-structure-live.test.ts +3 -3
- package/test/aggregate-not-findone.test.ts +112 -0
- package/test/attention-digest.test.ts +6 -6
- package/test/axis-read.test.ts +6 -5
- package/test/backfill-shape.test.ts +89 -0
- package/test/boot-resume.test.ts +20 -7
- package/test/broadcast-cost-baseline.test.ts +2 -1
- package/test/broadcast-period.test.ts +2 -2
- package/test/broadcast-queue-overflow.test.ts +244 -0
- package/test/canonical-ingest-vocabularies.test.ts +259 -8
- package/test/canonical-quantity-door.test.ts +2 -2
- package/test/capability-mapping.test.ts +1 -1
- package/test/checkpoint-refuses-empty.test.ts +2 -2
- package/test/connector-capability-declaration.test.ts +88 -0
- package/test/contract-layer-guard.test.ts +48 -0
- package/test/control-capability.test.ts +1 -1
- package/test/cursor-stall-not-read-failure.test.ts +2 -2
- package/test/declaration-reaches-model.test.ts +7 -5
- package/test/declared-stimulus.test.ts +4 -4
- package/test/discovery-result.test.ts +2 -2
- package/test/entity-delta.test.ts +4 -4
- package/test/equipment-identity-reaches.test.ts +57 -0
- package/test/event-subjects.test.ts +147 -0
- package/test/event-time-column.test.ts +3 -3
- package/test/event-type-split.test.ts +155 -0
- package/test/fact-scope-wiring.test.ts +96 -0
- package/test/fold-gap-speaks.test.ts +53 -0
- package/test/forecast-metrics.test.ts +1 -1
- package/test/forecast-tuning.test.ts +1 -1
- package/test/generated-not-counted.test.ts +99 -0
- package/test/ingest-bench.test.ts +2 -1
- package/test/ingest-health-engine.test.ts +2 -2
- package/test/ingest-health-wiring.test.ts +7 -5
- package/test/ingest-health.test.ts +6 -6
- package/test/ingest-history.test.ts +2 -2
- package/test/ingest-idempotent.test.ts +132 -0
- package/test/ingest-space.test.ts +3 -3
- package/test/ingest-wiring-guard.test.ts +197 -0
- package/test/integration-probes.test.ts +3 -3
- package/test/integration-runner.test.ts +1 -1
- package/test/item-ref.test.ts +1 -1
- package/test/journal-order-axis.test.ts +80 -0
- package/test/journal-read-discipline.test.ts +16 -6
- package/test/journal-retention.test.ts +171 -29
- package/test/journal-sort-axis.test.ts +1 -1
- package/test/journal-write-door.test.ts +3 -3
- package/test/journal-write-trend.test.ts +1 -1
- package/test/kernel-kind-guard.test.ts +3 -3
- package/test/knob-defaults.test.ts +1 -1
- package/test/kpi-baseline-db.test.ts +2 -2
- package/test/kpi-fold.test.ts +266 -14
- package/test/kpi-query-bench.test.ts +3 -3
- package/test/live-cursor-wiring.test.ts +6 -6
- package/test/live-kernel-facts.test.ts +11 -11
- package/test/live-mirror-parity.test.ts +9 -6
- package/test/load-meter.test.ts +1 -1
- package/test/local-declarations.test.ts +13 -12
- package/test/log-stamp.test.ts +1 -1
- package/test/master-to-twin.test.ts +2 -2
- package/test/measured-yield.test.ts +2 -2
- package/test/merge-streams.test.ts +70 -0
- package/test/mirror-resumes-from-checkpoint.test.ts +13 -13
- package/test/model-basis.test.ts +2 -2
- package/test/model-gap.test.ts +4 -4
- package/test/model-vocabulary.test.ts +2 -2
- package/test/move-space.test.ts +1 -1
- package/test/oee-accumulator.test.ts +20 -5
- package/test/operational-vocabulary.test.ts +2 -2
- package/test/project-structure-db.test.ts +2 -2
- package/test/projection-reaches-screen.test.ts +7 -2
- package/test/projection-reads-declared.test.ts +109 -0
- package/test/property-effects.test.ts +10 -6
- package/test/read-failure-ledger.test.ts +106 -0
- package/test/read-failure-visible.test.ts +7 -7
- package/test/read-time.test.ts +118 -0
- package/test/reference-grounding.test.ts +1 -1
- package/test/reference-hook.test.ts +86 -0
- package/test/registry-key-guard.test.ts +1 -1
- package/test/rename-twin.test.ts +81 -0
- package/test/restart-policy.test.ts +3 -3
- package/test/resync-origin-site.test.ts +3 -3
- package/test/revision-axis.test.ts +14 -8
- package/test/runtime-key.test.ts +3 -3
- package/test/scale-twin-bench.test.ts +2 -1
- package/test/snapshot-freshness.test.ts +4 -4
- package/test/source-outcome-audit.test.ts +1 -1
- package/test/space-integrity.test.ts +1 -1
- package/test/space-representation.test.ts +66 -0
- package/test/space-scope-includes-stopped.test.ts +55 -0
- package/test/spec-coverage.test.ts +1 -1
- package/test/standard-coverage.test.ts +19 -21
- package/test/status-tally.test.ts +1 -1
- package/test/structure-revision-db.test.ts +7 -7
- package/test/subject-fast-path.test.ts +148 -0
- package/test/subject-read-model.test.ts +175 -0
- package/test/touched-items.test.ts +2 -2
- package/test/twin-audit.test.ts +1 -1
- package/test/twin-event-keys.test.ts +1 -1
- package/test/twin-model-item-db.test.ts +5 -5
- package/test/twin-model-tree-db.test.ts +35 -2
- package/test/twin-origin-resync.test.ts +1 -1
- package/test/vocabulary-guard.test.ts +1 -1
- package/test/warm-start-seam.test.ts +1 -1
- package/test/warm-start.test.ts +6 -6
- package/test/withheld-door.test.ts +2 -2
- package/test/yield-loop.test.ts +4 -4
- package/tsconfig.shared.tsbuildinfo +1 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* ── 왜 떼어냈나 ─────────────────────────────────────────────────────────────
|
|
6
6
|
* 이 판정이 틀리면 증상이 정반대 두 방향으로 나온다: 주입해야 할 때 안 주입하면 **있는 재고가 0 으로**
|
|
7
7
|
* 보이고(2026-07-31 hatiolab-wms: 저널에 입고 540·출고 94 인데 재고 화면이 비어 있었다), 심지
|
|
8
|
-
* 말아야 할 벤치에 주입하면 **용량 측정이 오염된다.** 둘 다
|
|
8
|
+
* 말아야 할 벤치에 주입하면 **용량 측정이 오염된다.** 둘 다 알리지 않고 틀리는 종류라 규칙을 고정한다.
|
|
9
9
|
*
|
|
10
10
|
* ── 오더를 버리던 전제가 낡았다 (2026-08-05) ────────────────────────────────
|
|
11
11
|
* 예전 주석은 "스냅샷의 오더에는 progress 밖에 없어서 역산하면 없는 숫자를 지어내는 셈이므로
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* `StateProjector` 다. 사람·자산·작업도 같은 스냅샷에 있고 커널이 심을 수 있는데 넘기지 않고 있었다.
|
|
18
18
|
* 그래서 씨앗을 **관측 스냅샷 전체**로 넓힌다.
|
|
19
19
|
*
|
|
20
|
-
* 다만 넓히면서
|
|
20
|
+
* 다만 넓히면서 **들어온 것이 없는 누락을 만들지 않는다.** 원값이 없는 오더는 커널이 걸러 내므로(지어내지
|
|
21
21
|
* 않는다), 그 수를 여기서 세어 호출부가 말할 수 있게 한다. 세지 않으면 "주문이 없다" 와 "주문을
|
|
22
22
|
* 못 심었다" 가 화면에서 같아진다.
|
|
23
23
|
*/
|
|
@@ -95,11 +95,11 @@ const looksLikeState = (x: any): boolean =>
|
|
|
95
95
|
!!x && typeof x === 'object' && ['locations', 'items', 'equipment', 'orders', 'tasks', 'persons', 'assets', 'acked'].some(k => k in x)
|
|
96
96
|
|
|
97
97
|
/**
|
|
98
|
-
* 봉투를 벗긴다 — **체크포인트가 겹포장돼 복원이
|
|
98
|
+
* 봉투를 벗긴다 — **체크포인트가 겹포장돼 복원이 알리지 않고 죽던 자리.**
|
|
99
99
|
*
|
|
100
100
|
* `TwinEngine.snapshot()` 은 라이브에서는 상태를, 시뮬에서는 `runtime.resync()` 봉투(`{revision,state}`)를
|
|
101
101
|
* 돌려준다. 그 봉투를 다시 `{revision, state}` 로 감싸 캐시에 넣었으므로, 꺼낸 `state` 는 축이 하나도
|
|
102
|
-
* 없는 껍데기였다 → `planWarmStart` 가 "심을 것이 없다" 로 판정하고
|
|
102
|
+
* 없는 껍데기였다 → `planWarmStart` 가 "심을 것이 없다" 로 판정하고 **알리지 않고** 넘어갔다. 저널에 수천 건이
|
|
103
103
|
* 있고 캐시도 최신인데 트윈이 빈 채로 떴다(오류도 로그도 없다).
|
|
104
104
|
*
|
|
105
105
|
* 반환 모양을 통일하는 것이 뿌리 수정이지만, 그 값은 State 구독 페이로드로도 나가므로 클라이언트 계약을
|
|
@@ -128,7 +128,7 @@ export function planWarmStart(state: ObservedState | null | undefined, purpose:
|
|
|
128
128
|
/* 벤치 판정이 먼저다 — 상태가 있든 없든 벤치에는 주입하지 않는다는 사실이 바뀌지 않는다. */
|
|
129
129
|
if (purpose === 'bench') return { action: 'skip', reason: 'bench' }
|
|
130
130
|
if (!state) return { action: 'skip', reason: 'no-state' }
|
|
131
|
-
/* 봉투가 들어오면 스스로 벗는다 — 호출부가 한 곳이라도 잊으면 그 경로가
|
|
131
|
+
/* 봉투가 들어오면 스스로 벗는다 — 호출부가 한 곳이라도 잊으면 그 경로가 알리지 않고 죽는다(실제로 그랬다). */
|
|
132
132
|
state = unwrapState(state)
|
|
133
133
|
|
|
134
134
|
const locations = arr(state.locations)
|
|
@@ -191,7 +191,7 @@ export function planWarmStart(state: ObservedState | null | undefined, purpose:
|
|
|
191
191
|
* 곧 정정하므로, 심으면 **떠난 물건이 되살아나는** 위험만 남는다(그래서 `startLive` 는 오랫동안 아무것도
|
|
192
192
|
* 심지 않았다).
|
|
193
193
|
*
|
|
194
|
-
* 그런데 원천이 **애초에 다시 말해 주지 않는 축**이 있다. 우리가
|
|
194
|
+
* 그런데 원천이 **애초에 다시 말해 주지 않는 축**이 있다. 우리가 계산해 만든 것들이다:
|
|
195
195
|
* · 확인(ack)·조건이 처음 성립한 시각 — 사람이 한 일과 우리가 기억한 시각
|
|
196
196
|
* · 에너지의 열린 구간 누적·적산 기준점·관측 이후 최대 — SCADA 는 「이번 15분에 지금까지 얼마」를 모른다
|
|
197
197
|
*
|
package/server/index.ts
CHANGED
|
@@ -32,7 +32,7 @@ process.on('bootstrap-module-start' as any, async ({ app, config, client }: any)
|
|
|
32
32
|
* 실측: 개발 저널이 시간당 581,794행 · 0.85GB(하루 20GB) 로 자랐고 지우는 것이 없었다. 그런데
|
|
33
33
|
* 「며칠까지 남기나」는 **배포마다 갈리는 사실**이다 — 개발은 짧아도 되고 실증은 길어야 한다.
|
|
34
34
|
*
|
|
35
|
-
* 선언이 없으면 **아무것도 지우지 않는다.**
|
|
35
|
+
* 선언이 없으면 **아무것도 지우지 않는다.** 알리지 않고 지우는 편이 알리지 않고 쌓는 것보다 나쁘다.
|
|
36
36
|
* 값은 스냅샷 TTL(7일)보다 짧을 수 없다 — 엔진이 그것을 올려 주고 그 사실을 말한다.
|
|
37
37
|
*/
|
|
38
38
|
/*
|
|
@@ -17,7 +17,7 @@ import { MigrationInterface, QueryRunner } from 'typeorm'
|
|
|
17
17
|
*
|
|
18
18
|
* ── 멱등 ──────────────────────────────────────────────────────────────────────
|
|
19
19
|
* 이미 `model` 이 있으면(엔티티 선언 + synchronize 가 먼저 지나간 경우) 아무것도 하지 않는다.
|
|
20
|
-
* 그 경우 옛 컬럼이 남아 있을 수 있는데 **지우지 않는다** — 값이 든 컬럼을 마이그레이션이
|
|
20
|
+
* 그 경우 옛 컬럼이 남아 있을 수 있는데 **지우지 않는다** — 값이 든 컬럼을 마이그레이션이 알리지 않고
|
|
21
21
|
* 버리면 안 된다. 남은 컬럼의 처분은 사람이 확인한 뒤의 일이다.
|
|
22
22
|
*/
|
|
23
23
|
const TABLE_NAME = 'twin_instances'
|
|
@@ -5,17 +5,17 @@ import { MigrationInterface, QueryRunner, TableColumn, TableIndex } from 'typeor
|
|
|
5
5
|
*
|
|
6
6
|
* ① `twin_events.action` — EPCIS 행위(ADD/OBSERVE/DELETE) 승격.
|
|
7
7
|
* 되풀어 읽는 스냅샷은 원리적으로 `DELETE` 를 못 낸다. 그래서 「이 트윈이 반출을 한 번이라도
|
|
8
|
-
*
|
|
8
|
+
* 계산했는가」가 진짜 delta 피드의 증거인데, 값이 `payload`(simple-json = TEXT) 안에 있는 동안에는
|
|
9
9
|
* 5개 드라이버 어디에서도 그 질문을 할 수 없었다(DB별 JSON 연산자 금지).
|
|
10
10
|
*
|
|
11
11
|
* ② `twin_references.last_warning_count` — 마지막 동기에서 못 옮긴 값의 수.
|
|
12
|
-
* 세지 않은 구조 인제스트는 도크를 랙으로 바꿔 놓고도
|
|
12
|
+
* 세지 않은 구조 인제스트는 도크를 랙으로 바꿔 놓고도 성공이라 답하면서 아무 일도 하지 않는다.
|
|
13
13
|
*
|
|
14
14
|
* **둘 다 nullable, 기본값 없음.** 기본값 0 을 깔면 「없었다」와 「세지 않았다」가 같아지고, 그
|
|
15
15
|
* 구분이 단계 판정의 전부다(옛 행은 모르는 것이 맞다 — 채워 넣지 않는다).
|
|
16
16
|
*
|
|
17
17
|
* 인덱스 `ix_twin_event_6` 는 저널이 큰 환경에서 생성에 시간이 걸린다. 그래도 만드는 이유:
|
|
18
|
-
* 없으면 「반출을
|
|
18
|
+
* 없으면 「반출을 계산했나」가 그 인스턴스 저널 전량 주사가 되고, 반출이 **없는** 경우(초기 상태이자
|
|
19
19
|
* 가장 흔한 경우)가 항상 최악이다.
|
|
20
20
|
*
|
|
21
21
|
* 멱등: 이미 있으면 아무것도 하지 않는다. TypeORM API 만 쓴다(raw SQL 금지 — 드라이버 5종).
|
|
@@ -3,7 +3,7 @@ import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm'
|
|
|
3
3
|
/**
|
|
4
4
|
* `twin_references.live_cursor` — **어디까지 읽었나**를 재기동 너머로 잇는다.
|
|
5
5
|
*
|
|
6
|
-
* ── 무엇이
|
|
6
|
+
* ── 무엇이 오류 없이 사라지고 있었나 (2026-08-23 실측) ─────────────────────────
|
|
7
7
|
* 라이브 어댑터가 붙을 때마다 `Date.now() − 되돌아볼 날수` 로 창을 새로 만들었다. 그래서 **재기동마다
|
|
8
8
|
* 미러의 과거가 잘렸다** — 작업 2,855 → 2,820(그 사이 8시간이 흐른 만큼). 미러가 아는 것이 「원본의
|
|
9
9
|
* 사실」이 아니라 「창의 함수」였고, 화면은 그 수를 보이면서 그 사실을 말하지 않았다.
|
|
@@ -15,7 +15,7 @@ import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm'
|
|
|
15
15
|
* 재기동 연속성으로 이어받는다. 「우리가 어디까지 읽었나」가 정확히 그 성질이다.
|
|
16
16
|
*
|
|
17
17
|
* 그리고 스냅샷 체크포인트와 성질이 다르다: 그쪽은 잃어도 원천이 정정해 준다(그래서 TTL 캐시로 족하다).
|
|
18
|
-
* **이 값은 잃으면 원천에 묻지 않게 된다** — 창이 다시 미끄러지고, 그 손실은 오류 없이
|
|
18
|
+
* **이 값은 잃으면 원천에 묻지 않게 된다** — 창이 다시 미끄러지고, 그 손실은 오류 없이 들어온 것이 없다.
|
|
19
19
|
* 만료되는 자리에 둘 수 없다.
|
|
20
20
|
*
|
|
21
21
|
* ── 모양 ────────────────────────────────────────────────────────────────────
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { MigrationInterface, QueryRunner, TableIndex } from 'typeorm'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 「이 종류의 사실을 시각순으로」 — 그 축에 인덱스가 없었다.
|
|
5
|
+
*
|
|
6
|
+
* ── 무엇이 났나 (2026-08-25 실측 · 승화푸드) ─────────────────────────────────
|
|
7
|
+
* 지난 기록 조회는 필터가 없을 때 `revision DESC`(적힌 순서)로 잘랐다. 연결된 시스템을 되읽는
|
|
8
|
+
* 트윈에서는 그 순서가 일어난 순서와 달라서, 공정 타임라인이 4월 사실을 최근 것으로 그렸다. 그래서
|
|
9
|
+
* 기본을 시각순으로 고쳤는데(`journal-order.ts`), 그때 이 인덱스가 없는 것이 드러났다.
|
|
10
|
+
*
|
|
11
|
+
* ix_2 (domain, instance, eventType, revision) 종류는 걸러지고 정렬이 적힌 순서다
|
|
12
|
+
* ix_1 (domain, instance, eventTime) 정렬은 맞고 종류를 못 거른다
|
|
13
|
+
*
|
|
14
|
+
* 종류로 거르고 시각으로 정렬하면 `ix_1` 로 걷다가 행마다 테이블을 열어 종류를 확인한다. 찾는 종류가
|
|
15
|
+
* 며칠 전에 멈춘 트윈에서는 그 사이 행을 전부 읽고 버린다. 실측: 200건에 1.7초(같은 조회가 옛 정렬로는
|
|
16
|
+
* 0.001초). 승화푸드에서 작업 사실은 8월 22일에 멈춰 있고 그 뒤로 리비전이 110만 늘었다.
|
|
17
|
+
*
|
|
18
|
+
* ── 그 1.7초가 서버 전체를 막았다 ───────────────────────────────────────────
|
|
19
|
+
* 공정 타임라인은 5초마다 세 종류를 부른다. sqlite 드라이버는 연결이 하나라서, 5초 주기에 5초가 넘는
|
|
20
|
+
* 일감이 들어오면 나머지 질의가 전부 뒤에 선다. 39분 동안 느린 질의 3,152건이 쌓였고, 한 줄짜리
|
|
21
|
+
* `domains` 조회가 중간 7.4초로 찍혔다 — 그 질의의 문제가 아니라 줄을 선 값이다.
|
|
22
|
+
*
|
|
23
|
+
* ── 왜 이제 얹나 ────────────────────────────────────────────────────────────
|
|
24
|
+
* 엔티티 주석이 「저널 하나가 아주 커져서 이 축들의 조회가 느려지면 그때 측정을 근거로 인덱스를 추가할
|
|
25
|
+
* 일이지, 지레 얹어 쓰기를 무겁게 할 일은 아니다」라고 적어 두었다. 그 측정이 나왔다.
|
|
26
|
+
*
|
|
27
|
+
* 큰 기록에서는 생성에 시간이 걸린다(47GB 개발 파일에서 수분). 멱등이므로 다시 돌려도 안전하고,
|
|
28
|
+
* TypeORM API 만 쓴다(드라이버 다섯을 다 지나가야 하므로 raw SQL 금지).
|
|
29
|
+
*/
|
|
30
|
+
const EVENTS = 'twin_events'
|
|
31
|
+
const IX = 'ix_twin_event_7'
|
|
32
|
+
const COLUMNS = ['domain_id', 'instance_id', 'event_type', 'event_time']
|
|
33
|
+
|
|
34
|
+
export class IndexEventTypeByTime1786300000000 implements MigrationInterface {
|
|
35
|
+
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
36
|
+
const events = await queryRunner.getTable(EVENTS)
|
|
37
|
+
if (!events) return
|
|
38
|
+
if (events.indices.some(i => i.name === IX)) return
|
|
39
|
+
await queryRunner.createIndex(events, new TableIndex({ name: IX, columnNames: COLUMNS }))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
43
|
+
const events = await queryRunner.getTable(EVENTS)
|
|
44
|
+
if (!events) return
|
|
45
|
+
if (!events.indices.some(i => i.name === IX)) return
|
|
46
|
+
await queryRunner.dropIndex(events, IX)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { MigrationInterface, QueryRunner, TableIndex } from 'typeorm'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 「이 트윈의 오래된 기록」 — 보관 기간 정리가 쓰는 축에 인덱스가 없었다.
|
|
5
|
+
*
|
|
6
|
+
* ── 무엇이 났나 (2026-08-27 실측 · 개발 파일 51GB · 3,128만 행) ───────────────
|
|
7
|
+
* 정리는 `created_at` 으로 자른다(보관은 저장 나이의 문제이고 저장 나이는 실제 시계다). 그런데 그 축에
|
|
8
|
+
* 인덱스가 없어서, 매 배치마다 그 트윈의 저널을 전량 주사했다.
|
|
9
|
+
*
|
|
10
|
+
* hatio-mx2 의 7일 밖 행을 세기 208초 ← 1,029만 행을 전부 읽는다(7일 밖은 2,446건뿐이다)
|
|
11
|
+
* revision 으로 한 행 짚기 0.004초
|
|
12
|
+
*
|
|
13
|
+
* 정리는 한 창에 2초만 쓰도록 예산이 걸려 있다. 그 2초를 주사에 다 쓰고 44건만 지우고 멈췄다. 지금은
|
|
14
|
+
* 지울 것이 적어서 크게 드러나지 않지만, 보관 기간을 줄이거나 쓰는 양이 늘면 따라가지 못한다.
|
|
15
|
+
*
|
|
16
|
+
* ── 큰 기록에서는 만드는 데 시간이 걸린다 ───────────────────────────────────
|
|
17
|
+
* 3,128만 행에 인덱스를 만드는 동안 쓰기가 막힌다(sqlite 는 쓰기가 하나다). 서버를 내린 뒤 돌린다.
|
|
18
|
+
* 멱등이므로 다시 돌려도 안전하고, TypeORM API 만 쓴다(드라이버 다섯을 다 지나가야 하므로 raw SQL 금지).
|
|
19
|
+
*/
|
|
20
|
+
const EVENTS = 'twin_events'
|
|
21
|
+
const IX = 'ix_twin_event_8'
|
|
22
|
+
const COLUMNS = ['domain_id', 'instance_id', 'created_at']
|
|
23
|
+
|
|
24
|
+
export class IndexEventCreatedAt1786400000000 implements MigrationInterface {
|
|
25
|
+
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
26
|
+
const events = await queryRunner.getTable(EVENTS)
|
|
27
|
+
if (!events) return
|
|
28
|
+
if (events.indices.some(i => i.name === IX)) return
|
|
29
|
+
await queryRunner.createIndex(events, new TableIndex({ name: IX, columnNames: COLUMNS }))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
33
|
+
const events = await queryRunner.getTable(EVENTS)
|
|
34
|
+
if (!events) return
|
|
35
|
+
if (!events.indices.some(i => i.name === IX)) return
|
|
36
|
+
await queryRunner.dropIndex(events, IX)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { MigrationInterface, QueryRunner, Table, TableIndex } from 'typeorm'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* **대상별 사건 목록** — 조회용 파생 표를 만든다.
|
|
5
|
+
*
|
|
6
|
+
* ── 왜 (2026-08-27 실측) ────────────────────────────────────────────────────
|
|
7
|
+
* 360 화면이 「이 대상의 이력」을 묻고 그 조회가 64초다. 「무엇으로 걸러 시각순으로 정렬해 앞에서
|
|
8
|
+
* 30개」라는 모양에 맞는 목차가 저널에 없고, 얹어도 풀리지 않는다.
|
|
9
|
+
*
|
|
10
|
+
* 여러 값으로 거르면(IN) 정렬 때문에 목차를 못 쓴다
|
|
11
|
+
* 축이 늘 때마다 저널의 목차가 늘어난다 — 360 의 축 넷이면 13개가 된다
|
|
12
|
+
* 품목과 로트가 한 문자열에 붙어 있어 품목만으로는 찾을 수 없다
|
|
13
|
+
*
|
|
14
|
+
* ── 비어서 시작한다 ────────────────────────────────────────────────────────
|
|
15
|
+
* 이 표는 **앞으로만 채운다.** 기존 3,128만 건을 소급해서 채우지 않는다 — 그래야 새 조회 축이 필요할
|
|
16
|
+
* 때마다 전체를 다시 만드는 구조가 되지 않는다. 담지 않은 구간은 조회가 `coveredFrom` 으로 말한다.
|
|
17
|
+
*
|
|
18
|
+
* 그래서 이 마이그레이션은 큰 기록에서도 값싸다(표와 목차를 만들 뿐, 채우지 않는다).
|
|
19
|
+
*
|
|
20
|
+
* 멱등이고 TypeORM API 만 쓴다(드라이버 다섯을 다 지나가야 하므로 raw SQL 금지). 컬럼 타입 이름을
|
|
21
|
+
* 적지 않는 규율은 엔티티와 같다 — `datetime` 은 Postgres 가 거절한다.
|
|
22
|
+
*
|
|
23
|
+
* 설계: `operato-twin/design/plans/read-models.md`
|
|
24
|
+
*/
|
|
25
|
+
const TABLE = 'twin_subject_events'
|
|
26
|
+
|
|
27
|
+
export class CreateTwinSubjectEvents1786500000000 implements MigrationInterface {
|
|
28
|
+
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
29
|
+
/*
|
|
30
|
+
* 표마다 따로 본다 — 하나가 이미 있다고 여기서 돌아가면 나머지가 만들어지지 않는다. 실제로 그렇게
|
|
31
|
+
* 적어 두었다가, 사건 목록만 있고 진행 지점 표가 없는 상태를 만들었다.
|
|
32
|
+
*/
|
|
33
|
+
if (!(await queryRunner.hasTable(TABLE))) {
|
|
34
|
+
await this.createSubjectEvents(queryRunner)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private async createSubjectEvents(queryRunner: QueryRunner): Promise<void> {
|
|
39
|
+
await queryRunner.createTable(
|
|
40
|
+
new Table({
|
|
41
|
+
name: TABLE,
|
|
42
|
+
columns: [
|
|
43
|
+
{ name: 'id', type: 'varchar', length: '36', isPrimary: true },
|
|
44
|
+
{ name: 'domain_id', type: 'varchar', length: '36', isNullable: true },
|
|
45
|
+
{ name: 'instance_id', type: 'varchar', isNullable: false },
|
|
46
|
+
{ name: 'subject_kind', type: 'varchar', isNullable: false },
|
|
47
|
+
{ name: 'subject_id', type: 'varchar', isNullable: false },
|
|
48
|
+
/* 시각 컬럼의 타입 이름은 드라이버가 정한다 — `timestamp` 는 sqlite 가 모르고 `datetime` 은 Postgres 가 거절한다. */
|
|
49
|
+
{ name: 'event_time', type: queryRunner.connection.driver.mappedDataTypes.createDate as string, isNullable: false },
|
|
50
|
+
{ name: 'revision', type: 'int', isNullable: false },
|
|
51
|
+
{ name: 'event_type', type: 'varchar', isNullable: false },
|
|
52
|
+
{ name: 'event_id', type: 'varchar', length: '36', isNullable: true },
|
|
53
|
+
{
|
|
54
|
+
name: 'created_at',
|
|
55
|
+
type: queryRunner.connection.driver.mappedDataTypes.createDate as string,
|
|
56
|
+
isNullable: true
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
}),
|
|
60
|
+
true
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
const table = await queryRunner.getTable(TABLE)
|
|
64
|
+
if (!table) return
|
|
65
|
+
|
|
66
|
+
/* 이 표의 존재 이유 — 「이 대상의 것을 최신순으로」가 목차를 위에서 읽고 끝난다(정렬 없음). */
|
|
67
|
+
if (!table.indices.some(i => i.name === 'ix_twin_subject_event_0')) {
|
|
68
|
+
await queryRunner.createIndex(
|
|
69
|
+
table,
|
|
70
|
+
new TableIndex({
|
|
71
|
+
name: 'ix_twin_subject_event_0',
|
|
72
|
+
columnNames: ['domain_id', 'instance_id', 'subject_kind', 'subject_id', 'event_time']
|
|
73
|
+
})
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
/* 보관 상한을 지우는 축 — 「이 트윈에서 오래된 줄」. */
|
|
77
|
+
if (!table.indices.some(i => i.name === 'ix_twin_subject_event_1')) {
|
|
78
|
+
await queryRunner.createIndex(
|
|
79
|
+
table,
|
|
80
|
+
new TableIndex({
|
|
81
|
+
name: 'ix_twin_subject_event_1',
|
|
82
|
+
columnNames: ['domain_id', 'instance_id', 'event_time']
|
|
83
|
+
})
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
90
|
+
if (!(await queryRunner.hasTable(TABLE))) return
|
|
91
|
+
await queryRunner.dropTable(TABLE)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -5,9 +5,15 @@
|
|
|
5
5
|
import { RenameTwinInstanceBoardToModel1786000000000 } from './1786000000000-RenameTwinInstanceBoardToModel.js'
|
|
6
6
|
import { PromoteEventActionAndSyncWarnings1786100000000 } from './1786100000000-PromoteEventActionAndSyncWarnings.js'
|
|
7
7
|
import { CarryLiveFeedCursor1786200000000 } from './1786200000000-CarryLiveFeedCursor.js'
|
|
8
|
+
import { IndexEventTypeByTime1786300000000 } from './1786300000000-IndexEventTypeByTime.js'
|
|
9
|
+
import { IndexEventCreatedAt1786400000000 } from './1786400000000-IndexEventCreatedAt.js'
|
|
10
|
+
import { CreateTwinSubjectEvents1786500000000 } from './1786500000000-CreateTwinSubjectEvents.js'
|
|
8
11
|
|
|
9
12
|
export const migrations = [
|
|
10
13
|
RenameTwinInstanceBoardToModel1786000000000,
|
|
11
14
|
PromoteEventActionAndSyncWarnings1786100000000,
|
|
12
|
-
CarryLiveFeedCursor1786200000000
|
|
15
|
+
CarryLiveFeedCursor1786200000000,
|
|
16
|
+
IndexEventTypeByTime1786300000000,
|
|
17
|
+
IndexEventCreatedAt1786400000000,
|
|
18
|
+
CreateTwinSubjectEvents1786500000000
|
|
13
19
|
]
|
package/server/routes.ts
CHANGED
|
@@ -1,5 +1,73 @@
|
|
|
1
|
-
/*
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
/*
|
|
2
|
+
* 이 패키지가 여는 HTTP 자리 — 지금은 웹훅 하나다.
|
|
3
|
+
*
|
|
4
|
+
* ── 왜 여기인가 ─────────────────────────────────────────────────────────────
|
|
5
|
+
* 커넥터는 주소를 만들 수 없다. 어댑터 계약에 주소를 등록하는 자리가 없고, 있어서도 안 된다 — 주소는
|
|
6
|
+
* 앱의 것이고 커넥터마다 제각기 열면 인증도 제각기가 된다.
|
|
7
|
+
*
|
|
8
|
+
* 그래서 입구는 **하나**다. 어느 연결의 것인지는 주소의 이름으로 가려내고, 본문을 우리 어휘로 옮기는
|
|
9
|
+
* 것은 그 연결의 커넥터가 한다(§`reference-hook`).
|
|
10
|
+
*
|
|
11
|
+
* ── 왜 도메인 공개 라우터인가 ───────────────────────────────────────────────
|
|
12
|
+
* 밀어 주는 쪽은 사람이 아니다. 사람 인증(JWT)을 요구하면 연결된 시스템이 우리 토큰을 발급받아
|
|
13
|
+
* 갱신하며 관리해야 하고, 대부분의 현장에서 그것 때문에 연동이 서지 않는다.
|
|
14
|
+
*
|
|
15
|
+
* 대신 **연결마다 비밀값**을 두고 그것으로 확인한다. 비밀값을 선언하지 않은 연결은 훅을 받지 않는다 —
|
|
16
|
+
* 인증 없는 입구를 열어 두는 것보다 훅을 못 쓰는 것이 낫다.
|
|
17
|
+
*/
|
|
18
|
+
import { ingestCanonicalRecords } from './engine/canonical-ingest.js'
|
|
19
|
+
import { TwinEngine } from './engine/twin-engine.js'
|
|
20
|
+
import { handleHook } from './service/reference/reference-hook.js'
|
|
21
|
+
|
|
22
|
+
process.on('bootstrap-module-domain-public-route' as any, (_app: any, router: any) => {
|
|
23
|
+
/*
|
|
24
|
+
* 주소에 트윈까지 적는다 — 어느 트윈에 넣을지 짐작하지 않는다.
|
|
25
|
+
*
|
|
26
|
+
* 한 연결이 여러 사이트를 만들면 트윈도 여럿이다. 본문을 보고 고르게 하면 그 판단이 커넥터마다
|
|
27
|
+
* 달라지고, 틀리면 다른 공장의 사실이 이 공장에 들어간다. 주소가 말하게 하면 그 일이 없다.
|
|
28
|
+
*/
|
|
29
|
+
router.post('/twin/hook/:source/:instanceId', async (ctx: any) => {
|
|
30
|
+
const domain = ctx.state?.domain
|
|
31
|
+
if (!domain) {
|
|
32
|
+
ctx.status = 400
|
|
33
|
+
ctx.body = { ok: false, error: 'no domain context' }
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
const outcome = await handleHook({
|
|
37
|
+
domainId: domain.id,
|
|
38
|
+
source: String(ctx.params.source ?? ''),
|
|
39
|
+
instanceId: String(ctx.params.instanceId ?? ''),
|
|
40
|
+
headers: (ctx.request?.headers ?? {}) as Record<string, unknown>,
|
|
41
|
+
body: ctx.request?.body,
|
|
42
|
+
/*
|
|
43
|
+
* 폴링이 쓰는 길을 그대로 쓴다 — 검증도 유입도 같은 자리다. 그래서 트윈 쪽에서는 물어서 받은
|
|
44
|
+
* 것과 밀어 받은 것이 구별되지 않는다. 구별되는 것은 유입 장부의 `sources` 한 줄뿐이다.
|
|
45
|
+
*/
|
|
46
|
+
ingest: (instanceId, records) => {
|
|
47
|
+
const { accepted, rejected, masterData } = ingestCanonicalRecords(
|
|
48
|
+
records as any,
|
|
49
|
+
domain.id,
|
|
50
|
+
new Date().toISOString(),
|
|
51
|
+
TwinEngine.factScope(domain.id, instanceId)
|
|
52
|
+
)
|
|
53
|
+
/* 사건이 아닌 것은 상태만 세운다 — 밀어 받는 길도 같은 규율이다(§`applyMasterData`). */
|
|
54
|
+
if (masterData.length) TwinEngine.applyMasterData(domain.id, instanceId, masterData)
|
|
55
|
+
const { applied, duplicates } = TwinEngine.ingestLiveResult(domain.id, instanceId, accepted, 'hook')
|
|
56
|
+
/*
|
|
57
|
+
* 커널이 받지 못한 수를 이름으로 남긴다 — 트윈이 멈춘 사이 버려진 레코드를 「통과」로 세면
|
|
58
|
+
* 화면이 100% 라고 말한다. 중복으로 걸러진 것은 버려진 것이 아니므로 빼고 센다.
|
|
59
|
+
*/
|
|
60
|
+
const undelivered = Math.max(0, accepted.length - applied - duplicates)
|
|
61
|
+
TwinEngine.recordIngestResult(domain.id, instanceId, records.length, rejected, Date.now(), undelivered)
|
|
62
|
+
return { applied, duplicates, rejected: rejected.length }
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
ctx.status = outcome.status
|
|
66
|
+
ctx.body = outcome.body
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
/* 나머지 셋은 이 패키지가 쓰지 않는다 — 자리는 남겨 둔다(다른 모듈이 같은 신호를 기다린다). */
|
|
71
|
+
process.on('bootstrap-module-global-public-route' as any, (_app: any, _router: any) => {})
|
|
72
|
+
process.on('bootstrap-module-global-private-route' as any, (_app: any, _router: any) => {})
|
|
73
|
+
process.on('bootstrap-module-domain-private-route' as any, (_app: any, _router: any) => {})
|
package/server/service/index.ts
CHANGED
|
@@ -24,6 +24,7 @@ export * from './twin-lifecycle/domain-catalog.js'
|
|
|
24
24
|
|
|
25
25
|
/* IMPORT ENTITIES AND RESOLVERS */
|
|
26
26
|
import { entities as TwinEventEntities } from './twin-event/index.js'
|
|
27
|
+
import { entities as TwinSubjectEntities, resolvers as TwinSubjectResolvers } from './twin-subject/index.js'
|
|
27
28
|
import { entities as TwinAuditEntities, resolvers as TwinAuditResolvers } from './twin-audit/index.js'
|
|
28
29
|
import { entities as TwinInstanceEntities } from './twin-instance/index.js'
|
|
29
30
|
import { entities as TwinStructureEntities } from './twin-structure/index.js'
|
|
@@ -41,10 +42,13 @@ import { resolvers as TwinForecastResolvers } from './twin-forecast/index.js'
|
|
|
41
42
|
import { resolvers as TwinAttentionResolvers } from './twin-attention/index.js'
|
|
42
43
|
import { resolvers as TwinMetricsResolvers } from './twin-metrics/index.js'
|
|
43
44
|
import { resolvers as TwinReadinessResolvers } from './twin-readiness/index.js'
|
|
45
|
+
/* 지난 기록 채우기 — 저널에만 쓰는 문. 라이브 유입과 판단이 달라 문을 따로 둔다. */
|
|
46
|
+
import { resolvers as TwinBackfillResolvers } from './twin-backfill/index.js'
|
|
44
47
|
|
|
45
48
|
export const entities = [
|
|
46
49
|
/* ENTITIES */
|
|
47
50
|
...TwinEventEntities,
|
|
51
|
+
...TwinSubjectEntities,
|
|
48
52
|
...TwinIngestWindowEntities,
|
|
49
53
|
...TwinAuditEntities,
|
|
50
54
|
...TwinInstanceEntities,
|
|
@@ -71,11 +75,13 @@ export const schema = {
|
|
|
71
75
|
...TwinAttentionResolvers,
|
|
72
76
|
...TwinMetricsResolvers,
|
|
73
77
|
...TwinReadinessResolvers,
|
|
78
|
+
...TwinBackfillResolvers,
|
|
74
79
|
...TwinIngestWindowResolvers,
|
|
75
80
|
...TwinModelResolvers,
|
|
76
81
|
...TwinSpaceResolvers,
|
|
77
82
|
...TwinTargetResolvers,
|
|
78
|
-
...TwinReferenceResolvers
|
|
83
|
+
...TwinReferenceResolvers,
|
|
84
|
+
...TwinSubjectResolvers
|
|
79
85
|
],
|
|
80
86
|
directives: {}
|
|
81
87
|
}
|
|
@@ -22,7 +22,7 @@ import { capabilitiesOf, getAdapter, type ControlCommand, type ControlResult, ty
|
|
|
22
22
|
/**
|
|
23
23
|
* 이 트윈의 원본에 구동 명령을 보낸다 — **원본이 없거나 능력을 선언하지 않으면 거절한다.**
|
|
24
24
|
*
|
|
25
|
-
* 거절을
|
|
25
|
+
* 거절을 알리지 않고 하지 않는다: 사유 코드를 돌려주고 화면이 그것을 사람 말로 옮긴다. 「아무 일도 일어나지
|
|
26
26
|
* 않았다」가 가장 비싼 답이다.
|
|
27
27
|
*/
|
|
28
28
|
export async function routeControl(domainId: string, instanceId: string, command: ControlCommand): Promise<ControlResult> {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* ── 왜 있나 (2026-08-06) ────────────────────────────────────────────────────
|
|
5
5
|
* 실 시스템을 연결해 "거기 있는 것" 으로 트윈을 만드는 경로에서, 발견이 실패하면 화면이
|
|
6
|
-
* **"사이트를 찾지 못했습니다"** 라고만 말했다. 네 가지가 그 한
|
|
6
|
+
* **"사이트를 찾지 못했습니다"** 라고만 말했다. 네 가지가 그 한 문장에 섞여 있었다.
|
|
7
7
|
*
|
|
8
8
|
* ① 그 레퍼런스가 등록돼 있지 않다 (`!ref → []`)
|
|
9
9
|
* ② 어댑터 종류를 모른다 (`!adapter → []`)
|
|
@@ -71,7 +71,7 @@ export const discoveryTimedOut = (source: string, seconds: number): DiscoveryErr
|
|
|
71
71
|
})
|
|
72
72
|
|
|
73
73
|
/**
|
|
74
|
-
* 어댑터가 준 것을 결과로
|
|
74
|
+
* 어댑터가 준 것을 결과로 처리한다.
|
|
75
75
|
*
|
|
76
76
|
* **사이트가 없는 것을 실패로 만들지 않는다** — 연결이 되고 조회가 성공했는데 공장이 하나도 없는
|
|
77
77
|
* 시스템은 있을 수 있다(권한이 좁거나 아직 등록되지 않은 경우). 그것은 사실이고, 화면은 "없다" 고
|
|
@@ -81,7 +81,7 @@ export function discovered<S>(sites: readonly S[] | undefined | null): Discovery
|
|
|
81
81
|
return { sites: Array.isArray(sites) ? [...sites] : [] }
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
/** 실패를 결과로
|
|
84
|
+
/** 실패를 결과로 처리한다 — 사이트는 비어 있다(반쪽 목록을 성공처럼 보이게 하지 않는다). */
|
|
85
85
|
export function failed<S>(error: DiscoveryError): DiscoveryResult<S> {
|
|
86
86
|
return { sites: [], error }
|
|
87
87
|
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 웹훅의 규약 — 상태 코드 · 비밀값 · 어느 트윈인가.
|
|
3
|
+
*
|
|
4
|
+
* 데이터베이스를 부르는 부분과 갈라 둔다(§`reference-hook`): 규약만 있는 자리라야 시험이 엔티티를
|
|
5
|
+
* 끌고 오지 않는다. 실제로 그것 때문에 시험이 서지 않았다.
|
|
6
|
+
*
|
|
7
|
+
* ── 왜 필요한가 ─────────────────────────────────────────────────────────────
|
|
8
|
+
* 지금 모든 커넥터가 주기적으로 물어봅니다(폴링). 그 방식은 두 가지를 치릅니다. 연결된 시스템이 값을
|
|
9
|
+
* 갱신하는 주기보다 자주 물으면 같은 값을 되풀이 받고, 그쪽 서버에 그만큼 부담을 줍니다. 실제로
|
|
10
|
+
* 태양광 발전소에 10초마다 물으면서 10분마다 바뀌는 값을 받아 왔습니다.
|
|
11
|
+
*
|
|
12
|
+
* 연결된 시스템이 「바뀌었다」를 말해 줄 수 있으면 그것을 받는 것이 낫습니다. 이 파일이 그 입구입니다.
|
|
13
|
+
*
|
|
14
|
+
* ── 규약 ────────────────────────────────────────────────────────────────────
|
|
15
|
+
*
|
|
16
|
+
* 주소 POST /domain/<도메인>/twin/hook/<연결이름>/<트윈>
|
|
17
|
+
* 인증 연결 설정에 저장한 비밀값(`hookSecret`)을 헤더로 보낸다
|
|
18
|
+
* X-Twin-Hook-Secret: <비밀값> 또는 Authorization: Bearer <비밀값>
|
|
19
|
+
* 본문 연결된 시스템이 정한 모양 그대로. 우리 어휘로 옮기는 것은 커넥터의 일이다
|
|
20
|
+
* 응답 아래 §`hookStatus`
|
|
21
|
+
*
|
|
22
|
+
* ── 왜 사람 인증(JWT)이 아닌가 ──────────────────────────────────────────────
|
|
23
|
+
* 밀어 주는 쪽은 사람이 아닙니다. 우리 토큰을 발급받아 갱신하며 관리하라고 요구하면 대부분의 현장에서
|
|
24
|
+
* 연동이 서지 않습니다. 그래서 연결마다 비밀값을 두고 그것으로 확인합니다.
|
|
25
|
+
*
|
|
26
|
+
* **비밀값이 없는 연결은 훅을 받지 않습니다.** 기본이 거부입니다 — 인증 없는 입구를 열어 두는 것보다
|
|
27
|
+
* 훅을 못 쓰는 것이 낫습니다.
|
|
28
|
+
*
|
|
29
|
+
* ── 왜 커넥터가 옮기나 (시나리오가 아니라) ──────────────────────────────────
|
|
30
|
+
* 연결된 시스템의 웹훅은 그쪽이 정한 고정된 모양으로 옵니다. 그 모양을 아는 것은 그 커넥터이고,
|
|
31
|
+
* 옮기는 판단은 코드여야 테스트로 지킬 수 있습니다. 실제로 지금 커넥터들이 코드로 판단합니다 —
|
|
32
|
+
* 통신이 끊긴 설비의 값은 보내지 않고, 잰 시각이 나아가지 않은 줄은 거릅니다. 그 판단을 변환식으로
|
|
33
|
+
* 옮기면 약해지고 테스트가 없어집니다.
|
|
34
|
+
*
|
|
35
|
+
* ── 밀어 주기만으로는 안 된다 ───────────────────────────────────────────────
|
|
36
|
+
* 훅은 반드시 놓칩니다 — 우리가 내려가 있을 때, 그쪽이 못 보냈을 때, 네트워크가 끊겼을 때. 그래서
|
|
37
|
+
* 훅을 쓰는 연결도 **드문 폴링을 유지합니다.** 폴링의 역할이 「값을 가져오는 것」에서 「놓친 것이 있나
|
|
38
|
+
* 확인하는 것」으로 바뀌는 것입니다. 그 주기는 연결 설정이 정합니다.
|
|
39
|
+
*
|
|
40
|
+
* ── 같은 것이 두 번 오는 것 ─────────────────────────────────────────────────
|
|
41
|
+
* 밀어 주는 방식은 재전송이 정상입니다(그쪽이 우리 응답을 못 받으면 다시 보냅니다). 그것은 유입
|
|
42
|
+
* 경계가 이미 거릅니다(§`FactDeduper`) — 이 파일이 따로 하지 않습니다. 응답에 몇 건이 중복이었는지
|
|
43
|
+
* 함께 알립니다.
|
|
44
|
+
*/
|
|
45
|
+
/** 훅 요청의 결과 — 상태 코드와 본문을 함께 정한다(부르는 쪽이 그대로 답한다). */
|
|
46
|
+
export interface HookOutcome {
|
|
47
|
+
status: number
|
|
48
|
+
body: Record<string, unknown>
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 응답 코드 규약 — **재시도로 해결되는 것과 그렇지 않은 것을 가른다.**
|
|
53
|
+
*
|
|
54
|
+
* 밀어 주는 쪽은 응답 코드로 재시도를 정합니다. 그래서 우리가 코드를 아무렇게나 주면 두 가지가
|
|
55
|
+
* 일어납니다: 고칠 수 없는 것을 영원히 다시 보내거나, 잠깐의 문제로 사라진 사실을 아무도 모릅니다.
|
|
56
|
+
*
|
|
57
|
+
* 200 받아서 반영했다
|
|
58
|
+
* 401 비밀값이 다르다 재시도해도 같다
|
|
59
|
+
* 404 모르는 연결이거나 모르는 트윈 재시도해도 같다
|
|
60
|
+
* 400 본문을 우리 레코드로 옮길 수 없다 재시도해도 같다(그쪽 모양이 바뀐 것이다)
|
|
61
|
+
* 409 그 트윈이 지금 실시간으로 돌지 않는다 사람이 트윈을 띄워야 한다
|
|
62
|
+
* 501 그 커넥터가 훅을 받을 줄 모른다 코드가 없다
|
|
63
|
+
* 500 우리 쪽 오류 재시도하면 될 수 있다
|
|
64
|
+
*/
|
|
65
|
+
export const HOOK_STATUS = {
|
|
66
|
+
ok: 200,
|
|
67
|
+
badSecret: 401,
|
|
68
|
+
unknownTarget: 404,
|
|
69
|
+
badPayload: 400,
|
|
70
|
+
notLive: 409,
|
|
71
|
+
unsupported: 501,
|
|
72
|
+
failed: 500
|
|
73
|
+
} as const
|
|
74
|
+
|
|
75
|
+
/** 헤더에서 비밀값을 꺼낸다 — 두 가지 방식을 받는다(밀어 주는 쪽의 관습이 갈린다). */
|
|
76
|
+
export function secretFromHeaders(headers: Record<string, unknown> | undefined): string {
|
|
77
|
+
const pick = (k: string) => String((headers ?? {})[k] ?? '').trim()
|
|
78
|
+
const direct = pick('x-twin-hook-secret')
|
|
79
|
+
if (direct) return direct
|
|
80
|
+
const auth = pick('authorization')
|
|
81
|
+
return auth.toLowerCase().startsWith('bearer ') ? auth.slice(7).trim() : ''
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 비밀값 비교 — **길이가 다르면 바로 거절하고, 같으면 끝까지 비교한다.**
|
|
86
|
+
*
|
|
87
|
+
* 앞에서 다른 것을 발견하고 곧바로 답하면 응답 시간이 달라지고, 그 차이로 비밀값을 한 글자씩 알아낼 수
|
|
88
|
+
* 있습니다. 그래서 같은 길이면 전부를 비교합니다.
|
|
89
|
+
*/
|
|
90
|
+
export function secretMatches(given: string, expected: string): boolean {
|
|
91
|
+
if (!expected || !given || given.length !== expected.length) return false
|
|
92
|
+
let diff = 0
|
|
93
|
+
for (let i = 0; i < expected.length; i++) diff |= given.charCodeAt(i) ^ expected.charCodeAt(i)
|
|
94
|
+
return diff === 0
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** 이 연결이 이 트윈을 만들었나 — 남의 트윈에 밀어 넣지 못하게. */
|
|
98
|
+
export function producedInstance(ref: { scopeSpec?: any } | null | undefined, instanceId: string): { siteId: string } | undefined {
|
|
99
|
+
const produced: any[] = (ref?.scopeSpec as any)?.produced ?? []
|
|
100
|
+
const hit = produced.find(p => String(p?.instanceId ?? '') === instanceId)
|
|
101
|
+
return hit?.siteId ? { siteId: String(hit.siteId) } : undefined
|
|
102
|
+
}
|
|
103
|
+
|
|
@@ -9,14 +9,14 @@
|
|
|
9
9
|
* `<source>-site` 로 파생돼, 새 트윈은 늘 새 현장처럼 생겼다. 데이터로는 가능한 일이 저작으로는
|
|
10
10
|
* 불가능했다.
|
|
11
11
|
*
|
|
12
|
-
* ── 없는 공간에
|
|
12
|
+
* ── 없는 공간에 알리지 않고 새 공간을 만들지 않는다 ────────────────────────────────
|
|
13
13
|
* "이 현장에 더한다" 는 요청이 오타·낡은 목록으로 빗나갔을 때 새 공간을 만들어 주면, 사용자는
|
|
14
14
|
* 합쳤다고 믿고 화면에서는 따로 놀게 된다 — 나중에 두 현장을 손으로 합쳐야 하고, 그때는 이미
|
|
15
15
|
* 이벤트가 양쪽에 쌓여 있다. 그래서 **모르는 공간이면 거절한다.**
|
|
16
16
|
*
|
|
17
|
-
* ── 같은 id 를 쓰는 구역은
|
|
17
|
+
* ── 같은 id 를 쓰는 구역은 알리지 않고 합쳐지므로 알린다 ──────────────────────────
|
|
18
18
|
* 공간 content 의 구역·랜드마크는 id 로 합집합을 만든다(`unionById`). 두 렌즈가 우연히 같은 구역
|
|
19
|
-
* id(`a-yard` 따위)를 쓰면 서로 다른 구역이 하나로
|
|
19
|
+
* id(`a-yard` 따위)를 쓰면 서로 다른 구역이 하나로 합쳐진다. 막을 일은 아니다(같은 현장의 같은 구역을
|
|
20
20
|
* 가리키는 정당한 경우가 많다) — 다만 **모르고 지나가지 않게** 경고로 돌려준다.
|
|
21
21
|
*/
|
|
22
22
|
|
|
@@ -37,7 +37,7 @@ export const INGEST_SPACE_NOT_FOUND = 'space-not-found'
|
|
|
37
37
|
* @param masterSpaceId 마스터에서 파생된 공간 id(요청이 없을 때의 기본).
|
|
38
38
|
* @param requested 사용자가 고른 기존 공간 id. 없으면 빈 값/undefined.
|
|
39
39
|
* @param requestedExists 그 공간이 이 테넌트에 실제로 있나(호출측이 조회해 넘긴다).
|
|
40
|
-
* @throws 요청한 공간이 없으면 —
|
|
40
|
+
* @throws 요청한 공간이 없으면 — 알리지 않고 새로 만들지 않는다.
|
|
41
41
|
*/
|
|
42
42
|
export function resolveIngestSpace(masterSpaceId: string, requested: string | undefined | null, requestedExists: boolean): IngestSpaceChoice {
|
|
43
43
|
const want = (requested ?? '').trim()
|