@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 @@
|
|
|
1
|
+
{"version":3,"file":"event-type-split-shape.js","sourceRoot":"","sources":["../../../server/service/twin-journal/event-type-split-shape.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * **이 물음이 「여러 종류를 시각순으로」인가** — 순수 판단만 여기 둔다.\n *\n * 데이터베이스를 부르지 않는다. 그래서 시험이 이 파일을 그대로 부를 수 있다(데코레이터가 없다).\n * 판단이 조회 코드 안에 섞여 있으면 그 판단만 확인할 방법이 없고, 그러면 「어떤 물음이 나눠 묻는\n * 갈래로 가는가」가 아무도 읽지 못하는 규칙이 된다.\n *\n * 실측 근거와 고침의 뜻은 `event-type-split.ts` 머리에 있다.\n */\nimport { absoluteTime } from '../../engine/read-time.js'\n\n/** 질의를 몇 개까지 나눌까 — 대상 × 종류. 이 이상이면 나누는 것이 손해다. */\nexport const MAX_SPLIT = 24\n\n/** 종류 목록의 상한 — 이보다 많으면 「이 종류만」이 아니라 사실상 전체다. */\nconst MAX_TYPES = 12\n\nexport interface SplitShape {\n eventTypes: string[]\n /**\n * 공정 단계 하나로 좁힌 경우 — 그 값을 갈래마다 함께 건다.\n *\n * 이 필터를 만나면 갈래를 포기하던 것을 고쳤다(2026-08-29). 원장은 종류를 담아 묻고 그 위에 단계\n * 칩을 겹치는데, 칩을 고른 순간 예전 경로로 떨어져 **고르지 않은 쪽이 더 빨랐다.**\n *\n * 단계 컬럼에는 색인이 없다. 그래도 느려지지 않는다 — 종류 정확일치가 색인으로 시각 순서까지 주고,\n * 단계는 그 안에서 행마다 확인하는 조건이다(걷는 행이 그 종류의 것으로 이미 좁혀져 있다).\n */\n bizStep?: string\n untilTime?: Date\n fromTime?: Date\n}\n\n/**\n * 이 물음이 「여러 종류를 시각순으로」인가 — 아니면 `null`.\n *\n * 받는 모양\n * `eventType` 을 `in` 으로 · 시각 상한이나 구간 · 시각 내림차순 정렬\n *\n * 보내는 모양(일반 경로로)\n * 자유 검색어 · 그 밖의 필터(축·상관 식별자·업무 단계) · 그 밖의 정렬이나 연산자\n */\nexport function splitShape(params: any, sortings: any[]): SplitShape | null {\n const filters: any[] = Array.isArray(params?.filters) ? params.filters : []\n if (typeof params?.search === 'string' && params.search.trim()) return null\n if (sortings.length !== 1 || sortings[0]?.name !== 'eventTime' || sortings[0]?.desc !== true) return null\n\n let eventTypes: string[] | undefined\n let untilTime: Date | undefined\n let fromTime: Date | undefined\n let bizStep: string | undefined\n for (const f of filters) {\n if (f?.name === 'eventType') {\n if (f.operator !== 'in' || !Array.isArray(f.value)) return null\n const types: string[] = f.value.map((v: unknown) => String(v ?? '').trim()).filter((t: string) => !!t)\n /* 하나면 나눌 것이 없다(이미 정확일치라 색인을 탄다). 너무 많으면 사실상 전체다. */\n if (types.length < 2 || types.length > MAX_TYPES) return null\n if (eventTypes) return null\n eventTypes = [...new Set(types)]\n } else if (f?.name === 'bizStep') {\n /* 하나로 좁힌 경우만 받는다 — 여러 값이면 종류 × 단계로 갈래가 곱해진다. */\n if (f.operator !== 'eq') return null\n const v = String(f.value ?? '').trim()\n if (!v) return null\n if (bizStep) return null\n bizStep = v\n } else if (f?.name === 'eventTime') {\n if (f.operator === 'lte' || f.operator === 'lt') {\n untilTime = absoluteTime(f.value) ?? undefined\n if (!untilTime) return null\n } else if (f.operator === 'between' && Array.isArray(f.value)) {\n fromTime = absoluteTime(f.value[0]) ?? undefined\n untilTime = absoluteTime(f.value[1]) ?? undefined\n if (!fromTime || !untilTime) return null\n } else return null\n } else return null\n }\n return eventTypes ? { eventTypes, untilTime, fromTime, bizStep } : null\n}\n\n/**\n * **나눠 물은 갈래들을 합쳐 앞에서 `limit` 개** — 이 규율이 정확성의 근거다.\n *\n * ── 왜 갈래마다 `limit` 까지 받아야 하나 ────────────────────────────────────\n * 갈래마다 `limit` 개를 받아 합치면, 합친 것의 앞 `limit` 개는 **전체에서도** 앞 `limit` 개다.\n * 어떤 갈래에서 빠진 행은 그 갈래의 `limit` 번째보다 뒤이고, 우리는 전체에서 `limit` 개만 취하므로\n * 그 행이 이 페이지에 들어올 수 없다.\n *\n * 갈래마다 **더 적게** 받으면 그 성질이 깨진다. 한 갈래가 앞을 다 차지하는 경우\n * (종류 A 의 마지막이 종류 B 의 첫째보다 앞선다) 그 갈래에서 못 받은 것이 빠지고, 결과는 구간의\n * 앞부분만 계산한 것이 된다 — 그런데 화면은 「일부만 읽었다」고만 말하고 어느 부분인지는 말하지 못한다.\n * (2026-08-28 · UX 레인이 미리 짚어 준 함정이다.)\n *\n * 그래서 이 함수는 **`limit` 미만으로 받은 갈래가 있으면 그것을 말한다** — 부르는 쪽이 그 상태로\n * 답하지 않게.\n */\nexport function mergeStreams<T>(\n streams: readonly T[][],\n limit: number,\n /** 앞에 와야 하는 것이 음수 — `Array.prototype.sort` 와 같은 규약. */\n compare: (a: T, b: T) => number\n): { items: T[]; capped: boolean; underfilled: boolean } {\n const underfilled = streams.some(s => s.length > limit)\n const merged = streams.flat().sort(compare)\n return {\n items: merged.slice(0, limit),\n /* 합친 것이 상한을 넘었으면 더 있었다는 뜻이다 — 잘랐다는 사실을 부르는 쪽이 말해야 한다. */\n capped: merged.length > limit || streams.some(s => s.length >= limit),\n /* 갈래가 상한보다 많이 받아 왔다 — 있을 수 없는 일이므로 부르는 쪽이 알아야 한다. */\n underfilled\n }\n}\n"]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { TwinEvent } from '../twin-event/twin-event.js';
|
|
2
|
+
import { type SplitShape } from './event-type-split-shape.js';
|
|
3
|
+
export interface SplitPage {
|
|
4
|
+
items: TwinEvent[];
|
|
5
|
+
nextCursor?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* 나눠 묻고 합쳐 한 페이지 — 답할 수 없으면 `null`.
|
|
9
|
+
*
|
|
10
|
+
* 이어 읽기 지점은 **모든 갈래에 같이** 건다. 그래야 합친 결과가 커서 아래에서만 나온다 — 갈래마다
|
|
11
|
+
* 다르게 걸면 어떤 종류는 이미 지나간 구간을 다시 낸다.
|
|
12
|
+
*/
|
|
13
|
+
export declare function eventTypeSplitPage(domainId: string, instanceIds: string[], shape: SplitShape, limit: number,
|
|
14
|
+
/** 동시각을 가리는 축 — 트윈 하나면 `revision`, 여럿이면 기본키(§`cursorTiebreak`). */
|
|
15
|
+
tiebreak: 'revision' | 'id', after?: string): Promise<SplitPage | null>;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.eventTypeSplitPage = eventTypeSplitPage;
|
|
4
|
+
const shell_1 = require("@things-factory/shell");
|
|
5
|
+
const twin_event_js_1 = require("../twin-event/twin-event.js");
|
|
6
|
+
const read_time_js_1 = require("../../engine/read-time.js");
|
|
7
|
+
const event_type_split_shape_js_1 = require("./event-type-split-shape.js");
|
|
8
|
+
/**
|
|
9
|
+
* 나눠 묻고 합쳐 한 페이지 — 답할 수 없으면 `null`.
|
|
10
|
+
*
|
|
11
|
+
* 이어 읽기 지점은 **모든 갈래에 같이** 건다. 그래야 합친 결과가 커서 아래에서만 나온다 — 갈래마다
|
|
12
|
+
* 다르게 걸면 어떤 종류는 이미 지나간 구간을 다시 낸다.
|
|
13
|
+
*/
|
|
14
|
+
async function eventTypeSplitPage(domainId, instanceIds, shape, limit,
|
|
15
|
+
/** 동시각을 가리는 축 — 트윈 하나면 `revision`, 여럿이면 기본키(§`cursorTiebreak`). */
|
|
16
|
+
tiebreak, after) {
|
|
17
|
+
const pairs = instanceIds.length * shape.eventTypes.length;
|
|
18
|
+
if (!pairs || pairs > event_type_split_shape_js_1.MAX_SPLIT)
|
|
19
|
+
return null;
|
|
20
|
+
let cursor = null;
|
|
21
|
+
if (after) {
|
|
22
|
+
cursor = decodeCursorPair(after);
|
|
23
|
+
if (!cursor)
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
const rows = [];
|
|
27
|
+
for (const instanceId of instanceIds) {
|
|
28
|
+
for (const eventType of shape.eventTypes) {
|
|
29
|
+
const qb = (0, shell_1.getRepository)(twin_event_js_1.TwinEvent)
|
|
30
|
+
.createQueryBuilder('e')
|
|
31
|
+
.where('e.domain = :domainId', { domainId })
|
|
32
|
+
.andWhere('e.instanceId = :instanceId', { instanceId })
|
|
33
|
+
/* **정확일치**가 요점이다 — 그래야 색인이 시각 순서까지 준다(§ 위 실측). */
|
|
34
|
+
.andWhere('e.eventType = :eventType', { eventType });
|
|
35
|
+
/* 단계는 색인이 아니라 행 조건이다 — 종류 정확일치가 이미 걷는 범위를 좁혀 놓았다. */
|
|
36
|
+
if (shape.bizStep)
|
|
37
|
+
qb.andWhere('e.bizStep = :bizStep', { bizStep: shape.bizStep });
|
|
38
|
+
if (shape.fromTime)
|
|
39
|
+
qb.andWhere('e.eventTime >= :fromTime', { fromTime: shape.fromTime });
|
|
40
|
+
if (shape.untilTime)
|
|
41
|
+
qb.andWhere('e.eventTime <= :untilTime', { untilTime: shape.untilTime });
|
|
42
|
+
if (cursor) {
|
|
43
|
+
qb.andWhere(`(e.eventTime < :curAt OR (e.eventTime = :curAt AND e.${tiebreak} < :curTie))`, {
|
|
44
|
+
curAt: cursor.at,
|
|
45
|
+
curTie: cursor.tie
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
const page = await qb
|
|
49
|
+
.orderBy('e.eventTime', 'DESC')
|
|
50
|
+
.addOrderBy(`e.${tiebreak}`, 'DESC')
|
|
51
|
+
.limit(limit)
|
|
52
|
+
.getMany()
|
|
53
|
+
.catch(() => null);
|
|
54
|
+
/* 한 갈래라도 못 읽으면 합친 결과가 반쪽이다 — 반쪽을 전부인 것처럼 내지 않는다. */
|
|
55
|
+
if (!page)
|
|
56
|
+
return null;
|
|
57
|
+
rows.push(page);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/*
|
|
61
|
+
* 합친다. 갈래마다 `limit` 까지 받았으므로, 합쳐서 앞의 `limit` 개는 **전체에서도** 앞의 `limit` 개다
|
|
62
|
+
* (각 갈래가 자기 안에서 정렬돼 있고 그보다 앞선 것을 빠뜨릴 수 없다).
|
|
63
|
+
*/
|
|
64
|
+
const merged = rows
|
|
65
|
+
.flat()
|
|
66
|
+
.sort((a, b) => {
|
|
67
|
+
/* `getMany` 는 엔티티를 주므로 시각이 이미 날짜다 — 감싸면 드라이버 값을 손으로 읽는 모양이 된다. */
|
|
68
|
+
const at = b.eventTime.getTime() - a.eventTime.getTime();
|
|
69
|
+
if (at !== 0)
|
|
70
|
+
return at;
|
|
71
|
+
const x = a[tiebreak];
|
|
72
|
+
const y = b[tiebreak];
|
|
73
|
+
return x === y ? 0 : x < y ? 1 : -1;
|
|
74
|
+
})
|
|
75
|
+
.slice(0, limit);
|
|
76
|
+
const last = merged[merged.length - 1];
|
|
77
|
+
const nextCursor = merged.length === limit && last?.eventTime instanceof Date && last[tiebreak] !== undefined
|
|
78
|
+
? encodeCursorPair(last.eventTime, last[tiebreak])
|
|
79
|
+
: undefined;
|
|
80
|
+
return { items: merged, nextCursor };
|
|
81
|
+
}
|
|
82
|
+
/** 저널 경로와 같은 커서 — `[eventTime, 동시각 축]`(§`buildNextCursor`). */
|
|
83
|
+
function encodeCursorPair(at, tie) {
|
|
84
|
+
return Buffer.from(JSON.stringify([at, tie]), 'utf8').toString('base64url');
|
|
85
|
+
}
|
|
86
|
+
function decodeCursorPair(cursor) {
|
|
87
|
+
let values;
|
|
88
|
+
try {
|
|
89
|
+
values = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
if (!Array.isArray(values) || values.length !== 2)
|
|
95
|
+
return null;
|
|
96
|
+
const at = (0, read_time_js_1.absoluteTime)(values[0]);
|
|
97
|
+
const tie = values[1];
|
|
98
|
+
if (!at || (typeof tie !== 'string' && typeof tie !== 'number'))
|
|
99
|
+
return null;
|
|
100
|
+
return { at, tie };
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=event-type-split.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"event-type-split.js","sourceRoot":"","sources":["../../../server/service/twin-journal/event-type-split.ts"],"names":[],"mappings":"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","sourcesContent":["import { getRepository } from '@things-factory/shell'\n\nimport { TwinEvent } from '../twin-event/twin-event.js'\nimport { absoluteTime } from '../../engine/read-time.js'\nimport { MAX_SPLIT, type SplitShape } from './event-type-split-shape.js'\n\n/*\n * **여러 종류를 시각순으로 — 종류를 하나씩 나눠 묻는다** (2026-08-28 실측).\n *\n * ── 무엇이 느렸나 ───────────────────────────────────────────────────────────\n * 에너지 원장은 「이 여덟 종류를 시각 내림차순으로 50건」을 묻는다. 그 모양에서 sqlite 는 종류 색인을\n * **버린다**:\n *\n * 종류 IN + 시각순 SEARCH USING INDEX ix_twin_event_1 (domain, instance) 37.181초 · 결과 0건\n * 같은 조건 · 정렬 없음 SEARCH USING INDEX ix_twin_event_7 (…, event_type=?) 0.003초\n * 종류 하나 + 시각순 SEARCH USING INDEX ix_twin_event_7 (…, event_type=?) 0.003초\n *\n * 정렬을 붙이는 순간 여러 종류의 순서를 합칠 방법이 없어, 시각 색인으로 337만 행을 걸으며 행마다\n * 종류를 확인한다. 승화푸드에 에너지 사건은 0건이다 — **없는 것을 찾느라 37초**를 쓴다.\n *\n * 360 이력이 64초였던 것과 같은 부류다: **거르는 축과 정렬 축이 어긋난다.**\n *\n * ── 고침 ────────────────────────────────────────────────────────────────────\n * 종류를 **정확일치로 하나씩** 물으면 색인이 시각 순서까지 그대로 준다(정렬이 사라진다). 여덟 종류면\n * 질의 여덟 개이고 각 0.003초다. 받은 것을 시각순으로 합쳐 한 페이지를 만든다.\n *\n * 새 색인도, 새 테이블도 필요 없다 — 있는 색인을 쓰게 하는 것이 전부다.\n *\n * ── 좁게 잡는다 ─────────────────────────────────────────────────────────────\n * 이 모양이 아닌 물음은 답하지 않고(`null`) 저널의 일반 경로로 보낸다. 억지로 답하면 목록이 알리지\n * 않고 달라진다.\n *\n * ── ★ **이 갈래를 걷을 조건** ────────────────────────────────────────────────\n * 앱에서 종류를 여러 값으로 보내는 곳은 **에너지 원장 한 곳뿐**이다(2026-08-28 확인). 그 화면이\n * 종류를 하나씩 고르는 모양으로 바뀌면 이 갈래를 부르는 데가 없어진다 — 그때 **지운다.**\n *\n * 부르는 데가 없는 갈래를 들고 있으면, 그것이 무언가를 받쳐 주는 것처럼 보이면서 실제로는 아무것도\n * 하지 않는다. 지금 살려 두는 이유는 하나다: 실 고객 트윈에서 37초 질의가 돌고 있고 화면 수정은\n * 다른 레인의 일이다.\n */\n\nexport interface SplitPage {\n items: TwinEvent[]\n nextCursor?: string\n}\n\n/**\n * 나눠 묻고 합쳐 한 페이지 — 답할 수 없으면 `null`.\n *\n * 이어 읽기 지점은 **모든 갈래에 같이** 건다. 그래야 합친 결과가 커서 아래에서만 나온다 — 갈래마다\n * 다르게 걸면 어떤 종류는 이미 지나간 구간을 다시 낸다.\n */\nexport async function eventTypeSplitPage(\n domainId: string,\n instanceIds: string[],\n shape: SplitShape,\n limit: number,\n /** 동시각을 가리는 축 — 트윈 하나면 `revision`, 여럿이면 기본키(§`cursorTiebreak`). */\n tiebreak: 'revision' | 'id',\n after?: string\n): Promise<SplitPage | null> {\n const pairs = instanceIds.length * shape.eventTypes.length\n if (!pairs || pairs > MAX_SPLIT) return null\n\n let cursor: { at: Date; tie: string | number } | null = null\n if (after) {\n cursor = decodeCursorPair(after)\n if (!cursor) return null\n }\n\n const rows: TwinEvent[][] = []\n for (const instanceId of instanceIds) {\n for (const eventType of shape.eventTypes) {\n const qb = getRepository(TwinEvent)\n .createQueryBuilder('e')\n .where('e.domain = :domainId', { domainId })\n .andWhere('e.instanceId = :instanceId', { instanceId })\n /* **정확일치**가 요점이다 — 그래야 색인이 시각 순서까지 준다(§ 위 실측). */\n .andWhere('e.eventType = :eventType', { eventType })\n /* 단계는 색인이 아니라 행 조건이다 — 종류 정확일치가 이미 걷는 범위를 좁혀 놓았다. */\n if (shape.bizStep) qb.andWhere('e.bizStep = :bizStep', { bizStep: shape.bizStep })\n if (shape.fromTime) qb.andWhere('e.eventTime >= :fromTime', { fromTime: shape.fromTime })\n if (shape.untilTime) qb.andWhere('e.eventTime <= :untilTime', { untilTime: shape.untilTime })\n if (cursor) {\n qb.andWhere(`(e.eventTime < :curAt OR (e.eventTime = :curAt AND e.${tiebreak} < :curTie))`, {\n curAt: cursor.at,\n curTie: cursor.tie\n })\n }\n const page = await qb\n .orderBy('e.eventTime', 'DESC')\n .addOrderBy(`e.${tiebreak}`, 'DESC')\n .limit(limit)\n .getMany()\n .catch(() => null)\n /* 한 갈래라도 못 읽으면 합친 결과가 반쪽이다 — 반쪽을 전부인 것처럼 내지 않는다. */\n if (!page) return null\n rows.push(page)\n }\n }\n\n /*\n * 합친다. 갈래마다 `limit` 까지 받았으므로, 합쳐서 앞의 `limit` 개는 **전체에서도** 앞의 `limit` 개다\n * (각 갈래가 자기 안에서 정렬돼 있고 그보다 앞선 것을 빠뜨릴 수 없다).\n */\n const merged = rows\n .flat()\n .sort((a: any, b: any) => {\n /* `getMany` 는 엔티티를 주므로 시각이 이미 날짜다 — 감싸면 드라이버 값을 손으로 읽는 모양이 된다. */\n const at = (b.eventTime as Date).getTime() - (a.eventTime as Date).getTime()\n if (at !== 0) return at\n const x = a[tiebreak]\n const y = b[tiebreak]\n return x === y ? 0 : x < y ? 1 : -1\n })\n .slice(0, limit)\n\n const last: any = merged[merged.length - 1]\n const nextCursor =\n merged.length === limit && last?.eventTime instanceof Date && last[tiebreak] !== undefined\n ? encodeCursorPair(last.eventTime, last[tiebreak])\n : undefined\n\n return { items: merged, nextCursor }\n}\n\n/** 저널 경로와 같은 커서 — `[eventTime, 동시각 축]`(§`buildNextCursor`). */\nfunction encodeCursorPair(at: Date, tie: string | number): string {\n return Buffer.from(JSON.stringify([at, tie]), 'utf8').toString('base64url')\n}\n\nfunction decodeCursorPair(cursor: string): { at: Date; tie: string | number } | null {\n let values: unknown\n try {\n values = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'))\n } catch {\n return null\n }\n if (!Array.isArray(values) || values.length !== 2) return null\n const at = absoluteTime(values[0])\n const tie = values[1]\n if (!at || (typeof tie !== 'string' && typeof tie !== 'number')) return null\n return { at, tie }\n}\n"]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 지난 기록 조회의 정렬 축 — **부르는 쪽이 무엇으로 물었는지에 맞춘다.**
|
|
3
|
+
*
|
|
4
|
+
* 리졸버 안에 두면 이 규칙을 확인할 방법이 소스를 글자로 전수 조회하는 것뿐이다. 규칙 자체를 부를 수 있게
|
|
5
|
+
* 밖으로 뺀다(`test/journal-order-axis.test.ts`).
|
|
6
|
+
*/
|
|
7
|
+
export declare function journalOrder(q: {
|
|
8
|
+
untilTime?: string | null;
|
|
9
|
+
fromTime?: string | null;
|
|
10
|
+
fromRevision?: number | null;
|
|
11
|
+
toRevision?: number | null;
|
|
12
|
+
}): {
|
|
13
|
+
eventTime?: 'DESC';
|
|
14
|
+
revision: 'DESC';
|
|
15
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.journalOrder = journalOrder;
|
|
4
|
+
/**
|
|
5
|
+
* 지난 기록 조회의 정렬 축 — **부르는 쪽이 무엇으로 물었는지에 맞춘다.**
|
|
6
|
+
*
|
|
7
|
+
* 리졸버 안에 두면 이 규칙을 확인할 방법이 소스를 글자로 전수 조회하는 것뿐이다. 규칙 자체를 부를 수 있게
|
|
8
|
+
* 밖으로 뺀다(`test/journal-order-axis.test.ts`).
|
|
9
|
+
*/
|
|
10
|
+
function journalOrder(q) {
|
|
11
|
+
/* `revision` 으로 물었으면 그 순서가 물음의 답이다. 거르는 축과 정렬 축을 맞추는 규칙과도 같다. */
|
|
12
|
+
const byTime = q.untilTime != null || q.fromTime != null;
|
|
13
|
+
if (!byTime && (q.fromRevision != null || q.toRevision != null))
|
|
14
|
+
return { revision: 'DESC' };
|
|
15
|
+
return { eventTime: 'DESC', revision: 'DESC' };
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=journal-order.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"journal-order.js","sourceRoot":"","sources":["../../../server/service/twin-journal/journal-order.ts"],"names":[],"mappings":";;AAMA,oCAaC;AAnBD;;;;;GAKG;AACH,SAAgB,YAAY,CAAC,CAK5B;IAIC,iEAAiE;IACjE,MAAM,MAAM,GAAG,CAAC,CAAC,SAAS,IAAI,IAAI,IAAI,CAAC,CAAC,QAAQ,IAAI,IAAI,CAAA;IACxD,IAAI,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC,YAAY,IAAI,IAAI,IAAI,CAAC,CAAC,UAAU,IAAI,IAAI,CAAC;QAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAA;IAC5F,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAA;AAChD,CAAC","sourcesContent":["/**\n * 지난 기록 조회의 정렬 축 — **부르는 쪽이 무엇으로 물었는지에 맞춘다.**\n *\n * 리졸버 안에 두면 이 규칙을 확인할 방법이 소스를 글자로 전수 조회하는 것뿐이다. 규칙 자체를 부를 수 있게\n * 밖으로 뺀다(`test/journal-order-axis.test.ts`).\n */\nexport function journalOrder(q: {\n untilTime?: string | null\n fromTime?: string | null\n fromRevision?: number | null\n toRevision?: number | null\n}): {\n eventTime?: 'DESC'\n revision: 'DESC'\n} {\n /* `revision` 으로 물었으면 그 순서가 물음의 답이다. 거르는 축과 정렬 축을 맞추는 규칙과도 같다. */\n const byTime = q.untilTime != null || q.fromTime != null\n if (!byTime && (q.fromRevision != null || q.toRevision != null)) return { revision: 'DESC' }\n return { eventTime: 'DESC', revision: 'DESC' }\n}\n"]}
|
|
@@ -2,13 +2,13 @@ import { ListParam } from '@things-factory/shell';
|
|
|
2
2
|
import { TwinEvent } from '../twin-event/twin-event.js';
|
|
3
3
|
import { TwinEventList } from '../twin-event/twin-event-type.js';
|
|
4
4
|
export declare class TwinJournalQuery {
|
|
5
|
-
twinEvents(instanceId: string, fromRevision: number, toRevision: number, eventType: string, limit: number, context: ResolverContext, untilTime?: string): Promise<TwinEvent[]>;
|
|
5
|
+
twinEvents(instanceId: string, fromRevision: number, toRevision: number, eventType: string, limit: number, context: ResolverContext, untilTime?: string, fromTime?: string): Promise<TwinEvent[]>;
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
7
|
+
* 지난 기록 목록 — things-factory 표준 목록 계약(`ListParam` → `{ items, total }`).
|
|
8
8
|
*
|
|
9
9
|
* ── 왜 `twinEvents` 와 따로 두는가 ─────────────────────────────────────────
|
|
10
10
|
* `twinEvents` 는 배열만 돌려준다. 화면은 자기가 받은 게 전부인지 잘린 건지 알 수 없었고, 그래서
|
|
11
|
-
* 리스트가
|
|
11
|
+
* 리스트가 **알리지 않고 잘린 채 "이게 전부" 처럼** 보였다(원장 limit 120 이 대표적). 총건수를 함께
|
|
12
12
|
* 주면 "48 / 12,904" 라고 말할 수 있고, 사용자는 좁혀야 한다는 사실을 안다.
|
|
13
13
|
* 기존 호출자를 깨지 않으려고 `twinEvents` 는 그대로 두고 목록 계약을 새로 연다.
|
|
14
14
|
*
|
|
@@ -21,13 +21,13 @@ export declare class TwinJournalQuery {
|
|
|
21
21
|
*/
|
|
22
22
|
twinEventList(params: ListParam, context: ResolverContext, instanceId?: string, spaceId?: string): Promise<TwinEventList>;
|
|
23
23
|
/**
|
|
24
|
-
* 업무 KPI — 시간창 처리량·소요시간·자원 점유. 저널을
|
|
24
|
+
* 업무 KPI — 시간창 처리량·소요시간·자원 점유. 저널을 **계산해서** 만든다(새 계측을 주입하지 않는다).
|
|
25
25
|
*
|
|
26
26
|
* `twinMetrics`(초당 이벤트 수 = 인프라 부하)와 **다른 것**이다: 이건 "지난 한 시간에 몇 건 처리했고
|
|
27
27
|
* 한 건에 얼마나 걸렸나" 다. 이름이 비슷해 헷갈리기 쉬우므로 설명에 명시한다.
|
|
28
28
|
*
|
|
29
29
|
* 완료된 작업의 **시작 이벤트가 창 앞에 있을 수 있어** 조회 구간을 창보다 앞으로 넓힌다(lookback).
|
|
30
|
-
* 그래도 못 찾은 것은 결과의 `unpaired` 로 드러난다 — 평균에서
|
|
30
|
+
* 그래도 못 찾은 것은 결과의 `unpaired` 로 드러난다 — 평균에서 오류 없이 빠지면 지표가 거짓이 된다.
|
|
31
31
|
*/
|
|
32
32
|
twinKpi(context: ResolverContext, spaceId?: string, instanceId?: string,
|
|
33
33
|
/** 창 시작·끝(ISO). 미지정이면 최근 1시간. */
|
|
@@ -13,6 +13,10 @@ const index_js_1 = require("../../engine/index.js");
|
|
|
13
13
|
const state_axes_js_1 = require("../../engine/state-axes.js");
|
|
14
14
|
const attention_digest_js_1 = require("../../engine/attention-digest.js");
|
|
15
15
|
const kpi_query_js_1 = require("../../engine/kpi-query.js");
|
|
16
|
+
const journal_order_js_1 = require("./journal-order.js");
|
|
17
|
+
const twin_subject_fast_path_js_1 = require("../twin-subject/twin-subject-fast-path.js");
|
|
18
|
+
const event_type_split_js_1 = require("./event-type-split.js");
|
|
19
|
+
const event_type_split_shape_js_1 = require("./event-type-split-shape.js");
|
|
16
20
|
/*
|
|
17
21
|
* 저널 읽기 채널 — 상향 query.
|
|
18
22
|
* twinEvents : append-only 이벤트 저널 조회(Ledger/History/Entity360) — instanceId·revision 범위·eventType 필터.
|
|
@@ -39,13 +43,35 @@ function toDateValue(v) {
|
|
|
39
43
|
return Array.isArray(v) ? v.map(one) : one(v);
|
|
40
44
|
}
|
|
41
45
|
let TwinJournalQuery = class TwinJournalQuery {
|
|
42
|
-
async twinEvents(instanceId, fromRevision, toRevision, eventType, limit, context, untilTime) {
|
|
46
|
+
async twinEvents(instanceId, fromRevision, toRevision, eventType, limit, context, untilTime, fromTime) {
|
|
43
47
|
const where = { domain: { id: context.state.domain.id }, instanceId };
|
|
44
48
|
if (eventType)
|
|
45
49
|
where.eventType = eventType;
|
|
46
|
-
/*
|
|
47
|
-
|
|
50
|
+
/*
|
|
51
|
+
* ── 구간으로 물을 수 있게 한다 (2026-08-25 실측) ────────────────────────────
|
|
52
|
+
*
|
|
53
|
+
* `untilTime` 만 있으면 **끝만 정해진 조회**다. 그러면 위쪽 경계에서 뒤로 걸어오며 종류를 하나씩
|
|
54
|
+
* 확인해야 하고, 찾는 종류가 며칠 전에 멈춘 트윈에서는 그 사이 행을 전부 열어 본다. 실측:
|
|
55
|
+
*
|
|
56
|
+
* 공정 타임라인이 부르는 방식(종류만·200건) 22.0초
|
|
57
|
+
* 같은 조회에 `untilTime` 만 더한 것 9.7초
|
|
58
|
+
*
|
|
59
|
+
* 이유는 계획이 말한다. 종류별 시각순 인덱스가 없어서 `(domain, instanceId, eventTime)` 로 걷고
|
|
60
|
+
* 행마다 테이블을 열어 종류를 본다. 승화푸드에서 작업 사실은 8월 22일에 멈춰 있고 그 뒤로 리비전이
|
|
61
|
+
* 110만 늘었으므로, 200건에 닿기까지 그만큼을 읽고 버린다.
|
|
62
|
+
*
|
|
63
|
+
* **구간을 주면 그 범위만 걷는다**(`event_time>? AND event_time<?`). 화면은 자기가 그리는 구간을
|
|
64
|
+
* 알고 있으므로 그것을 그대로 물으면 된다 — 「전체」가 실제로는 상한에 걸린 6.7시간이었던 문제도
|
|
65
|
+
* 같은 자리에서 없어진다.
|
|
66
|
+
*
|
|
67
|
+
* 컬럼이 날짜라 **날짜로 넘긴다**(문자열로 비교하면 sqlite 에서 오류 없이 빈 결과가 나온다).
|
|
68
|
+
*/
|
|
69
|
+
if (untilTime != null && fromTime != null)
|
|
70
|
+
where.eventTime = (0, typeorm_1.Between)(new Date(fromTime), new Date(untilTime));
|
|
71
|
+
else if (untilTime != null)
|
|
48
72
|
where.eventTime = (0, typeorm_1.LessThanOrEqual)(new Date(untilTime));
|
|
73
|
+
else if (fromTime != null)
|
|
74
|
+
where.eventTime = (0, typeorm_1.MoreThanOrEqual)(new Date(fromTime));
|
|
49
75
|
else if (fromRevision != null && toRevision != null)
|
|
50
76
|
where.revision = (0, typeorm_1.Between)(fromRevision, toRevision);
|
|
51
77
|
else if (fromRevision != null)
|
|
@@ -67,7 +93,7 @@ let TwinJournalQuery = class TwinJournalQuery {
|
|
|
67
93
|
* `... FOR LAST TERM OF ORDER BY` — 시각은 인덱스(`ix_twin_event_1`)에서 순서대로 나오고
|
|
68
94
|
* **동시각만** 임시 정렬한다.
|
|
69
95
|
*
|
|
70
|
-
*
|
|
96
|
+
* **`revision` 경로는 그대로 둔다.** 그때는 거르는 축이 `revision` 이고 정렬도 `revision` 이라 이미
|
|
71
97
|
* 맞아 있다(`ix_twin_event_0`). 축이 어긋난 경로만 고친다 — 맞는 자리를 건드리면 그쪽이 어긋난다.
|
|
72
98
|
*
|
|
73
99
|
* 순서의 뜻이 **완전히 같지는 않다**(측정해서 고친 문장): `revision` 순서와 `eventTime` 순서가
|
|
@@ -75,15 +101,83 @@ let TwinJournalQuery = class TwinJournalQuery {
|
|
|
75
101
|
* 이 목록은 사람이 이력을 훑는 화면이고 **시각순으로 보는 것이 그 화면의 뜻**이므로, 그 어긋남은
|
|
76
102
|
* 결함이 아니라 오히려 맞는 순서다. 동시각의 순서를 위해 `revision` 을 둘째 축으로 남긴다.
|
|
77
103
|
*/
|
|
78
|
-
|
|
79
|
-
|
|
104
|
+
/*
|
|
105
|
+
* ── 아무 필터도 없을 때도 시각순으로 낸다 (2026-08-25 실측) ────────────────
|
|
106
|
+
* 이 갈래가 `revision DESC` 였다. 공정 타임라인은 필터 없이 상한만 걸고 부르므로
|
|
107
|
+
* (`loadTwinEvents(id, { eventType: 'task.status', limit })`) 그 갈래로 떨어졌다.
|
|
108
|
+
*
|
|
109
|
+
* `revision` 은 **적힌 순서**다. 연결된 시스템을 되읽는 트윈에서는 그 순서가 일어난 순서와 다르다 —
|
|
110
|
+
* 커넥터가 원본의 갱신 순서를 걷기 때문에, 원본에서 나중에 손댄 행이 4월 사실을 담고 있으면 그
|
|
111
|
+
* 행이 큰 revision 을 받는다. 승화푸드 실측:
|
|
112
|
+
*
|
|
113
|
+
* 작업 축에 있는 가장 늦은 사실 2026-08-23 03:20
|
|
114
|
+
* revision 이 가장 큰 작업 사실 2026-04-15 10:49 ← 상한이 잡던 값
|
|
115
|
+
*
|
|
116
|
+
* 상한 안에 8월 두 달치가 들어오지 않았고, 화면은 4월 사실을 최근 것으로 그렸다.
|
|
117
|
+
*
|
|
118
|
+
* 아래 목록 계약(`twinEventList`)은 이미 시각순을 기본으로 골라 두었다(§`sortings`). 이 함수만
|
|
119
|
+
* 남아 있었다. 성능도 이유가 되지 않는다 — 시각 정렬이 인덱스를 순서대로 타는 것은 아래 실측이
|
|
120
|
+
* 말한다.
|
|
121
|
+
*
|
|
122
|
+
* `revision` 범위로 거를 때는 `revision DESC` 를 그대로 쓴다. 거르는 축과 정렬 축을 맞추는 규칙이고,
|
|
123
|
+
* 그때는 부르는 쪽이 리비전으로 말하고 있으므로 리비전 순서가 그 물음의 답이다.
|
|
124
|
+
*/
|
|
125
|
+
const order = (0, journal_order_js_1.journalOrder)({ untilTime, fromTime, fromRevision, toRevision });
|
|
126
|
+
/*
|
|
127
|
+
* ── `find` 이 아니라 질의 빌더로 묻는다 (2026-08-25 실측) ───────────────────
|
|
128
|
+
*
|
|
129
|
+
* `find({ where: { domain: { id } }, order, take })` 는 관계를 가진 엔티티에서 **두 단계 질의**가
|
|
130
|
+
* 된다. 실제로 나간 SQL 이 이렇다:
|
|
131
|
+
*
|
|
132
|
+
* SELECT DISTINCT "distinctAlias"."TwinEvent_id", …
|
|
133
|
+
* FROM (SELECT … 모든 컬럼 … FROM twin_events WHERE …) "distinctAlias"
|
|
134
|
+
* ORDER BY … LIMIT ?
|
|
135
|
+
*
|
|
136
|
+
* **안쪽에 상한이 없다.** 조건에 맞는 행을 전부 만들어 놓고 밖에서 자른다. 승화푸드의 작업 사실은
|
|
137
|
+
* 438,913건이고 `payload` 가 함께 실리므로, 200건을 얻는 데 그 전부를 읽었다.
|
|
138
|
+
*
|
|
139
|
+
* 실측: 같은 조회를 손으로 쓰면 0.024초, 이 함수를 통하면 12초였다. 인덱스를 새로 얹은 뒤에도
|
|
140
|
+
* 그대로였다 — 인덱스는 안쪽 질의를 도와줄 뿐이고, 문제는 안쪽이 전부를 만든다는 것이었다.
|
|
141
|
+
*
|
|
142
|
+
* 관계를 **컬럼으로** 물으면 감싸지 않는다. 이 파일의 다른 자리(`timeRange` · 최종 리비전)가 이미
|
|
143
|
+
* 그 방식이다. `getMany` 는 조인이 없으면 `DISTINCT` 를 붙이지 않는다.
|
|
144
|
+
*/
|
|
145
|
+
const qb = (0, shell_1.getRepository)(twin_event_js_1.TwinEvent)
|
|
146
|
+
.createQueryBuilder('e')
|
|
147
|
+
.where('e.domain = :domainId', { domainId: context.state.domain.id })
|
|
148
|
+
.andWhere('e.instanceId = :instanceId', { instanceId });
|
|
149
|
+
if (eventType)
|
|
150
|
+
qb.andWhere('e.eventType = :eventType', { eventType });
|
|
151
|
+
if (where.eventTime !== undefined) {
|
|
152
|
+
/* 조건은 위에서 이미 정했다 — 같은 판단을 두 벌로 적지 않는다. */
|
|
153
|
+
if (untilTime != null && fromTime != null) {
|
|
154
|
+
qb.andWhere('e.eventTime BETWEEN :fromTime AND :untilTime', { fromTime: new Date(fromTime), untilTime: new Date(untilTime) });
|
|
155
|
+
}
|
|
156
|
+
else if (untilTime != null) {
|
|
157
|
+
qb.andWhere('e.eventTime <= :untilTime', { untilTime: new Date(untilTime) });
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
qb.andWhere('e.eventTime >= :fromTime', { fromTime: new Date(fromTime) });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
else if (where.revision !== undefined) {
|
|
164
|
+
if (fromRevision != null && toRevision != null)
|
|
165
|
+
qb.andWhere('e.revision BETWEEN :from AND :to', { from: fromRevision, to: toRevision });
|
|
166
|
+
else if (fromRevision != null)
|
|
167
|
+
qb.andWhere('e.revision >= :from', { from: fromRevision });
|
|
168
|
+
else
|
|
169
|
+
qb.andWhere('e.revision <= :to', { to: toRevision });
|
|
170
|
+
}
|
|
171
|
+
for (const [column, direction] of Object.entries(order))
|
|
172
|
+
qb.addOrderBy(`e.${column}`, direction);
|
|
173
|
+
return qb.limit(limit ?? 200).getMany();
|
|
80
174
|
}
|
|
81
175
|
/**
|
|
82
|
-
*
|
|
176
|
+
* 지난 기록 목록 — things-factory 표준 목록 계약(`ListParam` → `{ items, total }`).
|
|
83
177
|
*
|
|
84
178
|
* ── 왜 `twinEvents` 와 따로 두는가 ─────────────────────────────────────────
|
|
85
179
|
* `twinEvents` 는 배열만 돌려준다. 화면은 자기가 받은 게 전부인지 잘린 건지 알 수 없었고, 그래서
|
|
86
|
-
* 리스트가
|
|
180
|
+
* 리스트가 **알리지 않고 잘린 채 "이게 전부" 처럼** 보였다(원장 limit 120 이 대표적). 총건수를 함께
|
|
87
181
|
* 주면 "48 / 12,904" 라고 말할 수 있고, 사용자는 좁혀야 한다는 사실을 안다.
|
|
88
182
|
* 기존 호출자를 깨지 않으려고 `twinEvents` 는 그대로 두고 목록 계약을 새로 연다.
|
|
89
183
|
*
|
|
@@ -99,7 +193,7 @@ let TwinJournalQuery = class TwinJournalQuery {
|
|
|
99
193
|
if (!instanceId && !spaceId)
|
|
100
194
|
throw new Error('either instanceId or spaceId is required');
|
|
101
195
|
/* 테넌트 격리 — twinKpi 와 같은 규약. 남의 트윈은 빈 결과가 아니라 명시 실패로 알린다
|
|
102
|
-
* (
|
|
196
|
+
* (들어온 것이 없는 빈 목록은 "권한 없음" 과 "데이터 없음" 을 구분할 수 없게 만든다). */
|
|
103
197
|
if (instanceId && !(await index_js_1.TwinEngine.ownsRegistered(domain.id, instanceId))) {
|
|
104
198
|
throw new Error(`twin instance not found in this tenant: ${instanceId}`);
|
|
105
199
|
}
|
|
@@ -111,7 +205,16 @@ let TwinJournalQuery = class TwinJournalQuery {
|
|
|
111
205
|
const limit = Math.min(params.pagination?.limit ?? 100, 500);
|
|
112
206
|
const effective = {
|
|
113
207
|
...params,
|
|
114
|
-
|
|
208
|
+
/*
|
|
209
|
+
* ── 이어 읽기 지점을 **잃지 않는다** (2026-08-28 실측) ─────────────────────
|
|
210
|
+
*
|
|
211
|
+
* 여기서 페이지 값을 다시 만들면서 `after` 를 빼고 있었다. 그래서 응답은 다음 지점(`nextCursor`)을
|
|
212
|
+
* 내주는데 다음 요청의 그 값은 아무 데도 쓰이지 않았고, 「더 보기」가 **1페이지를 다시 냈다.**
|
|
213
|
+
* 실측: 같은 품목의 30건을 세 번 이어 읽었더니 90건 중 60건이 되풀었다(같은 id).
|
|
214
|
+
*
|
|
215
|
+
* 오류가 나지 않는 결함이라 화면에서는 「이력이 30건뿐」이나 「같은 사건이 계속 나온다」로 보인다.
|
|
216
|
+
*/
|
|
217
|
+
pagination: { page: params.pagination?.page ?? 1, limit, after: params.pagination?.after },
|
|
115
218
|
/* 기본 정렬은 시각 내림차순 — 여러 트윈을 합칠 때 공통 축은 revision(트윈별 카운터)이 아니라
|
|
116
219
|
* **시각**이다. revision 으로 섞으면 서로 다른 트윈의 무관한 카운터가 뒤엉킨다. */
|
|
117
220
|
sortings: params.sortings?.length ? params.sortings : [{ name: 'eventTime', desc: true }],
|
|
@@ -125,6 +228,69 @@ let TwinJournalQuery = class TwinJournalQuery {
|
|
|
125
228
|
*/
|
|
126
229
|
filters: (params.filters ?? []).map(f => (f.name === 'eventTime' ? { ...f, value: toDateValue(f.value) } : f))
|
|
127
230
|
};
|
|
231
|
+
/*
|
|
232
|
+
* ── 대상 하나의 이력은 **파생 테이블**이 답한다 (2026-08-28 실측) ───────────
|
|
233
|
+
*
|
|
234
|
+
* 360 화면의 이력이 이 문으로 온다. 물음은 「이 품목(오더·자리·설비)의 사건을 최신순으로 30건」
|
|
235
|
+
* 인데, 저널에는 그 모양의 목차가 없다: 축으로 걸러도 시각으로 정렬하는 순간 목차를 못 쓰고,
|
|
236
|
+
* 품목과 로트가 한 문자열에 붙어 있어 품목으로는 아예 찾지 못한다. 실측 **63.998초**였다.
|
|
237
|
+
*
|
|
238
|
+
* 그 물음의 모양대로 만든 테이블이 이미 있다(`twin_subject_events`). 같은 질의가 **0.078초**다.
|
|
239
|
+
* 화면을 고치지 않고 여기서 답을 가져오는 곳만 바꾼다 — 계약도 커서도 그대로다.
|
|
240
|
+
*
|
|
241
|
+
* 맞지 않는 물음(여러 트윈·자유 검색·종류 필터·다른 정렬)이거나 파생 테이블이 그 구간을 말할 수
|
|
242
|
+
* 없으면 `null` 이 오고, 그때는 아래 저널 경로로 간다. 느리지만 온전하다.
|
|
243
|
+
*/
|
|
244
|
+
if (instanceIds.length === 1) {
|
|
245
|
+
const fast = await (0, twin_subject_fast_path_js_1.readModelPage)(domain.id, instanceIds[0], effective, limit);
|
|
246
|
+
if (fast)
|
|
247
|
+
return fast;
|
|
248
|
+
}
|
|
249
|
+
/*
|
|
250
|
+
* ── **여러 종류를 시각순으로 물으면 나눠 묻는다** (2026-08-28 실측) ──────────
|
|
251
|
+
*
|
|
252
|
+
* 에너지 원장이 「이 여덟 종류를 시각 내림차순으로」를 묻는다. 그 모양에서 sqlite 는 종류 색인을
|
|
253
|
+
* 버리고 시각 색인으로 337만 행을 걸으며 행마다 종류를 확인한다 — **37.181초, 결과 0건**.
|
|
254
|
+
* 종류를 정확일치로 하나씩 물으면 색인이 시각 순서까지 주므로 **각 0.003초**다.
|
|
255
|
+
*
|
|
256
|
+
* 새 색인도 새 테이블도 필요 없다. 있는 색인(`ix_twin_event_7`)을 쓰게 하는 것이 전부다.
|
|
257
|
+
* 건수 세기는 이미 빠르다(정렬을 지우므로 같은 색인을 탄다) — 목록만 이 갈래를 탄다.
|
|
258
|
+
*/
|
|
259
|
+
const tiebreak = instanceIds.length === 1 ? 'revision' : 'id';
|
|
260
|
+
const split = (0, event_type_split_shape_js_1.splitShape)(effective, effective.sortings ?? []);
|
|
261
|
+
if (split) {
|
|
262
|
+
const page = await (0, event_type_split_js_1.eventTypeSplitPage)(domain.id, instanceIds, split, limit, tiebreak, effective.pagination?.after);
|
|
263
|
+
if (page) {
|
|
264
|
+
/*
|
|
265
|
+
* 건수는 일반 경로와 같은 방식으로 센다 — 상한까지 키만 읽는다. 이 조건은 정렬이 없어 종류
|
|
266
|
+
* 색인을 그대로 타므로 값이 싸다(실측 0.003초).
|
|
267
|
+
*/
|
|
268
|
+
const probe = (0, shell_1.getQueryBuilderFromListParams)({
|
|
269
|
+
repository: (0, shell_1.getRepository)(twin_event_js_1.TwinEvent),
|
|
270
|
+
params: { ...effective, pagination: { page: 1, limit: COUNT_CAP + 1 } },
|
|
271
|
+
domain,
|
|
272
|
+
searchables: [],
|
|
273
|
+
sortables: ['eventTime', 'revision', 'eventType', 'bizStep'],
|
|
274
|
+
defaultLimit: COUNT_CAP + 1,
|
|
275
|
+
maxLimit: COUNT_CAP + 1
|
|
276
|
+
});
|
|
277
|
+
probe.andWhere(`${probe.alias}.instanceId IN (:...instanceIds)`, { instanceIds });
|
|
278
|
+
const counted = await probe
|
|
279
|
+
.orderBy()
|
|
280
|
+
.select(`${probe.alias}.id`)
|
|
281
|
+
.getRawMany()
|
|
282
|
+
.catch(() => null);
|
|
283
|
+
const capped = !counted || counted.length > COUNT_CAP;
|
|
284
|
+
return {
|
|
285
|
+
items: page.items,
|
|
286
|
+
total: capped ? undefined : counted.length,
|
|
287
|
+
capped,
|
|
288
|
+
countCap: COUNT_CAP,
|
|
289
|
+
nextCursor: page.nextCursor,
|
|
290
|
+
oldestRevision: undefined
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
}
|
|
128
294
|
const qb = (0, shell_1.getQueryBuilderFromListParams)({
|
|
129
295
|
repository: (0, shell_1.getRepository)(twin_event_js_1.TwinEvent),
|
|
130
296
|
params: effective,
|
|
@@ -134,7 +300,22 @@ let TwinJournalQuery = class TwinJournalQuery {
|
|
|
134
300
|
/* 정렬은 인덱스가 받쳐 주는 축으로만 — 큰 저널에서 임의 컬럼 정렬은 전체 정렬 스캔이다. */
|
|
135
301
|
sortables: ['eventTime', 'revision', 'eventType', 'bizStep'],
|
|
136
302
|
defaultLimit: 100,
|
|
137
|
-
maxLimit: 500
|
|
303
|
+
maxLimit: 500,
|
|
304
|
+
/*
|
|
305
|
+
* ── 동시각을 가리는 축을 **두 경로가 같게** 쓴다 (2026-08-28 실측) ─────────
|
|
306
|
+
*
|
|
307
|
+
* 커서는 「이 행 다음부터」이므로 시각이 같은 행들 사이에서 무엇이 앞인지 정해야 한다. 기본값은
|
|
308
|
+
* 기본키(`id`)인데, 파생 테이블 경로는 `revision` 으로 가린다(그 테이블에 `id` 가 없다).
|
|
309
|
+
*
|
|
310
|
+
* 두 경로가 한 목록을 나눠 답하면 그 축이 달라지고, 시각이 같은 행이 많은 자리에서 **본 행이
|
|
311
|
+
* 또 나온다.** 실측: 재고 사건 2,665건이 같은 시각을 가진 트윈에서 30건씩 네 번 이어 읽어
|
|
312
|
+
* 120건 중 15건이 되풀었다.
|
|
313
|
+
*
|
|
314
|
+
* 그래서 트윈 하나를 볼 때는 `revision` 으로 가린다 — 그 트윈 안에서 유일하고 빈 값이 없다.
|
|
315
|
+
* 여러 트윈을 합쳐 보는 목록에서는 리비전이 트윈마다 따로 세는 번호라 유일하지 않으므로
|
|
316
|
+
* 기본키를 쓴다.
|
|
317
|
+
*/
|
|
318
|
+
cursorTiebreak: tiebreak
|
|
138
319
|
});
|
|
139
320
|
/* 대상 범위는 호출자가 필터로 빼먹을 수 있는 값이 아니다 — 계약상 필수라 여기서 강제한다. */
|
|
140
321
|
qb.andWhere(`${qb.alias}.instanceId IN (:...instanceIds)`, { instanceIds });
|
|
@@ -146,7 +327,7 @@ let TwinJournalQuery = class TwinJournalQuery {
|
|
|
146
327
|
*
|
|
147
328
|
* 상한을 두면 같은 답이 **0.04초**다(130배). 상한을 넘으면 정확한 수를 모르는 것이 사실이므로
|
|
148
329
|
* 목록 규약의 「서버 상한에 부딪힘(total 모름)」 갈래로 낸다 — 화면은 「처음 N 건만」이라고 말하고
|
|
149
|
-
* 좁히기를 권한다.
|
|
330
|
+
* 좁히기를 권한다. **알리지 않고 자르지 않는다**: 상한을 넘었다는 사실을 `capped` 로 함께 낸다.
|
|
150
331
|
*
|
|
151
332
|
* 왜 하위 질의로 세지 않나: 5개 드라이버에서 같이 도는 형태여야 한다(이 저장소의 절대 규칙).
|
|
152
333
|
* 그래서 **키 컬럼만 상한까지 읽어 세는** 방식을 쓴다 — 색인만 읽으므로 행 본문을 만들지 않는다.
|
|
@@ -163,22 +344,50 @@ let TwinJournalQuery = class TwinJournalQuery {
|
|
|
163
344
|
/* 상한을 넘었으면 **전체 수를 모른다** — 상한값을 전체로 적으면 화면이 거짓을 말한다. */
|
|
164
345
|
const total = capped ? undefined : counted.length;
|
|
165
346
|
/* 다음 커서는 **이번 페이지의 마지막 행**에서 만든다. 페이지가 상한보다 짧으면 뒤가 없다. */
|
|
166
|
-
const axes = (0, shell_1.cursorSortings)(effective.sortings,
|
|
347
|
+
const axes = (0, shell_1.cursorSortings)(effective.sortings, tiebreak);
|
|
167
348
|
const nextCursor = items.length === limit ? (0, shell_1.buildNextCursor)(items[items.length - 1], axes) : undefined;
|
|
168
|
-
|
|
349
|
+
/*
|
|
350
|
+
* ── **앞부분이 지워졌는지 함께 낸다** (2026-08-27) ─────────────────────────
|
|
351
|
+
*
|
|
352
|
+
* 보관 기간 정리가 오래된 사건을 지운다(기본 7일). 그러면 이 목록을 아무리 내려가도 어느 지점에서
|
|
353
|
+
* 기록이 끝나는데, 화면이 그것을 말하지 않으면 사람은 「이 트윈은 그때 시작했다」로 읽는다.
|
|
354
|
+
* 없었던 것과 지워진 것은 다른 사실이다.
|
|
355
|
+
*
|
|
356
|
+
* 저장하지 않고 그때그때 구한다 — 저장한 값은 실제 데이터와 어긋날 수 있고, 어긋나면 어느 쪽이
|
|
357
|
+
* 사실인지 알 수 없다. 색인 `(domain, instanceId, revision)` 의 첫 행 하나라서 값이 싸다.
|
|
358
|
+
*
|
|
359
|
+
* 트윈 하나를 물었을 때만 낸다. 여러 트윈을 합쳐 보는 목록에서는 리비전이 트윈마다 따로 세는
|
|
360
|
+
* 번호라 하나의 답이 없다.
|
|
361
|
+
*/
|
|
362
|
+
let oldestRevision;
|
|
363
|
+
if (instanceIds.length === 1) {
|
|
364
|
+
const first = await (0, shell_1.getRepository)(twin_event_js_1.TwinEvent)
|
|
365
|
+
.createQueryBuilder('e')
|
|
366
|
+
.select('e.revision', 'revision')
|
|
367
|
+
.where('e.domain = :domainId', { domainId: domain.id })
|
|
368
|
+
.andWhere('e.instanceId = :instanceId', { instanceId: instanceIds[0] })
|
|
369
|
+
.orderBy('e.revision', 'ASC')
|
|
370
|
+
.limit(1)
|
|
371
|
+
.getRawOne()
|
|
372
|
+
.catch(() => null);
|
|
373
|
+
const at = Number(first?.revision);
|
|
374
|
+
if (Number.isFinite(at) && at > 0)
|
|
375
|
+
oldestRevision = at;
|
|
376
|
+
}
|
|
377
|
+
return { items, total, capped, countCap: COUNT_CAP, nextCursor, oldestRevision };
|
|
169
378
|
}
|
|
170
379
|
/**
|
|
171
|
-
* 업무 KPI — 시간창 처리량·소요시간·자원 점유. 저널을
|
|
380
|
+
* 업무 KPI — 시간창 처리량·소요시간·자원 점유. 저널을 **계산해서** 만든다(새 계측을 주입하지 않는다).
|
|
172
381
|
*
|
|
173
382
|
* `twinMetrics`(초당 이벤트 수 = 인프라 부하)와 **다른 것**이다: 이건 "지난 한 시간에 몇 건 처리했고
|
|
174
383
|
* 한 건에 얼마나 걸렸나" 다. 이름이 비슷해 헷갈리기 쉬우므로 설명에 명시한다.
|
|
175
384
|
*
|
|
176
385
|
* 완료된 작업의 **시작 이벤트가 창 앞에 있을 수 있어** 조회 구간을 창보다 앞으로 넓힌다(lookback).
|
|
177
|
-
* 그래도 못 찾은 것은 결과의 `unpaired` 로 드러난다 — 평균에서
|
|
386
|
+
* 그래도 못 찾은 것은 결과의 `unpaired` 로 드러난다 — 평균에서 오류 없이 빠지면 지표가 거짓이 된다.
|
|
178
387
|
*/
|
|
179
388
|
async twinKpi(context, spaceId, instanceId, fromTime, toTime, windowMinutes, buckets, lookbackMinutes, groupBy, groupLimit, compareTo, baselinePeriods, baselineStride) {
|
|
180
389
|
/* 테넌트 격리 — 트윈 지정이면 소속을 확인한다. 공간 지정이면 대상 해소가 도메인 목록에서
|
|
181
|
-
* 이뤄지므로(computeTwinKpi) 남의 트윈이 섞이지 않는다.
|
|
390
|
+
* 이뤄지므로(computeTwinKpi) 남의 트윈이 섞이지 않는다. 들어온 것이 없는 빈 결과가 아니라 명시 실패. */
|
|
182
391
|
if (instanceId && !(await index_js_1.TwinEngine.ownsRegistered(context.state.domain.id, instanceId))) {
|
|
183
392
|
throw new Error(`twin instance not found in this tenant: ${instanceId}`);
|
|
184
393
|
}
|
|
@@ -206,7 +415,7 @@ let TwinJournalQuery = class TwinJournalQuery {
|
|
|
206
415
|
async twinReplay(instanceId, untilRevision, untilTime, context, axes, attentionLimit) {
|
|
207
416
|
const state = await index_js_1.TwinEngine.recover(context.state.domain.id, instanceId, untilRevision ?? undefined, untilTime ?? undefined);
|
|
208
417
|
const picked = (0, state_axes_js_1.pickAxes)(state, axes);
|
|
209
|
-
/* 오타를
|
|
418
|
+
/* 오타를 알리지 않고 넘기지 않는다 — 화면이 빈 채로 뜨는 이유를 아무도 모르게 되는 것이 가장 나쁘다. */
|
|
210
419
|
if (picked.unknown.length)
|
|
211
420
|
(0, log_js_1.twinWarn)(`[twin] twinReplay: unknown axes ignored for "${instanceId}": ${picked.unknown.join(', ')}`);
|
|
212
421
|
if (!attentionLimit || !picked.state?.attentions)
|
|
@@ -225,7 +434,7 @@ let TwinJournalQuery = class TwinJournalQuery {
|
|
|
225
434
|
throw new Error('unitsPerDay must be greater than zero');
|
|
226
435
|
/*
|
|
227
436
|
* 기준 주를 안 주면 **이번 주 월요일**을 쓴다. 그 주에 공휴일이 끼면 상한이 조금 낮게 나오는데,
|
|
228
|
-
* 그것은
|
|
437
|
+
* 그것은 들어온 것이 없는 오류가 아니라 **이번 주의 실제 가용 시간**이라 답으로서 정직하다. 평상시 상한을
|
|
229
438
|
* 보려는 호출자는 평상주를 지정한다.
|
|
230
439
|
*/
|
|
231
440
|
const base = sampleWeekStart ? Date.parse(sampleWeekStart) : Date.now();
|
|
@@ -272,7 +481,8 @@ let TwinJournalQuery = class TwinJournalQuery {
|
|
|
272
481
|
exports.TwinJournalQuery = TwinJournalQuery;
|
|
273
482
|
tslib_1.__decorate([
|
|
274
483
|
(0, type_graphql_1.Query)(returns => [twin_event_js_1.TwinEvent], {
|
|
275
|
-
description: 'Read the append-only twin event journal (Ledger / History / audit)
|
|
484
|
+
description: 'Read the append-only twin event journal (Ledger / History / audit). Newest first by event time; ' +
|
|
485
|
+
'ordered by revision only when a revision range is given.'
|
|
276
486
|
}),
|
|
277
487
|
tslib_1.__param(0, (0, type_graphql_1.Arg)('instanceId')),
|
|
278
488
|
tslib_1.__param(1, (0, type_graphql_1.Arg)('fromRevision', type => type_graphql_1.Int, { nullable: true })),
|
|
@@ -281,8 +491,9 @@ tslib_1.__decorate([
|
|
|
281
491
|
tslib_1.__param(4, (0, type_graphql_1.Arg)('limit', type => type_graphql_1.Int, { nullable: true })),
|
|
282
492
|
tslib_1.__param(5, (0, type_graphql_1.Ctx)()),
|
|
283
493
|
tslib_1.__param(6, (0, type_graphql_1.Arg)('untilTime', { nullable: true })),
|
|
494
|
+
tslib_1.__param(7, (0, type_graphql_1.Arg)('fromTime', { nullable: true })),
|
|
284
495
|
tslib_1.__metadata("design:type", Function),
|
|
285
|
-
tslib_1.__metadata("design:paramtypes", [String, Number, Number, String, Number, Object, String]),
|
|
496
|
+
tslib_1.__metadata("design:paramtypes", [String, Number, Number, String, Number, Object, String, String]),
|
|
286
497
|
tslib_1.__metadata("design:returntype", Promise)
|
|
287
498
|
], TwinJournalQuery.prototype, "twinEvents", null);
|
|
288
499
|
tslib_1.__decorate([
|