@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
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TwinSubjectMutation = exports.TwinSubjectBackfillResult = exports.TwinSubjectQuery = exports.TwinSubjectHistory = exports.TwinSubjectEventView = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const type_graphql_1 = require("type-graphql");
|
|
6
|
+
const shell_1 = require("@things-factory/shell");
|
|
7
|
+
const twin_subject_event_js_1 = require("./twin-subject-event.js");
|
|
8
|
+
const event_subjects_js_1 = require("./event-subjects.js");
|
|
9
|
+
const twin_subject_writer_js_1 = require("./twin-subject-writer.js");
|
|
10
|
+
const twin_subject_backfill_js_1 = require("./twin-subject-backfill.js");
|
|
11
|
+
const read_time_js_1 = require("../../engine/read-time.js");
|
|
12
|
+
/*
|
|
13
|
+
* **대상별 사건 목록 조회** — 360 화면의 이력이 이 문으로 온다.
|
|
14
|
+
*
|
|
15
|
+
* ── 무엇이 달라지나 (2026-08-27 실측) ───────────────────────────────────────
|
|
16
|
+
* 지금 화면은 저널을 직접 묻고 그 조회가 64초다. 「무엇으로 걸러 시각순으로 정렬해 앞에서 30개」라는
|
|
17
|
+
* 모양에 맞는 목차가 저널에 없고, 얹어도 풀리지 않는다(정렬이 목차를 무력화한다 · 품목과 로트가 한
|
|
18
|
+
* 문자열에 붙어 있다).
|
|
19
|
+
*
|
|
20
|
+
* 이 표는 그 모양대로 생겼다. 목차가 `(도메인, 트윈, 종류, 식별자, 시각)` 이므로 위에서 30줄 읽고
|
|
21
|
+
* 끝난다 — 정렬이 없다.
|
|
22
|
+
*
|
|
23
|
+
* ── 준비되지 않은 구간을 말한다 ─────────────────────────────────────────────
|
|
24
|
+
* 이 표는 **앞으로만 채운다**(소급하지 않는다). 그래서 표가 만들어진 시점보다 앞의 사건은 여기에 없다.
|
|
25
|
+
* 그것을 「사건이 없다」로 그리면 화면이 거짓을 말한다. `coveredFrom` 이 그 경계를 함께 낸다 —
|
|
26
|
+
* 화면은 「이 목록은 …부터입니다. 그 앞은 기간을 지정해 조회하십시오」를 말할 수 있다.
|
|
27
|
+
*
|
|
28
|
+
* 적지 못한 줄이 있으면 그것도 함께 낸다(`writeFailures`). 알리지 않고 비면 화면이 「그 대상에는 사건이
|
|
29
|
+
* 없다」고 말하게 된다.
|
|
30
|
+
*
|
|
31
|
+
* 설계: `operato-twin/design/plans/read-models.md`
|
|
32
|
+
*/
|
|
33
|
+
let TwinSubjectEventView = class TwinSubjectEventView {
|
|
34
|
+
};
|
|
35
|
+
exports.TwinSubjectEventView = TwinSubjectEventView;
|
|
36
|
+
tslib_1.__decorate([
|
|
37
|
+
(0, type_graphql_1.Field)({ description: 'Event type.' }),
|
|
38
|
+
tslib_1.__metadata("design:type", String)
|
|
39
|
+
], TwinSubjectEventView.prototype, "eventType", void 0);
|
|
40
|
+
tslib_1.__decorate([
|
|
41
|
+
(0, type_graphql_1.Field)({ description: 'When it happened, by the twin clock.' }),
|
|
42
|
+
tslib_1.__metadata("design:type", Date)
|
|
43
|
+
], TwinSubjectEventView.prototype, "eventTime", void 0);
|
|
44
|
+
tslib_1.__decorate([
|
|
45
|
+
(0, type_graphql_1.Field)(type => type_graphql_1.Int, { description: 'Journal revision — where this sits in the record.' }),
|
|
46
|
+
tslib_1.__metadata("design:type", Number)
|
|
47
|
+
], TwinSubjectEventView.prototype, "revision", void 0);
|
|
48
|
+
tslib_1.__decorate([
|
|
49
|
+
(0, type_graphql_1.Field)({ nullable: true, description: 'Journal row id. Null when that row has been pruned.' }),
|
|
50
|
+
tslib_1.__metadata("design:type", String)
|
|
51
|
+
], TwinSubjectEventView.prototype, "eventId", void 0);
|
|
52
|
+
exports.TwinSubjectEventView = TwinSubjectEventView = tslib_1.__decorate([
|
|
53
|
+
(0, type_graphql_1.ObjectType)({ description: 'One row of a subject history list.' })
|
|
54
|
+
], TwinSubjectEventView);
|
|
55
|
+
let TwinSubjectHistory = class TwinSubjectHistory {
|
|
56
|
+
};
|
|
57
|
+
exports.TwinSubjectHistory = TwinSubjectHistory;
|
|
58
|
+
tslib_1.__decorate([
|
|
59
|
+
(0, type_graphql_1.Field)(type => [TwinSubjectEventView], { description: 'The events on this page, newest first.' }),
|
|
60
|
+
tslib_1.__metadata("design:type", Array)
|
|
61
|
+
], TwinSubjectHistory.prototype, "items", void 0);
|
|
62
|
+
tslib_1.__decorate([
|
|
63
|
+
(0, type_graphql_1.Field)({
|
|
64
|
+
nullable: true,
|
|
65
|
+
description: 'Oldest event time this list can speak for, for this twin. Events before it are in the journal but not in this list — the screen must say so rather than implying there were none. Null when the list holds nothing for this twin yet.'
|
|
66
|
+
}),
|
|
67
|
+
tslib_1.__metadata("design:type", Date)
|
|
68
|
+
], TwinSubjectHistory.prototype, "coveredFrom", void 0);
|
|
69
|
+
tslib_1.__decorate([
|
|
70
|
+
(0, type_graphql_1.Field)({ nullable: true, description: 'Cursor for the next page: pass it back as before. Null when this page is the last.' }),
|
|
71
|
+
tslib_1.__metadata("design:type", Date)
|
|
72
|
+
], TwinSubjectHistory.prototype, "nextBefore", void 0);
|
|
73
|
+
tslib_1.__decorate([
|
|
74
|
+
(0, type_graphql_1.Field)(type => type_graphql_1.Int, {
|
|
75
|
+
description: 'Rows this twin failed to write into the read model since start. Greater than zero means this list is incomplete for reasons other than coverage — the journal still holds those events.'
|
|
76
|
+
}),
|
|
77
|
+
tslib_1.__metadata("design:type", Number)
|
|
78
|
+
], TwinSubjectHistory.prototype, "writeFailures", void 0);
|
|
79
|
+
exports.TwinSubjectHistory = TwinSubjectHistory = tslib_1.__decorate([
|
|
80
|
+
(0, type_graphql_1.ObjectType)({
|
|
81
|
+
description: 'History of one subject, newest first, plus how far back this list can speak for. The list is a read model built forward from the journal, so it does not cover events older than coveredFrom — say so on screen instead of drawing an empty list as "nothing happened".'
|
|
82
|
+
})
|
|
83
|
+
], TwinSubjectHistory);
|
|
84
|
+
const PAGE_MAX = 200;
|
|
85
|
+
let TwinSubjectQuery = class TwinSubjectQuery {
|
|
86
|
+
async twinSubjectHistory(context, instanceId, subjectKind, subjectId, limit, before) {
|
|
87
|
+
const domain = context.state.domain;
|
|
88
|
+
const kind = String(subjectKind ?? '').trim();
|
|
89
|
+
/*
|
|
90
|
+
* 종류는 닫힌 목록이다. 모르는 값이면 빈 목록이 아니라 오류를 낸다 — 빈 목록으로 답하면 오타가
|
|
91
|
+
* 「그 대상에는 사건이 없다」로 보인다.
|
|
92
|
+
*/
|
|
93
|
+
if (!event_subjects_js_1.SUBJECT_KINDS.includes(kind)) {
|
|
94
|
+
throw new Error(`unknown subjectKind "${subjectKind}" — one of: ${event_subjects_js_1.SUBJECT_KINDS.join(', ')}`);
|
|
95
|
+
}
|
|
96
|
+
const id = String(subjectId ?? '').trim();
|
|
97
|
+
if (!id)
|
|
98
|
+
throw new Error('subjectId is required — an empty subject would read the whole twin');
|
|
99
|
+
const take = Math.min(Math.max(Number(limit) || 30, 1), PAGE_MAX);
|
|
100
|
+
const repo = (0, shell_1.getRepository)(twin_subject_event_js_1.TwinSubjectEvent);
|
|
101
|
+
const qb = repo
|
|
102
|
+
.createQueryBuilder('s')
|
|
103
|
+
.where('s.domain = :domainId', { domainId: domain.id })
|
|
104
|
+
.andWhere('s.instanceId = :instanceId', { instanceId })
|
|
105
|
+
.andWhere('s.subjectKind = :kind', { kind })
|
|
106
|
+
.andWhere('s.subjectId = :id', { id })
|
|
107
|
+
/*
|
|
108
|
+
* 같은 시각에 사건이 여럿일 수 있다. 그때 순서를 리비전으로 가른다 — 그러지 않으면 같은 목록을
|
|
109
|
+
* 두 번 물었을 때 순서가 달라지고, 시각 커서로 넘기는 페이지에서 본 줄이 다시 나온다.
|
|
110
|
+
*/
|
|
111
|
+
.orderBy('s.eventTime', 'DESC')
|
|
112
|
+
.addOrderBy('s.revision', 'DESC')
|
|
113
|
+
.take(take + 1);
|
|
114
|
+
/* 시각으로 페이지를 넘긴다 — 머리에 계속 쌓이는 목록에서 건너뛰기(offset)는 본 줄을 다시 낸다. */
|
|
115
|
+
if (before)
|
|
116
|
+
qb.andWhere('s.eventTime < :before', { before });
|
|
117
|
+
const rows = await qb.getMany();
|
|
118
|
+
const items = rows.slice(0, take).map(r => ({
|
|
119
|
+
eventType: r.eventType,
|
|
120
|
+
eventTime: r.eventTime,
|
|
121
|
+
revision: Number(r.revision),
|
|
122
|
+
...(r.eventId ? { eventId: r.eventId } : {})
|
|
123
|
+
}));
|
|
124
|
+
const nextBefore = rows.length > take ? items[items.length - 1]?.eventTime : undefined;
|
|
125
|
+
return {
|
|
126
|
+
items,
|
|
127
|
+
...(await coverage(domain.id, instanceId)),
|
|
128
|
+
...(nextBefore ? { nextBefore } : {}),
|
|
129
|
+
writeFailures: (0, twin_subject_writer_js_1.subjectWriteFailures)(domain.id, instanceId)
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
exports.TwinSubjectQuery = TwinSubjectQuery;
|
|
134
|
+
tslib_1.__decorate([
|
|
135
|
+
(0, type_graphql_1.Query)(returns => TwinSubjectHistory, {
|
|
136
|
+
description: 'Events that refer to one subject (item, lot, order, location, equipment or task), newest first. Answered from a read model shaped for this question — one index lookup, no sort — instead of scanning the event journal. Paging is by time (before), not offset, because the list grows at the head. Always reports coveredFrom: this read model is built forward and does not contain events older than that, and an empty list before it means "not covered", not "nothing happened".'
|
|
137
|
+
}),
|
|
138
|
+
tslib_1.__param(0, (0, type_graphql_1.Ctx)()),
|
|
139
|
+
tslib_1.__param(1, (0, type_graphql_1.Arg)('instanceId', { description: 'The twin instance to read.' })),
|
|
140
|
+
tslib_1.__param(2, (0, type_graphql_1.Arg)('subjectKind', { description: `One of: ${event_subjects_js_1.SUBJECT_KINDS.join(', ')}.` })),
|
|
141
|
+
tslib_1.__param(3, (0, type_graphql_1.Arg)('subjectId', { description: 'Identifier of the thing, within its kind.' })),
|
|
142
|
+
tslib_1.__param(4, (0, type_graphql_1.Arg)('limit', type => type_graphql_1.Int, { nullable: true, description: `How many rows, at most ${PAGE_MAX}.` })),
|
|
143
|
+
tslib_1.__param(5, (0, type_graphql_1.Arg)('before', { nullable: true, description: 'Read events strictly older than this time — the previous page cursor.' })),
|
|
144
|
+
tslib_1.__metadata("design:type", Function),
|
|
145
|
+
tslib_1.__metadata("design:paramtypes", [Object, String, String, String, Number, Date]),
|
|
146
|
+
tslib_1.__metadata("design:returntype", Promise)
|
|
147
|
+
], TwinSubjectQuery.prototype, "twinSubjectHistory", null);
|
|
148
|
+
exports.TwinSubjectQuery = TwinSubjectQuery = tslib_1.__decorate([
|
|
149
|
+
(0, type_graphql_1.Resolver)()
|
|
150
|
+
], TwinSubjectQuery);
|
|
151
|
+
/**
|
|
152
|
+
* 이 표가 이 트윈에 대해 말할 수 있는 가장 오래된 시각 — 저장하지 않고 조회할 때 구한다.
|
|
153
|
+
*
|
|
154
|
+
* 저장하면 실제 자료와 어긋날 수 있고, 어긋나면 어느 쪽이 사실인지 알 수 없다. 목차
|
|
155
|
+
* `(도메인, 트윈, 시각)` 의 첫 줄 하나라서 값이 싸다.
|
|
156
|
+
*/
|
|
157
|
+
async function coverage(domainId, instanceId) {
|
|
158
|
+
const first = await (0, shell_1.getRepository)(twin_subject_event_js_1.TwinSubjectEvent)
|
|
159
|
+
.createQueryBuilder('s')
|
|
160
|
+
.select('s.eventTime', 'eventTime')
|
|
161
|
+
.where('s.domain = :domainId', { domainId })
|
|
162
|
+
.andWhere('s.instanceId = :instanceId', { instanceId })
|
|
163
|
+
.orderBy('s.eventTime', 'ASC')
|
|
164
|
+
.limit(1)
|
|
165
|
+
.getRawOne()
|
|
166
|
+
.catch(() => null);
|
|
167
|
+
/* 원시 결과의 컬럼 값은 드라이버에게 해석시킨다(§`hydratedDate`). */
|
|
168
|
+
const at = (0, read_time_js_1.hydratedDate)((0, shell_1.getRepository)(twin_subject_event_js_1.TwinSubjectEvent), 'eventTime', first?.eventTime);
|
|
169
|
+
return at ? { coveredFrom: at } : {};
|
|
170
|
+
}
|
|
171
|
+
let TwinSubjectBackfillResult = class TwinSubjectBackfillResult {
|
|
172
|
+
};
|
|
173
|
+
exports.TwinSubjectBackfillResult = TwinSubjectBackfillResult;
|
|
174
|
+
tslib_1.__decorate([
|
|
175
|
+
(0, type_graphql_1.Field)({ description: 'The twin that was filled.' }),
|
|
176
|
+
tslib_1.__metadata("design:type", String)
|
|
177
|
+
], TwinSubjectBackfillResult.prototype, "instanceId", void 0);
|
|
178
|
+
tslib_1.__decorate([
|
|
179
|
+
(0, type_graphql_1.Field)(type => type_graphql_1.Int, { description: 'Journal rows read.' }),
|
|
180
|
+
tslib_1.__metadata("design:type", Number)
|
|
181
|
+
], TwinSubjectBackfillResult.prototype, "read", void 0);
|
|
182
|
+
tslib_1.__decorate([
|
|
183
|
+
(0, type_graphql_1.Field)(type => type_graphql_1.Int, { description: 'Rows written into the read model.' }),
|
|
184
|
+
tslib_1.__metadata("design:type", Number)
|
|
185
|
+
], TwinSubjectBackfillResult.prototype, "written", void 0);
|
|
186
|
+
tslib_1.__decorate([
|
|
187
|
+
(0, type_graphql_1.Field)(type => type_graphql_1.Int, { description: 'Journal revision reached so far.' }),
|
|
188
|
+
tslib_1.__metadata("design:type", Number)
|
|
189
|
+
], TwinSubjectBackfillResult.prototype, "upToRevision", void 0);
|
|
190
|
+
tslib_1.__decorate([
|
|
191
|
+
(0, type_graphql_1.Field)(type => type_graphql_1.Int, { description: 'The target: the journal tip at the moment this fill started. Reaching it means the twin is fully filled.' }),
|
|
192
|
+
tslib_1.__metadata("design:type", Number)
|
|
193
|
+
], TwinSubjectBackfillResult.prototype, "targetRevision", void 0);
|
|
194
|
+
tslib_1.__decorate([
|
|
195
|
+
(0, type_graphql_1.Field)({ description: 'True when upToRevision has reached the target.' }),
|
|
196
|
+
tslib_1.__metadata("design:type", Boolean)
|
|
197
|
+
], TwinSubjectBackfillResult.prototype, "complete", void 0);
|
|
198
|
+
tslib_1.__decorate([
|
|
199
|
+
(0, type_graphql_1.Field)({ description: 'True when the call stopped on its time budget rather than finishing.' }),
|
|
200
|
+
tslib_1.__metadata("design:type", Boolean)
|
|
201
|
+
], TwinSubjectBackfillResult.prototype, "paused", void 0);
|
|
202
|
+
exports.TwinSubjectBackfillResult = TwinSubjectBackfillResult = tslib_1.__decorate([
|
|
203
|
+
(0, type_graphql_1.ObjectType)({ description: 'How far a backfill got.' })
|
|
204
|
+
], TwinSubjectBackfillResult);
|
|
205
|
+
let TwinSubjectMutation = class TwinSubjectMutation {
|
|
206
|
+
/* 쓰기는 정본 게이트를 단다 — 로그인만 하면 누구나 부를 수 있는 문을 두지 않는다. */
|
|
207
|
+
async backfillTwinSubjects(context, instanceId, budgetMs) {
|
|
208
|
+
const domain = context.state.domain;
|
|
209
|
+
/*
|
|
210
|
+
* 이어받기 인자가 없다 — 이 표의 현재 바닥(가장 오래된 시각)이 곧 이어받기 지점이다. 늘 그 아래만
|
|
211
|
+
* 채우므로 덩어리가 갈라지지 않고, 부르는 쪽이 아무것도 들고 있지 않아도 이어서 한다.
|
|
212
|
+
*/
|
|
213
|
+
return await (0, twin_subject_backfill_js_1.backfillSubjects)(domain.id, instanceId, { budgetMs: budgetMs ?? undefined });
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
exports.TwinSubjectMutation = TwinSubjectMutation;
|
|
217
|
+
tslib_1.__decorate([
|
|
218
|
+
(0, type_graphql_1.Directive)('@privilege(category: "twin", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)'),
|
|
219
|
+
(0, type_graphql_1.Mutation)(returns => TwinSubjectBackfillResult, {
|
|
220
|
+
description: 'Move already-stored events of one twin into the subject read model. Not automatic: the read model is built forward, so filling old events is an explicit request for a twin that already has a journal. Works in slices on the time axis: each call fills events older than what the read model already covers, so calling it again continues downward with nothing to pass in. Blocks cannot split, and calling it twice does not duplicate rows.'
|
|
221
|
+
}),
|
|
222
|
+
tslib_1.__param(0, (0, type_graphql_1.Ctx)()),
|
|
223
|
+
tslib_1.__param(1, (0, type_graphql_1.Arg)('instanceId', { description: 'The twin to fill.' })),
|
|
224
|
+
tslib_1.__param(2, (0, type_graphql_1.Arg)('budgetMs', type => type_graphql_1.Int, { nullable: true, description: 'How long this call may run, in milliseconds. Default 30000.' })),
|
|
225
|
+
tslib_1.__metadata("design:type", Function),
|
|
226
|
+
tslib_1.__metadata("design:paramtypes", [Object, String, Number]),
|
|
227
|
+
tslib_1.__metadata("design:returntype", Promise)
|
|
228
|
+
], TwinSubjectMutation.prototype, "backfillTwinSubjects", null);
|
|
229
|
+
exports.TwinSubjectMutation = TwinSubjectMutation = tslib_1.__decorate([
|
|
230
|
+
(0, type_graphql_1.Resolver)()
|
|
231
|
+
], TwinSubjectMutation);
|
|
232
|
+
//# sourceMappingURL=twin-subject-query.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"twin-subject-query.js","sourceRoot":"","sources":["../../../server/service/twin-subject/twin-subject-query.ts"],"names":[],"mappings":";;;;AAAA,+CAAqG;AACrG,iDAAqD;AAErD,mEAA0D;AAC1D,2DAAmD;AACnD,qEAA+D;AAC/D,yEAA6D;AAC7D,4DAAwD;AAExD;;;;;;;;;;;;;;;;;;;;GAoBG;AAGI,IAAM,oBAAoB,GAA1B,MAAM,oBAAoB;CAYhC,CAAA;AAZY,oDAAoB;AAE/B;IADC,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,aAAa,EAAE,CAAC;;uDACrB;AAGjB;IADC,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,sCAAsC,EAAE,CAAC;sCACpD,IAAI;uDAAA;AAGf;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,WAAW,EAAE,mDAAmD,EAAE,CAAC;;sDACzE;AAGhB;IADC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,qDAAqD,EAAE,CAAC;;qDAC9E;+BAXL,oBAAoB;IADhC,IAAA,yBAAU,EAAC,EAAE,WAAW,EAAE,oCAAoC,EAAE,CAAC;GACrD,oBAAoB,CAYhC;AAMM,IAAM,kBAAkB,GAAxB,MAAM,kBAAkB;CAmB9B,CAAA;AAnBY,gDAAkB;AAE7B;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,CAAC,oBAAoB,CAAC,EAAE,EAAE,WAAW,EAAE,wCAAwC,EAAE,CAAC;;iDACpE;AAO7B;IALC,IAAA,oBAAK,EAAC;QACL,QAAQ,EAAE,IAAI;QACd,WAAW,EACT,uOAAuO;KAC1O,CAAC;sCACY,IAAI;uDAAA;AAGlB;IADC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,oFAAoF,EAAE,CAAC;sCAChH,IAAI;sDAAA;AAMjB;IAJC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE;QAClB,WAAW,EACT,yLAAyL;KAC5L,CAAC;;yDACmB;6BAlBV,kBAAkB;IAJ9B,IAAA,yBAAU,EAAC;QACV,WAAW,EACT,yQAAyQ;KAC5Q,CAAC;GACW,kBAAkB,CAmB9B;AAED,MAAM,QAAQ,GAAG,GAAG,CAAA;AAGb,IAAM,gBAAgB,GAAtB,MAAM,gBAAgB;IAKrB,AAAN,KAAK,CAAC,kBAAkB,CACf,OAAY,EAC+C,UAAkB,EACP,WAAmB,EAChB,SAAiB,EACE,KAAc,EAEjH,MAAa;QAEb,MAAM,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,MAAM,CAAA;QACnC,MAAM,IAAI,GAAG,MAAM,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAA;QAC7C;;;WAGG;QACH,IAAI,CAAE,iCAAmC,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;YACzD,MAAM,IAAI,KAAK,CAAC,wBAAwB,WAAW,eAAe,iCAAa,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QAC/F,CAAC;QACD,MAAM,EAAE,GAAG,MAAM,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAA;QACzC,IAAI,CAAC,EAAE;YAAE,MAAM,IAAI,KAAK,CAAC,oEAAoE,CAAC,CAAA;QAE9F,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAA;QACjE,MAAM,IAAI,GAAG,IAAA,qBAAa,EAAC,wCAAgB,CAAC,CAAA;QAE5C,MAAM,EAAE,GAAG,IAAI;aACZ,kBAAkB,CAAC,GAAG,CAAC;aACvB,KAAK,CAAC,sBAAsB,EAAE,EAAE,QAAQ,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC;aACtD,QAAQ,CAAC,4BAA4B,EAAE,EAAE,UAAU,EAAE,CAAC;aACtD,QAAQ,CAAC,uBAAuB,EAAE,EAAE,IAAI,EAAE,CAAC;aAC3C,QAAQ,CAAC,mBAAmB,EAAE,EAAE,EAAE,EAAE,CAAC;YACtC;;;eAGG;aACF,OAAO,CAAC,aAAa,EAAE,MAAM,CAAC;aAC9B,UAAU,CAAC,YAAY,EAAE,MAAM,CAAC;aAChC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,CAAA;QACjB,+DAA+D;QAC/D,IAAI,MAAM;YAAE,EAAE,CAAC,QAAQ,CAAC,uBAAuB,EAAE,EAAE,MAAM,EAAE,CAAC,CAAA;QAE5D,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,OAAO,EAAE,CAAA;QAC/B,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YAC1C,SAAS,EAAE,CAAC,CAAC,SAAS;YACtB,SAAS,EAAE,CAAC,CAAC,SAAS;YACtB,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC;YAC5B,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC7C,CAAC,CAAC,CAAA;QACH,MAAM,UAAU,GAAG,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,SAAS,CAAA;QAEtF,OAAO;YACL,KAAK;YACL,GAAG,CAAC,MAAM,QAAQ,CAAC,MAAM,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC;YAC1C,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACrC,aAAa,EAAE,IAAA,6CAAoB,EAAC,MAAM,CAAC,EAAE,EAAE,UAAU,CAAC;SAC3D,CAAA;IACH,CAAC;CACF,CAAA;AA7DY,4CAAgB;AAKrB;IAJL,IAAA,oBAAK,EAAC,OAAO,CAAC,EAAE,CAAC,kBAAkB,EAAE;QACpC,WAAW,EACT,ydAAyd;KAC5d,CAAC;IAEC,mBAAA,IAAA,kBAAG,GAAE,CAAA;IACL,mBAAA,IAAA,kBAAG,EAAC,YAAY,EAAE,EAAE,WAAW,EAAE,4BAA4B,EAAE,CAAC,CAAA;IAChE,mBAAA,IAAA,kBAAG,EAAC,aAAa,EAAE,EAAE,WAAW,EAAE,WAAW,iCAAa,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAA;IAC3E,mBAAA,IAAA,kBAAG,EAAC,WAAW,EAAE,EAAE,WAAW,EAAE,2CAA2C,EAAE,CAAC,CAAA;IAC9E,mBAAA,IAAA,kBAAG,EAAC,OAAO,EAAE,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,0BAA0B,QAAQ,GAAG,EAAE,CAAC,CAAA;IACjG,mBAAA,IAAA,kBAAG,EAAC,QAAQ,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,uEAAuE,EAAE,CAAC,CAAA;;qFAC/G,IAAI;;0DAgDd;2BA5DU,gBAAgB;IAD5B,IAAA,uBAAQ,GAAE;GACE,gBAAgB,CA6D5B;AAED;;;;;GAKG;AACH,KAAK,UAAU,QAAQ,CAAC,QAAgB,EAAE,UAAkB;IAC1D,MAAM,KAAK,GAAG,MAAM,IAAA,qBAAa,EAAC,wCAAgB,CAAC;SAChD,kBAAkB,CAAC,GAAG,CAAC;SACvB,MAAM,CAAC,aAAa,EAAE,WAAW,CAAC;SAClC,KAAK,CAAC,sBAAsB,EAAE,EAAE,QAAQ,EAAE,CAAC;SAC3C,QAAQ,CAAC,4BAA4B,EAAE,EAAE,UAAU,EAAE,CAAC;SACtD,OAAO,CAAC,aAAa,EAAE,KAAK,CAAC;SAC7B,KAAK,CAAC,CAAC,CAAC;SACR,SAAS,EAA0B;SACnC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAA;IACpB,iDAAiD;IACjD,MAAM,EAAE,GAAG,IAAA,2BAAY,EAAC,IAAA,qBAAa,EAAC,wCAAgB,CAAC,EAAE,WAAW,EAAE,KAAK,EAAE,SAAS,CAAC,CAAA;IACvF,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAA;AACtC,CAAC;AAGM,IAAM,yBAAyB,GAA/B,MAAM,yBAAyB;CA2BrC,CAAA;AA3BY,8DAAyB;AAEpC;IADC,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,2BAA2B,EAAE,CAAC;;6DAClC;AAGlB;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,WAAW,EAAE,oBAAoB,EAAE,CAAC;;uDAC9C;AAGZ;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,WAAW,EAAE,mCAAmC,EAAE,CAAC;;0DAC1D;AASf;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,WAAW,EAAE,kCAAkC,EAAE,CAAC;;+DACpD;AAGpB;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,WAAW,EAAE,0GAA0G,EAAE,CAAC;;iEAC1H;AAGtB;IADC,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,gDAAgD,EAAE,CAAC;;2DACxD;AAGjB;IADC,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,sEAAsE,EAAE,CAAC;;yDAChF;oCA1BJ,yBAAyB;IADrC,IAAA,yBAAU,EAAC,EAAE,WAAW,EAAE,yBAAyB,EAAE,CAAC;GAC1C,yBAAyB,CA2BrC;AAGM,IAAM,mBAAmB,GAAzB,MAAM,mBAAmB;IAC9B,qDAAqD;IAM/C,AAAN,KAAK,CAAC,oBAAoB,CACjB,OAAY,EACsC,UAAkB,EAE3E,QAAiB;QAEjB,MAAM,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,MAAM,CAAA;QACnC;;;WAGG;QACH,OAAO,MAAM,IAAA,2CAAgB,EAAC,MAAM,CAAC,EAAE,EAAE,UAAU,EAAE,EAAE,QAAQ,EAAE,QAAQ,IAAI,SAAS,EAAE,CAAC,CAAA;IAC3F,CAAC;CACF,CAAA;AApBY,kDAAmB;AAOxB;IALL,IAAA,wBAAS,EAAC,uGAAuG,CAAC;IAClH,IAAA,uBAAQ,EAAC,OAAO,CAAC,EAAE,CAAC,yBAAyB,EAAE;QAC9C,WAAW,EACT,obAAob;KACvb,CAAC;IAEC,mBAAA,IAAA,kBAAG,GAAE,CAAA;IACL,mBAAA,IAAA,kBAAG,EAAC,YAAY,EAAE,EAAE,WAAW,EAAE,mBAAmB,EAAE,CAAC,CAAA;IACvD,mBAAA,IAAA,kBAAG,EAAC,UAAU,EAAE,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,6DAA6D,EAAE,CAAC,CAAA;;;;+DAS9H;8BAnBU,mBAAmB;IAD/B,IAAA,uBAAQ,GAAE;GACE,mBAAmB,CAoB/B","sourcesContent":["import { Arg, Ctx, Directive, Field, Int, Mutation, ObjectType, Query, Resolver } from 'type-graphql'\nimport { getRepository } from '@things-factory/shell'\n\nimport { TwinSubjectEvent } from './twin-subject-event.js'\nimport { SUBJECT_KINDS } from './event-subjects.js'\nimport { subjectWriteFailures } from './twin-subject-writer.js'\nimport { backfillSubjects } from './twin-subject-backfill.js'\nimport { hydratedDate } from '../../engine/read-time.js'\n\n/*\n * **대상별 사건 목록 조회** — 360 화면의 이력이 이 문으로 온다.\n *\n * ── 무엇이 달라지나 (2026-08-27 실측) ───────────────────────────────────────\n * 지금 화면은 저널을 직접 묻고 그 조회가 64초다. 「무엇으로 걸러 시각순으로 정렬해 앞에서 30개」라는\n * 모양에 맞는 목차가 저널에 없고, 얹어도 풀리지 않는다(정렬이 목차를 무력화한다 · 품목과 로트가 한\n * 문자열에 붙어 있다).\n *\n * 이 표는 그 모양대로 생겼다. 목차가 `(도메인, 트윈, 종류, 식별자, 시각)` 이므로 위에서 30줄 읽고\n * 끝난다 — 정렬이 없다.\n *\n * ── 준비되지 않은 구간을 말한다 ─────────────────────────────────────────────\n * 이 표는 **앞으로만 채운다**(소급하지 않는다). 그래서 표가 만들어진 시점보다 앞의 사건은 여기에 없다.\n * 그것을 「사건이 없다」로 그리면 화면이 거짓을 말한다. `coveredFrom` 이 그 경계를 함께 낸다 —\n * 화면은 「이 목록은 …부터입니다. 그 앞은 기간을 지정해 조회하십시오」를 말할 수 있다.\n *\n * 적지 못한 줄이 있으면 그것도 함께 낸다(`writeFailures`). 알리지 않고 비면 화면이 「그 대상에는 사건이\n * 없다」고 말하게 된다.\n *\n * 설계: `operato-twin/design/plans/read-models.md`\n */\n\n@ObjectType({ description: 'One row of a subject history list.' })\nexport class TwinSubjectEventView {\n @Field({ description: 'Event type.' })\n eventType: string\n\n @Field({ description: 'When it happened, by the twin clock.' })\n eventTime: Date\n\n @Field(type => Int, { description: 'Journal revision — where this sits in the record.' })\n revision: number\n\n @Field({ nullable: true, description: 'Journal row id. Null when that row has been pruned.' })\n eventId?: string\n}\n\n@ObjectType({\n description:\n 'History of one subject, newest first, plus how far back this list can speak for. The list is a read model built forward from the journal, so it does not cover events older than coveredFrom — say so on screen instead of drawing an empty list as \"nothing happened\".'\n})\nexport class TwinSubjectHistory {\n @Field(type => [TwinSubjectEventView], { description: 'The events on this page, newest first.' })\n items: TwinSubjectEventView[]\n\n @Field({\n nullable: true,\n description:\n 'Oldest event time this list can speak for, for this twin. Events before it are in the journal but not in this list — the screen must say so rather than implying there were none. Null when the list holds nothing for this twin yet.'\n })\n coveredFrom?: Date\n\n @Field({ nullable: true, description: 'Cursor for the next page: pass it back as before. Null when this page is the last.' })\n nextBefore?: Date\n\n @Field(type => Int, {\n description:\n 'Rows this twin failed to write into the read model since start. Greater than zero means this list is incomplete for reasons other than coverage — the journal still holds those events.'\n })\n writeFailures: number\n}\n\nconst PAGE_MAX = 200\n\n@Resolver()\nexport class TwinSubjectQuery {\n @Query(returns => TwinSubjectHistory, {\n description:\n 'Events that refer to one subject (item, lot, order, location, equipment or task), newest first. Answered from a read model shaped for this question — one index lookup, no sort — instead of scanning the event journal. Paging is by time (before), not offset, because the list grows at the head. Always reports coveredFrom: this read model is built forward and does not contain events older than that, and an empty list before it means \"not covered\", not \"nothing happened\".'\n })\n async twinSubjectHistory(\n @Ctx() context: any,\n @Arg('instanceId', { description: 'The twin instance to read.' }) instanceId: string,\n @Arg('subjectKind', { description: `One of: ${SUBJECT_KINDS.join(', ')}.` }) subjectKind: string,\n @Arg('subjectId', { description: 'Identifier of the thing, within its kind.' }) subjectId: string,\n @Arg('limit', type => Int, { nullable: true, description: `How many rows, at most ${PAGE_MAX}.` }) limit?: number,\n @Arg('before', { nullable: true, description: 'Read events strictly older than this time — the previous page cursor.' })\n before?: Date\n ): Promise<TwinSubjectHistory> {\n const domain = context.state.domain\n const kind = String(subjectKind ?? '').trim()\n /*\n * 종류는 닫힌 목록이다. 모르는 값이면 빈 목록이 아니라 오류를 낸다 — 빈 목록으로 답하면 오타가\n * 「그 대상에는 사건이 없다」로 보인다.\n */\n if (!(SUBJECT_KINDS as readonly string[]).includes(kind)) {\n throw new Error(`unknown subjectKind \"${subjectKind}\" — one of: ${SUBJECT_KINDS.join(', ')}`)\n }\n const id = String(subjectId ?? '').trim()\n if (!id) throw new Error('subjectId is required — an empty subject would read the whole twin')\n\n const take = Math.min(Math.max(Number(limit) || 30, 1), PAGE_MAX)\n const repo = getRepository(TwinSubjectEvent)\n\n const qb = repo\n .createQueryBuilder('s')\n .where('s.domain = :domainId', { domainId: domain.id })\n .andWhere('s.instanceId = :instanceId', { instanceId })\n .andWhere('s.subjectKind = :kind', { kind })\n .andWhere('s.subjectId = :id', { id })\n /*\n * 같은 시각에 사건이 여럿일 수 있다. 그때 순서를 리비전으로 가른다 — 그러지 않으면 같은 목록을\n * 두 번 물었을 때 순서가 달라지고, 시각 커서로 넘기는 페이지에서 본 줄이 다시 나온다.\n */\n .orderBy('s.eventTime', 'DESC')\n .addOrderBy('s.revision', 'DESC')\n .take(take + 1)\n /* 시각으로 페이지를 넘긴다 — 머리에 계속 쌓이는 목록에서 건너뛰기(offset)는 본 줄을 다시 낸다. */\n if (before) qb.andWhere('s.eventTime < :before', { before })\n\n const rows = await qb.getMany()\n const items = rows.slice(0, take).map(r => ({\n eventType: r.eventType,\n eventTime: r.eventTime,\n revision: Number(r.revision),\n ...(r.eventId ? { eventId: r.eventId } : {})\n }))\n const nextBefore = rows.length > take ? items[items.length - 1]?.eventTime : undefined\n\n return {\n items,\n ...(await coverage(domain.id, instanceId)),\n ...(nextBefore ? { nextBefore } : {}),\n writeFailures: subjectWriteFailures(domain.id, instanceId)\n }\n }\n}\n\n/**\n * 이 표가 이 트윈에 대해 말할 수 있는 가장 오래된 시각 — 저장하지 않고 조회할 때 구한다.\n *\n * 저장하면 실제 자료와 어긋날 수 있고, 어긋나면 어느 쪽이 사실인지 알 수 없다. 목차\n * `(도메인, 트윈, 시각)` 의 첫 줄 하나라서 값이 싸다.\n */\nasync function coverage(domainId: string, instanceId: string): Promise<{ coveredFrom?: Date }> {\n const first = await getRepository(TwinSubjectEvent)\n .createQueryBuilder('s')\n .select('s.eventTime', 'eventTime')\n .where('s.domain = :domainId', { domainId })\n .andWhere('s.instanceId = :instanceId', { instanceId })\n .orderBy('s.eventTime', 'ASC')\n .limit(1)\n .getRawOne<{ eventTime: unknown }>()\n .catch(() => null)\n /* 원시 결과의 컬럼 값은 드라이버에게 해석시킨다(§`hydratedDate`). */\n const at = hydratedDate(getRepository(TwinSubjectEvent), 'eventTime', first?.eventTime)\n return at ? { coveredFrom: at } : {}\n}\n\n@ObjectType({ description: 'How far a backfill got.' })\nexport class TwinSubjectBackfillResult {\n @Field({ description: 'The twin that was filled.' })\n instanceId: string\n\n @Field(type => Int, { description: 'Journal rows read.' })\n read: number\n\n @Field(type => Int, { description: 'Rows written into the read model.' })\n written: number\n\n /*\n * 진행 상황을 리비전으로 말한다 — 옮기는 쪽이 저널을 리비전 순으로 읽고, 그 지점을 이 프로세스가 든다.\n *\n * 이 자리를 시각으로 적어 두었다가 옮기는 방식과 어긋났다. 응답의 칸과 실제로 드는 값이 다르면\n * 부르는 쪽이 진행을 판단할 수 없다.\n */\n @Field(type => Int, { description: 'Journal revision reached so far.' })\n upToRevision: number\n\n @Field(type => Int, { description: 'The target: the journal tip at the moment this fill started. Reaching it means the twin is fully filled.' })\n targetRevision: number\n\n @Field({ description: 'True when upToRevision has reached the target.' })\n complete: boolean\n\n @Field({ description: 'True when the call stopped on its time budget rather than finishing.' })\n paused: boolean\n}\n\n@Resolver()\nexport class TwinSubjectMutation {\n /* 쓰기는 정본 게이트를 단다 — 로그인만 하면 누구나 부를 수 있는 문을 두지 않는다. */\n @Directive('@privilege(category: \"twin\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Mutation(returns => TwinSubjectBackfillResult, {\n description:\n 'Move already-stored events of one twin into the subject read model. Not automatic: the read model is built forward, so filling old events is an explicit request for a twin that already has a journal. Works in slices on the time axis: each call fills events older than what the read model already covers, so calling it again continues downward with nothing to pass in. Blocks cannot split, and calling it twice does not duplicate rows.'\n })\n async backfillTwinSubjects(\n @Ctx() context: any,\n @Arg('instanceId', { description: 'The twin to fill.' }) instanceId: string,\n @Arg('budgetMs', type => Int, { nullable: true, description: 'How long this call may run, in milliseconds. Default 30000.' })\n budgetMs?: number\n ): Promise<TwinSubjectBackfillResult> {\n const domain = context.state.domain\n /*\n * 이어받기 인자가 없다 — 이 표의 현재 바닥(가장 오래된 시각)이 곧 이어받기 지점이다. 늘 그 아래만\n * 채우므로 덩어리가 갈라지지 않고, 부르는 쪽이 아무것도 들고 있지 않아도 이어서 한다.\n */\n return await backfillSubjects(domain.id, instanceId, { budgetMs: budgetMs ?? undefined })\n }\n}\n"]}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type ItemResolver } from './event-subjects.js';
|
|
2
|
+
export interface SubjectWriteEntry {
|
|
3
|
+
envelope: any;
|
|
4
|
+
revision: number;
|
|
5
|
+
/** 저널 줄의 id — 알면 함께 둔다. 벌크 저장에서는 모를 수 있다(그때는 비운다). */
|
|
6
|
+
eventId?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* 이 배치의 사건들이 가리키는 대상을 적는다 — 적은 줄 수를 돌려준다.
|
|
10
|
+
*
|
|
11
|
+
* `itemOf` 는 개체 식별자에서 선언된 품목을 찾아 주는 함수다. 없으면 품목 줄을 만들지 않는다 —
|
|
12
|
+
* 짐작으로 이으면 남의 품목 이력에 이 사건이 섞인다(§`event-subjects`).
|
|
13
|
+
*/
|
|
14
|
+
export declare function writeSubjectRows(domainId: string, instanceId: string, entries: readonly SubjectWriteEntry[], itemOf?: ItemResolver): Promise<number>;
|
|
15
|
+
/** 적지 못한 줄 수 — 조회하는 쪽이 「이 표가 온전한가」를 말할 수 있게. */
|
|
16
|
+
export declare function subjectWriteFailures(domainId: string, instanceId: string): number;
|
|
17
|
+
/** 시험이 쓰는 초기화 — 프로세스 안의 셈이므로 시험마다 비운다. */
|
|
18
|
+
export declare function resetSubjectWriteFailures(): void;
|
|
19
|
+
/**
|
|
20
|
+
* 보관 상한 — 이 트윈에서 이 시각보다 오래된 줄을 지운다.
|
|
21
|
+
*
|
|
22
|
+
* 이 표는 **버릴 수 있는 값**이다. 지우면 그 구간의 이력 조회가 느려지고(저널로 답한다) 사실은 사라지지
|
|
23
|
+
* 않는다. 그래서 저널보다 짧게 둘 수 있고, 그러는 편이 낫다 — 화면은 최근을 본다.
|
|
24
|
+
*
|
|
25
|
+
* 저널 정리와 같은 창에서 돌린다(§`TwinEngine.pruneJournal`). 나눠서 지우는 규율도 같다.
|
|
26
|
+
*/
|
|
27
|
+
export declare function pruneSubjectRows(domainId: string, instanceId: string, cutoff: Date, budgetMs: number, batch: number): Promise<number>;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.writeSubjectRows = writeSubjectRows;
|
|
4
|
+
exports.subjectWriteFailures = subjectWriteFailures;
|
|
5
|
+
exports.resetSubjectWriteFailures = resetSubjectWriteFailures;
|
|
6
|
+
exports.pruneSubjectRows = pruneSubjectRows;
|
|
7
|
+
const shell_1 = require("@things-factory/shell");
|
|
8
|
+
const log_js_1 = require("../../engine/log.js");
|
|
9
|
+
const read_time_js_1 = require("../../engine/read-time.js");
|
|
10
|
+
const twin_subject_event_js_1 = require("./twin-subject-event.js");
|
|
11
|
+
const event_subjects_js_1 = require("./event-subjects.js");
|
|
12
|
+
/*
|
|
13
|
+
* **대상별 사건 목록에 적는 쪽** — 저널을 쓸 때 함께 적는다.
|
|
14
|
+
*
|
|
15
|
+
* ── 왜 저널 쓰기와 같은 자리인가 ────────────────────────────────────────────
|
|
16
|
+
* 저널을 쓰는 문은 하나다(`flushJournal` → `persistBatch`·`persistCarried`). 그 자리에서 함께 적으면
|
|
17
|
+
* 두 표가 어긋나지 않는다. 다른 자리에서 나중에 적으면 「저널에는 있고 이 표에는 없는 사건」이 생기고,
|
|
18
|
+
* 그것은 화면에서 오류 없이 빠진 사건이 된다.
|
|
19
|
+
*
|
|
20
|
+
* ── 저널이 먼저다 ──────────────────────────────────────────────────────────
|
|
21
|
+
* 이 표에 적다가 실패해도 저널 쓰기를 되돌리지 않는다. 사실의 원본은 저널이고, 이 표는 다시 만들 수
|
|
22
|
+
* 있다. 반대로 두면 조회를 빠르게 하려고 만든 표가 사실을 잃는 원인이 된다.
|
|
23
|
+
*
|
|
24
|
+
* **그러나 알리지 않고 넘기지 않는다.** 실패를 세어 창마다 한 줄 남긴다 — 알리지 않고 비면 화면이 「그 대상에는
|
|
25
|
+
* 사건이 없다」고 말하게 된다.
|
|
26
|
+
*
|
|
27
|
+
* ── 앞으로만 채운다 ────────────────────────────────────────────────────────
|
|
28
|
+
* 이미 저장된 사건을 소급해서 채우지 않는다. 이 표가 만들어진 뒤 들어오는 사건만 적힌다. 그래서 새
|
|
29
|
+
* 조회 축이 필요할 때 3,128만 줄을 다시 만들지 않는다. 준비되지 않은 구간은 조회하는 쪽이 말한다
|
|
30
|
+
* (§`twin-subject-query` 의 `coveredFrom`).
|
|
31
|
+
*
|
|
32
|
+
* 설계: `operato-twin/design/plans/read-models.md`
|
|
33
|
+
*/
|
|
34
|
+
/** 한 번에 넣는 줄 수 — 저널 쓰기와 같은 규율(드라이버 파라미터 상한). */
|
|
35
|
+
const CHUNK = 500;
|
|
36
|
+
/** 적다가 실패한 것을 센다 — 트윈마다. 알리지 않고 비는 것을 막는 유일한 신호다. */
|
|
37
|
+
const failures = new Map();
|
|
38
|
+
/** 같은 실패를 매번 적지 않는다 — 로그가 그 자체로 부하가 된다. */
|
|
39
|
+
const FAIL_LOG_MS = 60_000;
|
|
40
|
+
/**
|
|
41
|
+
* 이 배치의 사건들이 가리키는 대상을 적는다 — 적은 줄 수를 돌려준다.
|
|
42
|
+
*
|
|
43
|
+
* `itemOf` 는 개체 식별자에서 선언된 품목을 찾아 주는 함수다. 없으면 품목 줄을 만들지 않는다 —
|
|
44
|
+
* 짐작으로 이으면 남의 품목 이력에 이 사건이 섞인다(§`event-subjects`).
|
|
45
|
+
*/
|
|
46
|
+
async function writeSubjectRows(domainId, instanceId, entries, itemOf) {
|
|
47
|
+
if (!entries.length)
|
|
48
|
+
return 0;
|
|
49
|
+
const rows = [];
|
|
50
|
+
for (const { envelope, revision, eventId } of entries) {
|
|
51
|
+
const eventTime = timeOf(envelope);
|
|
52
|
+
/*
|
|
53
|
+
* 시각을 모르는 사건은 이 표에 넣지 않는다. 이 표의 유일한 쓸모가 「시각순 목록」이고, 시각이 없는
|
|
54
|
+
* 줄은 그 목록의 어디에도 놓을 수 없다. 넣고 「지금」으로 메우면 그 사건이 최근으로 보인다.
|
|
55
|
+
*/
|
|
56
|
+
if (!eventTime)
|
|
57
|
+
continue;
|
|
58
|
+
const eventType = String(envelope?.eventType ?? '').trim();
|
|
59
|
+
if (!eventType)
|
|
60
|
+
continue;
|
|
61
|
+
for (const s of (0, event_subjects_js_1.subjectsOf)(envelope, { itemOf })) {
|
|
62
|
+
rows.push({
|
|
63
|
+
domain: { id: domainId },
|
|
64
|
+
instanceId,
|
|
65
|
+
subjectKind: s.kind,
|
|
66
|
+
subjectId: s.id,
|
|
67
|
+
eventTime,
|
|
68
|
+
revision,
|
|
69
|
+
eventType,
|
|
70
|
+
...(eventId ? { eventId } : {})
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (!rows.length)
|
|
75
|
+
return 0;
|
|
76
|
+
try {
|
|
77
|
+
const repo = (0, shell_1.getRepository)(twin_subject_event_js_1.TwinSubjectEvent);
|
|
78
|
+
for (let i = 0; i < rows.length; i += CHUNK)
|
|
79
|
+
await repo.insert(rows.slice(i, i + CHUNK));
|
|
80
|
+
return rows.length;
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
noteFailure(domainId, instanceId, rows.length, err?.message ?? String(err));
|
|
84
|
+
return 0;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** 사건의 시각 — 봉투가 말하는 값만 쓴다. 없으면 `null`(지금으로 메우지 않는다). */
|
|
88
|
+
function timeOf(envelope) {
|
|
89
|
+
/* 봉투는 컬럼이 아니다 — 정규 계약이 절대시각을 요구하므로 그 규칙으로 읽는다(§`absoluteTime`). */
|
|
90
|
+
return (0, read_time_js_1.absoluteTime)(envelope?.eventTime ?? envelope?.data?.at);
|
|
91
|
+
}
|
|
92
|
+
function noteFailure(domainId, instanceId, rows, message) {
|
|
93
|
+
const key = `${domainId}:${instanceId}`;
|
|
94
|
+
const f = failures.get(key) ?? { count: 0, lastLogMs: 0, lastError: '' };
|
|
95
|
+
f.count += rows;
|
|
96
|
+
f.lastError = message;
|
|
97
|
+
const now = Date.now();
|
|
98
|
+
if (now - f.lastLogMs >= FAIL_LOG_MS) {
|
|
99
|
+
f.lastLogMs = now;
|
|
100
|
+
(0, log_js_1.twinWarn)(`[twin-subject] "${instanceId}" 대상 목록에 적지 못했다 — 지금까지 ${f.count}줄. ` +
|
|
101
|
+
`저널에는 그 사건이 있으므로 사실은 잃지 않았지만, 이 트윈의 이력 화면에 그만큼이 비어 있다: ${message}`);
|
|
102
|
+
}
|
|
103
|
+
failures.set(key, f);
|
|
104
|
+
}
|
|
105
|
+
/** 적지 못한 줄 수 — 조회하는 쪽이 「이 표가 온전한가」를 말할 수 있게. */
|
|
106
|
+
function subjectWriteFailures(domainId, instanceId) {
|
|
107
|
+
return failures.get(`${domainId}:${instanceId}`)?.count ?? 0;
|
|
108
|
+
}
|
|
109
|
+
/** 시험이 쓰는 초기화 — 프로세스 안의 셈이므로 시험마다 비운다. */
|
|
110
|
+
function resetSubjectWriteFailures() {
|
|
111
|
+
failures.clear();
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* 보관 상한 — 이 트윈에서 이 시각보다 오래된 줄을 지운다.
|
|
115
|
+
*
|
|
116
|
+
* 이 표는 **버릴 수 있는 값**이다. 지우면 그 구간의 이력 조회가 느려지고(저널로 답한다) 사실은 사라지지
|
|
117
|
+
* 않는다. 그래서 저널보다 짧게 둘 수 있고, 그러는 편이 낫다 — 화면은 최근을 본다.
|
|
118
|
+
*
|
|
119
|
+
* 저널 정리와 같은 창에서 돌린다(§`TwinEngine.pruneJournal`). 나눠서 지우는 규율도 같다.
|
|
120
|
+
*/
|
|
121
|
+
async function pruneSubjectRows(domainId, instanceId, cutoff, budgetMs, batch) {
|
|
122
|
+
let deleted = 0;
|
|
123
|
+
const startedMs = Date.now();
|
|
124
|
+
for (;;) {
|
|
125
|
+
/*
|
|
126
|
+
* 경계를 시각으로 찾는다 — 목차가 `(domain, instance, eventTime)` 이므로 그 순서로 물어야 쓴다.
|
|
127
|
+
* 저널 정리에서 같은 것을 실측으로 배웠다(리비전 순으로 물으면 목차를 못 쓴다).
|
|
128
|
+
*/
|
|
129
|
+
const edge = await (0, shell_1.getRepository)(twin_subject_event_js_1.TwinSubjectEvent)
|
|
130
|
+
.createQueryBuilder('s')
|
|
131
|
+
.select('s.eventTime', 'eventTime')
|
|
132
|
+
.where('s.domain = :domainId', { domainId })
|
|
133
|
+
.andWhere('s.instanceId = :instanceId', { instanceId })
|
|
134
|
+
.andWhere('s.eventTime < :cutoff', { cutoff })
|
|
135
|
+
.orderBy('s.eventTime', 'ASC')
|
|
136
|
+
.offset(batch - 1)
|
|
137
|
+
.limit(1)
|
|
138
|
+
.getRawOne()
|
|
139
|
+
.catch(() => null);
|
|
140
|
+
/* 원시 결과의 컬럼 값은 드라이버에게 해석시킨다(§`hydratedDate`). */
|
|
141
|
+
const upTo = (0, read_time_js_1.hydratedDate)((0, shell_1.getRepository)(twin_subject_event_js_1.TwinSubjectEvent), 'eventTime', edge?.eventTime);
|
|
142
|
+
const del = (0, shell_1.getRepository)(twin_subject_event_js_1.TwinSubjectEvent)
|
|
143
|
+
.createQueryBuilder()
|
|
144
|
+
.delete()
|
|
145
|
+
.from(twin_subject_event_js_1.TwinSubjectEvent)
|
|
146
|
+
.where('domain_id = :domainId', { domainId })
|
|
147
|
+
.andWhere('instance_id = :instanceId', { instanceId });
|
|
148
|
+
const res = await (upTo
|
|
149
|
+
? del.andWhere('event_time <= :upTo', { upTo })
|
|
150
|
+
: del.andWhere('event_time < :cutoff', { cutoff }))
|
|
151
|
+
.execute()
|
|
152
|
+
.catch(err => {
|
|
153
|
+
(0, log_js_1.twinError)('[twin-subject] prune fail', err?.message ?? err);
|
|
154
|
+
return { affected: 0 };
|
|
155
|
+
});
|
|
156
|
+
const n = res.affected ?? 0;
|
|
157
|
+
deleted += n;
|
|
158
|
+
if (!n)
|
|
159
|
+
break;
|
|
160
|
+
if (Date.now() - startedMs >= budgetMs)
|
|
161
|
+
break;
|
|
162
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
163
|
+
}
|
|
164
|
+
return deleted;
|
|
165
|
+
}
|
|
166
|
+
//# sourceMappingURL=twin-subject-writer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"twin-subject-writer.js","sourceRoot":"","sources":["../../../server/service/twin-subject/twin-subject-writer.ts"],"names":[],"mappings":"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","sourcesContent":["import { getRepository } from '@things-factory/shell'\n\nimport { twinError, twinWarn } from '../../engine/log.js'\nimport { absoluteTime, hydratedDate } from '../../engine/read-time.js'\nimport { TwinSubjectEvent } from './twin-subject-event.js'\nimport { subjectsOf, type ItemResolver } from './event-subjects.js'\n\n/*\n * **대상별 사건 목록에 적는 쪽** — 저널을 쓸 때 함께 적는다.\n *\n * ── 왜 저널 쓰기와 같은 자리인가 ────────────────────────────────────────────\n * 저널을 쓰는 문은 하나다(`flushJournal` → `persistBatch`·`persistCarried`). 그 자리에서 함께 적으면\n * 두 표가 어긋나지 않는다. 다른 자리에서 나중에 적으면 「저널에는 있고 이 표에는 없는 사건」이 생기고,\n * 그것은 화면에서 오류 없이 빠진 사건이 된다.\n *\n * ── 저널이 먼저다 ──────────────────────────────────────────────────────────\n * 이 표에 적다가 실패해도 저널 쓰기를 되돌리지 않는다. 사실의 원본은 저널이고, 이 표는 다시 만들 수\n * 있다. 반대로 두면 조회를 빠르게 하려고 만든 표가 사실을 잃는 원인이 된다.\n *\n * **그러나 알리지 않고 넘기지 않는다.** 실패를 세어 창마다 한 줄 남긴다 — 알리지 않고 비면 화면이 「그 대상에는\n * 사건이 없다」고 말하게 된다.\n *\n * ── 앞으로만 채운다 ────────────────────────────────────────────────────────\n * 이미 저장된 사건을 소급해서 채우지 않는다. 이 표가 만들어진 뒤 들어오는 사건만 적힌다. 그래서 새\n * 조회 축이 필요할 때 3,128만 줄을 다시 만들지 않는다. 준비되지 않은 구간은 조회하는 쪽이 말한다\n * (§`twin-subject-query` 의 `coveredFrom`).\n *\n * 설계: `operato-twin/design/plans/read-models.md`\n */\n\n/** 한 번에 넣는 줄 수 — 저널 쓰기와 같은 규율(드라이버 파라미터 상한). */\nconst CHUNK = 500\n\n/** 적다가 실패한 것을 센다 — 트윈마다. 알리지 않고 비는 것을 막는 유일한 신호다. */\nconst failures = new Map<string, { count: number; lastLogMs: number; lastError: string }>()\n\n/** 같은 실패를 매번 적지 않는다 — 로그가 그 자체로 부하가 된다. */\nconst FAIL_LOG_MS = 60_000\n\nexport interface SubjectWriteEntry {\n envelope: any\n revision: number\n /** 저널 줄의 id — 알면 함께 둔다. 벌크 저장에서는 모를 수 있다(그때는 비운다). */\n eventId?: string\n}\n\n/**\n * 이 배치의 사건들이 가리키는 대상을 적는다 — 적은 줄 수를 돌려준다.\n *\n * `itemOf` 는 개체 식별자에서 선언된 품목을 찾아 주는 함수다. 없으면 품목 줄을 만들지 않는다 —\n * 짐작으로 이으면 남의 품목 이력에 이 사건이 섞인다(§`event-subjects`).\n */\nexport async function writeSubjectRows(\n domainId: string,\n instanceId: string,\n entries: readonly SubjectWriteEntry[],\n itemOf?: ItemResolver\n): Promise<number> {\n if (!entries.length) return 0\n\n const rows: any[] = []\n for (const { envelope, revision, eventId } of entries) {\n const eventTime = timeOf(envelope)\n /*\n * 시각을 모르는 사건은 이 표에 넣지 않는다. 이 표의 유일한 쓸모가 「시각순 목록」이고, 시각이 없는\n * 줄은 그 목록의 어디에도 놓을 수 없다. 넣고 「지금」으로 메우면 그 사건이 최근으로 보인다.\n */\n if (!eventTime) continue\n const eventType = String(envelope?.eventType ?? '').trim()\n if (!eventType) continue\n\n for (const s of subjectsOf(envelope, { itemOf })) {\n rows.push({\n domain: { id: domainId },\n instanceId,\n subjectKind: s.kind,\n subjectId: s.id,\n eventTime,\n revision,\n eventType,\n ...(eventId ? { eventId } : {})\n })\n }\n }\n if (!rows.length) return 0\n\n try {\n const repo = getRepository(TwinSubjectEvent)\n for (let i = 0; i < rows.length; i += CHUNK) await repo.insert(rows.slice(i, i + CHUNK))\n return rows.length\n } catch (err: any) {\n noteFailure(domainId, instanceId, rows.length, err?.message ?? String(err))\n return 0\n }\n}\n\n/** 사건의 시각 — 봉투가 말하는 값만 쓴다. 없으면 `null`(지금으로 메우지 않는다). */\nfunction timeOf(envelope: any): Date | null {\n /* 봉투는 컬럼이 아니다 — 정규 계약이 절대시각을 요구하므로 그 규칙으로 읽는다(§`absoluteTime`). */\n return absoluteTime(envelope?.eventTime ?? envelope?.data?.at)\n}\n\nfunction noteFailure(domainId: string, instanceId: string, rows: number, message: string): void {\n const key = `${domainId}:${instanceId}`\n const f = failures.get(key) ?? { count: 0, lastLogMs: 0, lastError: '' }\n f.count += rows\n f.lastError = message\n const now = Date.now()\n if (now - f.lastLogMs >= FAIL_LOG_MS) {\n f.lastLogMs = now\n twinWarn(\n `[twin-subject] \"${instanceId}\" 대상 목록에 적지 못했다 — 지금까지 ${f.count}줄. ` +\n `저널에는 그 사건이 있으므로 사실은 잃지 않았지만, 이 트윈의 이력 화면에 그만큼이 비어 있다: ${message}`\n )\n }\n failures.set(key, f)\n}\n\n/** 적지 못한 줄 수 — 조회하는 쪽이 「이 표가 온전한가」를 말할 수 있게. */\nexport function subjectWriteFailures(domainId: string, instanceId: string): number {\n return failures.get(`${domainId}:${instanceId}`)?.count ?? 0\n}\n\n/** 시험이 쓰는 초기화 — 프로세스 안의 셈이므로 시험마다 비운다. */\nexport function resetSubjectWriteFailures(): void {\n failures.clear()\n}\n\n/**\n * 보관 상한 — 이 트윈에서 이 시각보다 오래된 줄을 지운다.\n *\n * 이 표는 **버릴 수 있는 값**이다. 지우면 그 구간의 이력 조회가 느려지고(저널로 답한다) 사실은 사라지지\n * 않는다. 그래서 저널보다 짧게 둘 수 있고, 그러는 편이 낫다 — 화면은 최근을 본다.\n *\n * 저널 정리와 같은 창에서 돌린다(§`TwinEngine.pruneJournal`). 나눠서 지우는 규율도 같다.\n */\nexport async function pruneSubjectRows(\n domainId: string,\n instanceId: string,\n cutoff: Date,\n budgetMs: number,\n batch: number\n): Promise<number> {\n let deleted = 0\n const startedMs = Date.now()\n for (;;) {\n /*\n * 경계를 시각으로 찾는다 — 목차가 `(domain, instance, eventTime)` 이므로 그 순서로 물어야 쓴다.\n * 저널 정리에서 같은 것을 실측으로 배웠다(리비전 순으로 물으면 목차를 못 쓴다).\n */\n const edge = await getRepository(TwinSubjectEvent)\n .createQueryBuilder('s')\n .select('s.eventTime', 'eventTime')\n .where('s.domain = :domainId', { domainId })\n .andWhere('s.instanceId = :instanceId', { instanceId })\n .andWhere('s.eventTime < :cutoff', { cutoff })\n .orderBy('s.eventTime', 'ASC')\n .offset(batch - 1)\n .limit(1)\n .getRawOne<{ eventTime: unknown }>()\n .catch(() => null)\n\n /* 원시 결과의 컬럼 값은 드라이버에게 해석시킨다(§`hydratedDate`). */\n const upTo = hydratedDate(getRepository(TwinSubjectEvent), 'eventTime', edge?.eventTime)\n\n const del = getRepository(TwinSubjectEvent)\n .createQueryBuilder()\n .delete()\n .from(TwinSubjectEvent)\n .where('domain_id = :domainId', { domainId })\n .andWhere('instance_id = :instanceId', { instanceId })\n const res = await (upTo\n ? del.andWhere('event_time <= :upTo', { upTo })\n : del.andWhere('event_time < :cutoff', { cutoff })\n )\n .execute()\n .catch(err => {\n twinError('[twin-subject] prune fail', err?.message ?? err)\n return { affected: 0 }\n })\n\n const n = res.affected ?? 0\n deleted += n\n if (!n) break\n if (Date.now() - startedMs >= budgetMs) break\n await new Promise(resolve => setImmediate(resolve))\n }\n return deleted\n}\n"]}
|
|
@@ -47,7 +47,7 @@ let TwinTargetResolver = class TwinTargetResolver {
|
|
|
47
47
|
throw new Error('target must be a finite number');
|
|
48
48
|
if (!spaceId && !instanceId)
|
|
49
49
|
throw new Error('a target must be scoped to a space or an instance');
|
|
50
|
-
/* 축만 있고 값이 없으면 **어디에 거는 목표인지 알 수 없다** —
|
|
50
|
+
/* 축만 있고 값이 없으면 **어디에 거는 목표인지 알 수 없다** — 알리지 않고 전체 목표로 만들지 않는다
|
|
51
51
|
(그러면 사용자는 구역 목표를 걸었다고 믿는데 전체가 바뀐다). */
|
|
52
52
|
if (axis && !axisKey)
|
|
53
53
|
throw new Error('axisKey is required when axis is given');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"twin-target-resolver.js","sourceRoot":"","sources":["../../../server/service/twin-target/twin-target-resolver.ts"],"names":[],"mappings":"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","sourcesContent":["import { Arg, Ctx, Directive, Float, Mutation, Query, Resolver } from 'type-graphql'\n\nimport { getRepository } from '@things-factory/shell'\n\nimport { TwinTarget } from './twin-target.js'\nimport { TWIN_METRIC, isTwinMetric } from '../../engine/kpi-target.js'\n\n/*\n * 운영 목표 조회·편집 — **현장 관리자가 트윈 화면에서** 고친다(사용자 결정 2026-08-02).\n *\n * 경영 KPI 관리메뉴가 아니라 여기인 이유: 목표가 숫자 옆에 있어야 맥락이 살고, 운영 임계는 자주\n * 조정된다. 조직 단위 기간 평가는 `@things-factory/kpi` 의 일이고 이쪽으로 베껴 오지 않는다.\n */\n/*\n * **쓰기는 막고 읽기는 연다** (2026-08-06 · ADR-0027).\n *\n * 이 리졸버는 트윈 리졸버 중 **유일하게** 권한 게이트를 갖는데, 처음에는 우회 표시가 없었다\n * (`@privilege(category:\"twin\", privilege:\"query\")`). 그러면 `twin/query`·`twin/mutation` 권한을\n * 실제로 부여받은 사용자만 쓸 수 있는데 — **저장소 어디에도 그 권한을 만드는 곳이 없다.** 결과는\n * 도메인 소유자도 슈퍼유저도 목표를 걸 수 없는 상태였다: 성과 화면의 목표 버튼이 \"Unauthorized\" 로\n * 떨어지고, 이 관점의 최상위 기능(§5 목표 프레임)이 **아무도 쓸 수 없는 채로** 놓여 있었다.\n *\n * 그래서 프레임워크의 통용 관용구를 따른다 — 가장 가까운 형제인 `@things-factory/kpi`(목표·지표 관리)가\n * 74곳 전부 `domainOwnerGranted: true, superUserGranted: true` 를 쓴다. 같은 성격의 조작(무엇을 잘한\n * 것으로 볼지 정하는 일)이므로 같은 자세를 쓴다: **현장을 소유한 사람은 바로 쓰고**, 다른 사람에게\n * 맡기려면 `twin` 권한을 부여한다.\n *\n * 이제 트윈의 **모든 변경**에 같은 게이트가 있다(ADR-0027) — 처음에는 이 리졸버 하나만 게이트를 갖는\n * 반쪽이었다. 조회 게이트는 걷었다: 같은 판정이 `twinKpi` 로 이미 열려 있어 지키는 것이 없었다.\n */\n@Resolver()\nexport class TwinTargetResolver {\n /* 조회에는 게이트를 두지 않는다 — 같은 판정이 `twinKpi.targets` 로 이미 열려 있어(그쪽은 게이트가\n 없다) 여기만 막으면 지키는 것 없이 화면만 깨진다. 읽기의 경계는 **테넌트 소속**이다. */\n @Query(returns => [String], {\n description:\n 'The metric ids a twin target may refer to. Fixed in one place so a target and the fact it judges can never drift into two names.'\n })\n async twinTargetMetrics(): Promise<string[]> {\n return Object.keys(TWIN_METRIC)\n }\n\n\n @Query(returns => [TwinTarget], {\n description:\n 'Operational targets in scope — site-wide targets for the space plus targets bound to the given instance. Callers judge with the narrower one when both exist.'\n })\n async twinTargets(\n @Ctx() context: ResolverContext,\n @Arg('spaceId', { nullable: true }) spaceId?: string,\n @Arg('instanceId', { nullable: true }) instanceId?: string\n ): Promise<TwinTarget[]> {\n const domainId = context.state.domain.id\n const rows = await getRepository(TwinTarget).find({ where: { domain: { id: domainId } } })\n return rows.filter(r => (spaceId && r.spaceId === spaceId) || (instanceId && r.instanceId === instanceId))\n }\n\n @Directive('@privilege(category: \"twin\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Mutation(returns => Boolean, {\n description:\n 'Create or update one operational target. The metric must be one of twinTargetMetrics — an unknown name is rejected rather than stored, because a target nobody can evaluate reads as \"no target\" and hides the gap.'\n })\n async saveTwinTarget(\n @Arg('metric') metric: string,\n @Arg('target', type => Float) target: number,\n @Ctx() context: ResolverContext,\n @Arg('spaceId', { nullable: true }) spaceId?: string,\n @Arg('instanceId', { nullable: true }) instanceId?: string,\n @Arg('axis', { nullable: true }) axis?: string,\n @Arg('axisKey', { nullable: true }) axisKey?: string\n ): Promise<boolean> {\n if (!isTwinMetric(metric)) throw new Error(`unknown twin metric \"${metric}\" — see twinTargetMetrics`)\n if (!Number.isFinite(target)) throw new Error('target must be a finite number')\n if (!spaceId && !instanceId) throw new Error('a target must be scoped to a space or an instance')\n /* 축만 있고 값이 없으면 **어디에 거는 목표인지 알 수 없다** — 조용히 전체 목표로 만들지 않는다\n (그러면 사용자는 구역 목표를 걸었다고 믿는데 전체가 바뀐다). */\n if (axis && !axisKey) throw new Error('axisKey is required when axis is given')\n if (!axis && axisKey) throw new Error('axis is required when axisKey is given')\n const domainId = context.state.domain.id\n const repo = getRepository(TwinTarget)\n const existing = await repo.findOne({\n where: { domain: { id: domainId }, spaceId: spaceId ?? null, instanceId: instanceId ?? null, metric, axis: axis ?? null, axisKey: axisKey ?? null } as any\n })\n await repo.save(\n repo.create({\n ...(existing ?? {}),\n domain: { id: domainId } as any,\n spaceId: spaceId ?? null as any,\n instanceId: instanceId ?? null as any,\n axis: axis ?? null as any,\n axisKey: axisKey ?? null as any,\n metric,\n target,\n ...(existing ? { updater: context.state.user } : { creator: context.state.user, updater: context.state.user })\n })\n )\n return true\n }\n\n @Directive('@privilege(category: \"twin\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Mutation(returns => Boolean, { description: 'Remove an operational target. Removing a target removes the judgement, not the fact — the metric keeps being reported without a verdict.' })\n async deleteTwinTarget(\n @Arg('metric') metric: string,\n @Ctx() context: ResolverContext,\n @Arg('spaceId', { nullable: true }) spaceId?: string,\n @Arg('instanceId', { nullable: true }) instanceId?: string,\n @Arg('axis', { nullable: true }) axis?: string,\n @Arg('axisKey', { nullable: true }) axisKey?: string\n ): Promise<boolean> {\n const domainId = context.state.domain.id\n const repo = getRepository(TwinTarget)\n\n /*\n * **찾아서 그 행만 지운다.** 예전에는 조건 객체를 `delete()` 에 그대로 넘겼는데 **아무 행도\n * 안 지워지면서 `true` 를 돌려줬다.** 사용자는 목표를 지웠다고 믿고 화면을 떠나는데 판정은 그대로\n * 남는다(실화면에서 그렇게 당했다).\n *\n * 무엇이 조건을 깨뜨렸는지는 단정하지 않는다 — 이 조건에는 `domain` 관계 객체와 `null` 값들이\n * 함께 들어 있었고(`instanceId`·`axis`·`axisKey`), 둘 다 `delete()` 의 조건 해석에서 흔한 함정이다.\n * 같은 관계-객체 패턴을 쓰는 `resetJournal` 은 실제로 잘 지우므로 관계 객체 하나만으로 설명되지\n * 않는다. `null` 조건이 `IS NULL` 이 아니라 `= NULL` 로 나갔을 가능성이 크다.\n *\n * 그래서 조건을 고쳐 맞추는 대신 **행을 찾아 id 로 지운다**: 저장 경로와 **같은 조회 규칙**을\n * 쓰므로 규칙이 갈라지지 않고(`findOne` 은 `null` 을 제대로 다룬다), 지우는 대상이 정확히 한\n * 행이라 넓게 지울 위험도 없다.\n */\n const row = await repo.findOne({\n where: {\n domain: { id: domainId },\n spaceId: spaceId ?? null,\n instanceId: instanceId ?? null,\n axis: axis ?? null,\n axisKey: axisKey ?? null,\n metric\n } as any\n })\n /* 지울 것이 없으면 **없었다고 답한다.** 지우지 않고 true 를 주면 위와 같은 거짓말이 된다. */\n if (!row) return false\n await repo.delete(row.id)\n return true\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"twin-target-resolver.js","sourceRoot":"","sources":["../../../server/service/twin-target/twin-target-resolver.ts"],"names":[],"mappings":"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","sourcesContent":["import { Arg, Ctx, Directive, Float, Mutation, Query, Resolver } from 'type-graphql'\n\nimport { getRepository } from '@things-factory/shell'\n\nimport { TwinTarget } from './twin-target.js'\nimport { TWIN_METRIC, isTwinMetric } from '../../engine/kpi-target.js'\n\n/*\n * 운영 목표 조회·편집 — **현장 관리자가 트윈 화면에서** 고친다(사용자 결정 2026-08-02).\n *\n * 경영 KPI 관리메뉴가 아니라 여기인 이유: 목표가 숫자 옆에 있어야 맥락이 살고, 운영 임계는 자주\n * 조정된다. 조직 단위 기간 평가는 `@things-factory/kpi` 의 일이고 이쪽으로 베껴 오지 않는다.\n */\n/*\n * **쓰기는 막고 읽기는 연다** (2026-08-06 · ADR-0027).\n *\n * 이 리졸버는 트윈 리졸버 중 **유일하게** 권한 게이트를 갖는데, 처음에는 우회 표시가 없었다\n * (`@privilege(category:\"twin\", privilege:\"query\")`). 그러면 `twin/query`·`twin/mutation` 권한을\n * 실제로 부여받은 사용자만 쓸 수 있는데 — **저장소 어디에도 그 권한을 만드는 곳이 없다.** 결과는\n * 도메인 소유자도 슈퍼유저도 목표를 걸 수 없는 상태였다: 성과 화면의 목표 버튼이 \"Unauthorized\" 로\n * 떨어지고, 이 관점의 최상위 기능(§5 목표 프레임)이 **아무도 쓸 수 없는 채로** 놓여 있었다.\n *\n * 그래서 프레임워크의 통용 관용구를 따른다 — 가장 가까운 형제인 `@things-factory/kpi`(목표·지표 관리)가\n * 74곳 전부 `domainOwnerGranted: true, superUserGranted: true` 를 쓴다. 같은 성격의 조작(무엇을 잘한\n * 것으로 볼지 정하는 일)이므로 같은 자세를 쓴다: **현장을 소유한 사람은 바로 쓰고**, 다른 사람에게\n * 맡기려면 `twin` 권한을 부여한다.\n *\n * 이제 트윈의 **모든 변경**에 같은 게이트가 있다(ADR-0027) — 처음에는 이 리졸버 하나만 게이트를 갖는\n * 반쪽이었다. 조회 게이트는 걷었다: 같은 판정이 `twinKpi` 로 이미 열려 있어 지키는 것이 없었다.\n */\n@Resolver()\nexport class TwinTargetResolver {\n /* 조회에는 게이트를 두지 않는다 — 같은 판정이 `twinKpi.targets` 로 이미 열려 있어(그쪽은 게이트가\n 없다) 여기만 막으면 지키는 것 없이 화면만 깨진다. 읽기의 경계는 **테넌트 소속**이다. */\n @Query(returns => [String], {\n description:\n 'The metric ids a twin target may refer to. Fixed in one place so a target and the fact it judges can never drift into two names.'\n })\n async twinTargetMetrics(): Promise<string[]> {\n return Object.keys(TWIN_METRIC)\n }\n\n\n @Query(returns => [TwinTarget], {\n description:\n 'Operational targets in scope — site-wide targets for the space plus targets bound to the given instance. Callers judge with the narrower one when both exist.'\n })\n async twinTargets(\n @Ctx() context: ResolverContext,\n @Arg('spaceId', { nullable: true }) spaceId?: string,\n @Arg('instanceId', { nullable: true }) instanceId?: string\n ): Promise<TwinTarget[]> {\n const domainId = context.state.domain.id\n const rows = await getRepository(TwinTarget).find({ where: { domain: { id: domainId } } })\n return rows.filter(r => (spaceId && r.spaceId === spaceId) || (instanceId && r.instanceId === instanceId))\n }\n\n @Directive('@privilege(category: \"twin\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Mutation(returns => Boolean, {\n description:\n 'Create or update one operational target. The metric must be one of twinTargetMetrics — an unknown name is rejected rather than stored, because a target nobody can evaluate reads as \"no target\" and hides the gap.'\n })\n async saveTwinTarget(\n @Arg('metric') metric: string,\n @Arg('target', type => Float) target: number,\n @Ctx() context: ResolverContext,\n @Arg('spaceId', { nullable: true }) spaceId?: string,\n @Arg('instanceId', { nullable: true }) instanceId?: string,\n @Arg('axis', { nullable: true }) axis?: string,\n @Arg('axisKey', { nullable: true }) axisKey?: string\n ): Promise<boolean> {\n if (!isTwinMetric(metric)) throw new Error(`unknown twin metric \"${metric}\" — see twinTargetMetrics`)\n if (!Number.isFinite(target)) throw new Error('target must be a finite number')\n if (!spaceId && !instanceId) throw new Error('a target must be scoped to a space or an instance')\n /* 축만 있고 값이 없으면 **어디에 거는 목표인지 알 수 없다** — 알리지 않고 전체 목표로 만들지 않는다\n (그러면 사용자는 구역 목표를 걸었다고 믿는데 전체가 바뀐다). */\n if (axis && !axisKey) throw new Error('axisKey is required when axis is given')\n if (!axis && axisKey) throw new Error('axis is required when axisKey is given')\n const domainId = context.state.domain.id\n const repo = getRepository(TwinTarget)\n const existing = await repo.findOne({\n where: { domain: { id: domainId }, spaceId: spaceId ?? null, instanceId: instanceId ?? null, metric, axis: axis ?? null, axisKey: axisKey ?? null } as any\n })\n await repo.save(\n repo.create({\n ...(existing ?? {}),\n domain: { id: domainId } as any,\n spaceId: spaceId ?? null as any,\n instanceId: instanceId ?? null as any,\n axis: axis ?? null as any,\n axisKey: axisKey ?? null as any,\n metric,\n target,\n ...(existing ? { updater: context.state.user } : { creator: context.state.user, updater: context.state.user })\n })\n )\n return true\n }\n\n @Directive('@privilege(category: \"twin\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Mutation(returns => Boolean, { description: 'Remove an operational target. Removing a target removes the judgement, not the fact — the metric keeps being reported without a verdict.' })\n async deleteTwinTarget(\n @Arg('metric') metric: string,\n @Ctx() context: ResolverContext,\n @Arg('spaceId', { nullable: true }) spaceId?: string,\n @Arg('instanceId', { nullable: true }) instanceId?: string,\n @Arg('axis', { nullable: true }) axis?: string,\n @Arg('axisKey', { nullable: true }) axisKey?: string\n ): Promise<boolean> {\n const domainId = context.state.domain.id\n const repo = getRepository(TwinTarget)\n\n /*\n * **찾아서 그 행만 지운다.** 예전에는 조건 객체를 `delete()` 에 그대로 넘겼는데 **아무 행도\n * 안 지워지면서 `true` 를 돌려줬다.** 사용자는 목표를 지웠다고 믿고 화면을 떠나는데 판정은 그대로\n * 남는다(실화면에서 그렇게 당했다).\n *\n * 무엇이 조건을 깨뜨렸는지는 단정하지 않는다 — 이 조건에는 `domain` 관계 객체와 `null` 값들이\n * 함께 들어 있었고(`instanceId`·`axis`·`axisKey`), 둘 다 `delete()` 의 조건 해석에서 흔한 함정이다.\n * 같은 관계-객체 패턴을 쓰는 `resetJournal` 은 실제로 잘 지우므로 관계 객체 하나만으로 설명되지\n * 않는다. `null` 조건이 `IS NULL` 이 아니라 `= NULL` 로 나갔을 가능성이 크다.\n *\n * 그래서 조건을 고쳐 맞추는 대신 **행을 찾아 id 로 지운다**: 저장 경로와 **같은 조회 규칙**을\n * 쓰므로 규칙이 갈라지지 않고(`findOne` 은 `null` 을 제대로 다룬다), 지우는 대상이 정확히 한\n * 행이라 넓게 지울 위험도 없다.\n */\n const row = await repo.findOne({\n where: {\n domain: { id: domainId },\n spaceId: spaceId ?? null,\n instanceId: instanceId ?? null,\n axis: axis ?? null,\n axisKey: axisKey ?? null,\n metric\n } as any\n })\n /* 지울 것이 없으면 **없었다고 답한다.** 지우지 않고 true 를 주면 위와 같은 거짓말이 된다. */\n if (!row) return false\n await repo.delete(row.id)\n return true\n }\n}\n"]}
|
|
@@ -21,7 +21,7 @@ export declare const energyTag: (instanceId: string) => string;
|
|
|
21
21
|
/**
|
|
22
22
|
* 물품 집약의 상한 — 큰 현장은 물품이 수만 개라, 전량을 매 브로드캐스팅에 실으면 전달이 현장을 느리게 한다.
|
|
23
23
|
*
|
|
24
|
-
*
|
|
24
|
+
* **알리지 않고 자르지 않는다**: 넘치면 `total`(전체 수)과 `truncated`(잘린 수)를 함께 실어, 소비처가
|
|
25
25
|
* "이게 전부" 라고 오해하지 않게 한다. 개별 물품은 태그별 delta 로 따로 나가고 그쪽은 시그니처 dedup
|
|
26
26
|
* 이 걸려 **변한 것만** 재브로드캐스팅된다.
|
|
27
27
|
*/
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* 트윈에서 사소한 속성은 없다. 소속 구역·소속 팔레트·수량·만료처럼 곁가지로 보이는 값이 실제로는
|
|
8
8
|
* 구역 롤업·적재 표현·로트 추적의 전제다. 그래서 여기서 **필드를 손으로 골라 담지 않는다.**
|
|
9
9
|
*
|
|
10
|
-
* 예전에는 엔티티마다 필요할 것 같은 필드만 열거했고, 그 결과 계약에 있는 것이
|
|
10
|
+
* 예전에는 엔티티마다 필요할 것 같은 필드만 열거했고, 그 결과 계약에 있는 것이 오류 없이 빠졌다
|
|
11
11
|
* (2026-08-01 감사: 노드 parentId 누락 · 작업 전량 누락 · 물품 전량 누락). 화이트리스트 방식은
|
|
12
12
|
* **계약이 자라도 화면이 따라오지 못한다** — 커널이 속성을 늘릴 때마다 이 파일을 고쳐야 하고, 고치는
|
|
13
13
|
* 것을 잊으면 아무 신호 없이 정보가 사라진다.
|
|
@@ -49,7 +49,7 @@ exports.ENERGY_TAG_PREFIX = '__energy__';
|
|
|
49
49
|
/** 이 트윈의 집약 채널 태그. 트윈 식별자가 없으면 만들지 않는다 — 덮어쓰기의 원인이었다. */
|
|
50
50
|
function aggregateTag(prefix, instanceId) {
|
|
51
51
|
if (!instanceId)
|
|
52
|
-
throw new Error('aggregateTag: instanceId required —
|
|
52
|
+
throw new Error('aggregateTag: instanceId required — tags without instanceId overwrite other twin states across tenants');
|
|
53
53
|
return `${prefix}:${instanceId}`;
|
|
54
54
|
}
|
|
55
55
|
const ordersTag = (instanceId) => aggregateTag(exports.ORDERS_TAG_PREFIX, instanceId);
|
|
@@ -123,7 +123,7 @@ function aggregateItem(entity, idKey) {
|
|
|
123
123
|
/**
|
|
124
124
|
* 물품 집약의 상한 — 큰 현장은 물품이 수만 개라, 전량을 매 브로드캐스팅에 실으면 전달이 현장을 느리게 한다.
|
|
125
125
|
*
|
|
126
|
-
*
|
|
126
|
+
* **알리지 않고 자르지 않는다**: 넘치면 `total`(전체 수)과 `truncated`(잘린 수)를 함께 실어, 소비처가
|
|
127
127
|
* "이게 전부" 라고 오해하지 않게 한다. 개별 물품은 태그별 delta 로 따로 나가고 그쪽은 시그니처 dedup
|
|
128
128
|
* 이 걸려 **변한 것만** 재브로드캐스팅된다.
|
|
129
129
|
*/
|
|
@@ -280,7 +280,7 @@ function buildEntityDeltas(state, instanceId, scope) {
|
|
|
280
280
|
...(energy.open ? { open: energy.open } : {}),
|
|
281
281
|
...(energy.peakSince ? { peakSince: energy.peakSince } : {}),
|
|
282
282
|
/* 마감된 구간은 **최근 것만** 낸다 — 하루치를 매 브로드캐스팅에 실으면 채널이 그것으로 찬다.
|
|
283
|
-
전체 수는 함께 낸다(
|
|
283
|
+
전체 수는 함께 낸다(알리지 않고 자르지 않는다). */
|
|
284
284
|
recent: (energy.closed ?? []).slice(-8),
|
|
285
285
|
closedTotal: energy.closedTotal ?? 0
|
|
286
286
|
}
|