@things-factory/headless-twin 10.0.19 → 10.1.0

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.
Files changed (327) hide show
  1. package/dist-server/engine/canonical-ingest.d.ts +10 -1
  2. package/dist-server/engine/canonical-ingest.js +110 -33
  3. package/dist-server/engine/canonical-ingest.js.map +1 -1
  4. package/dist-server/engine/event-time.d.ts +25 -0
  5. package/dist-server/engine/event-time.js +98 -0
  6. package/dist-server/engine/event-time.js.map +1 -0
  7. package/dist-server/engine/index.d.ts +2 -0
  8. package/dist-server/engine/index.js +23 -0
  9. package/dist-server/engine/index.js.map +1 -1
  10. package/dist-server/engine/ingest-dedupe.d.ts +18 -6
  11. package/dist-server/engine/ingest-dedupe.js +201 -15
  12. package/dist-server/engine/ingest-dedupe.js.map +1 -1
  13. package/dist-server/engine/ingest-health.d.ts +60 -1
  14. package/dist-server/engine/ingest-health.js +43 -8
  15. package/dist-server/engine/ingest-health.js.map +1 -1
  16. package/dist-server/engine/kpi-fold.d.ts +19 -0
  17. package/dist-server/engine/kpi-fold.js +4 -1
  18. package/dist-server/engine/kpi-fold.js.map +1 -1
  19. package/dist-server/engine/kpi-query.js +23 -3
  20. package/dist-server/engine/kpi-query.js.map +1 -1
  21. package/dist-server/engine/oee-accumulator.d.ts +35 -2
  22. package/dist-server/engine/oee-accumulator.js +56 -4
  23. package/dist-server/engine/oee-accumulator.js.map +1 -1
  24. package/dist-server/engine/twin-engine.d.ts +72 -3
  25. package/dist-server/engine/twin-engine.js +118 -5
  26. package/dist-server/engine/twin-engine.js.map +1 -1
  27. package/dist-server/index.js +21 -0
  28. package/dist-server/index.js.map +1 -1
  29. package/dist-server/migrations/1786600000000-RenameOperatoMesAdapterType.d.ts +5 -0
  30. package/dist-server/migrations/1786600000000-RenameOperatoMesAdapterType.js +66 -0
  31. package/dist-server/migrations/1786600000000-RenameOperatoMesAdapterType.js.map +1 -0
  32. package/dist-server/migrations/1786700000000-AddDispatchRetryColumns.d.ts +5 -0
  33. package/dist-server/migrations/1786700000000-AddDispatchRetryColumns.js +79 -0
  34. package/dist-server/migrations/1786700000000-AddDispatchRetryColumns.js.map +1 -0
  35. package/dist-server/migrations/index.js +5 -1
  36. package/dist-server/migrations/index.js.map +1 -1
  37. package/dist-server/routes.js +12 -20
  38. package/dist-server/routes.js.map +1 -1
  39. package/dist-server/service/actuation/actuation-advisor.d.ts +60 -0
  40. package/dist-server/service/actuation/actuation-advisor.js +53 -0
  41. package/dist-server/service/actuation/actuation-advisor.js.map +1 -0
  42. package/dist-server/service/actuation/actuation-outcome.d.ts +52 -0
  43. package/dist-server/service/actuation/actuation-outcome.js +34 -0
  44. package/dist-server/service/actuation/actuation-outcome.js.map +1 -0
  45. package/dist-server/service/actuation/actuation-rule-resolver.d.ts +19 -0
  46. package/dist-server/service/actuation/actuation-rule-resolver.js +161 -0
  47. package/dist-server/service/actuation/actuation-rule-resolver.js.map +1 -0
  48. package/dist-server/service/actuation/actuation-rule.d.ts +55 -0
  49. package/dist-server/service/actuation/actuation-rule.js +130 -0
  50. package/dist-server/service/actuation/actuation-rule.js.map +1 -0
  51. package/dist-server/service/actuation/approval-gateway.d.ts +20 -0
  52. package/dist-server/service/actuation/approval-gateway.js +14 -0
  53. package/dist-server/service/actuation/approval-gateway.js.map +1 -0
  54. package/dist-server/service/actuation/catch-up-effects.d.ts +16 -0
  55. package/dist-server/service/actuation/catch-up-effects.js +120 -0
  56. package/dist-server/service/actuation/catch-up-effects.js.map +1 -0
  57. package/dist-server/service/actuation/command-dispatcher.d.ts +206 -8
  58. package/dist-server/service/actuation/command-dispatcher.js +191 -14
  59. package/dist-server/service/actuation/command-dispatcher.js.map +1 -1
  60. package/dist-server/service/actuation/command-store.d.ts +10 -2
  61. package/dist-server/service/actuation/command-store.js +168 -5
  62. package/dist-server/service/actuation/command-store.js.map +1 -1
  63. package/dist-server/service/actuation/dispatch-after-approval.d.ts +11 -0
  64. package/dist-server/service/actuation/dispatch-after-approval.js +73 -0
  65. package/dist-server/service/actuation/dispatch-after-approval.js.map +1 -0
  66. package/dist-server/service/actuation/effect-match.d.ts +53 -0
  67. package/dist-server/service/actuation/effect-match.js +65 -0
  68. package/dist-server/service/actuation/effect-match.js.map +1 -0
  69. package/dist-server/service/actuation/index.d.ts +21 -2
  70. package/dist-server/service/actuation/index.js +21 -2
  71. package/dist-server/service/actuation/index.js.map +1 -1
  72. package/dist-server/service/actuation/retry-plan.d.ts +63 -0
  73. package/dist-server/service/actuation/retry-plan.js +99 -0
  74. package/dist-server/service/actuation/retry-plan.js.map +1 -0
  75. package/dist-server/service/actuation/rule-evaluate.d.ts +127 -0
  76. package/dist-server/service/actuation/rule-evaluate.js +205 -0
  77. package/dist-server/service/actuation/rule-evaluate.js.map +1 -0
  78. package/dist-server/service/actuation/rule-loop.d.ts +5 -0
  79. package/dist-server/service/actuation/rule-loop.js +156 -0
  80. package/dist-server/service/actuation/rule-loop.js.map +1 -0
  81. package/dist-server/service/actuation/rule-runner.d.ts +52 -0
  82. package/dist-server/service/actuation/rule-runner.js +135 -0
  83. package/dist-server/service/actuation/rule-runner.js.map +1 -0
  84. package/dist-server/service/actuation/rule-wiring.d.ts +24 -0
  85. package/dist-server/service/actuation/rule-wiring.js +80 -0
  86. package/dist-server/service/actuation/rule-wiring.js.map +1 -0
  87. package/dist-server/service/actuation/settle-effects.d.ts +21 -0
  88. package/dist-server/service/actuation/settle-effects.js +101 -0
  89. package/dist-server/service/actuation/settle-effects.js.map +1 -0
  90. package/dist-server/service/actuation/sweep-dispatch.d.ts +37 -0
  91. package/dist-server/service/actuation/sweep-dispatch.js +125 -0
  92. package/dist-server/service/actuation/sweep-dispatch.js.map +1 -0
  93. package/dist-server/service/actuation/twin-command-resolver.d.ts +120 -0
  94. package/dist-server/service/actuation/twin-command-resolver.js +406 -0
  95. package/dist-server/service/actuation/twin-command-resolver.js.map +1 -0
  96. package/dist-server/service/actuation/twin-command.d.ts +81 -0
  97. package/dist-server/service/actuation/twin-command.js +60 -0
  98. package/dist-server/service/actuation/twin-command.js.map +1 -1
  99. package/dist-server/service/actuation/twin-now.d.ts +41 -0
  100. package/dist-server/service/actuation/twin-now.js +65 -0
  101. package/dist-server/service/actuation/twin-now.js.map +1 -0
  102. package/dist-server/service/index.d.ts +5 -2
  103. package/dist-server/service/index.js +8 -0
  104. package/dist-server/service/index.js.map +1 -1
  105. package/dist-server/service/reference/actuation-routing.d.ts +60 -0
  106. package/dist-server/service/reference/actuation-routing.js +126 -0
  107. package/dist-server/service/reference/actuation-routing.js.map +1 -0
  108. package/dist-server/service/reference/actuation-target.d.ts +65 -0
  109. package/dist-server/service/reference/actuation-target.js +94 -0
  110. package/dist-server/service/reference/actuation-target.js.map +1 -0
  111. package/dist-server/service/reference/connection-portability-resolver.d.ts +29 -0
  112. package/dist-server/service/reference/connection-portability-resolver.js +181 -0
  113. package/dist-server/service/reference/connection-portability-resolver.js.map +1 -0
  114. package/dist-server/service/reference/connection-portability.d.ts +138 -0
  115. package/dist-server/service/reference/connection-portability.js +230 -0
  116. package/dist-server/service/reference/connection-portability.js.map +1 -0
  117. package/dist-server/service/reference/fill-cadence.d.ts +17 -0
  118. package/dist-server/service/reference/fill-cadence.js +31 -0
  119. package/dist-server/service/reference/fill-cadence.js.map +1 -0
  120. package/dist-server/service/reference/fill-dedupe.d.ts +16 -0
  121. package/dist-server/service/reference/fill-dedupe.js +63 -0
  122. package/dist-server/service/reference/fill-dedupe.js.map +1 -0
  123. package/dist-server/service/reference/fill-loop.d.ts +35 -0
  124. package/dist-server/service/reference/fill-loop.js +187 -0
  125. package/dist-server/service/reference/fill-loop.js.map +1 -0
  126. package/dist-server/service/reference/fill-span.d.ts +10 -0
  127. package/dist-server/service/reference/fill-span.js +37 -0
  128. package/dist-server/service/reference/fill-span.js.map +1 -0
  129. package/dist-server/service/reference/hook-contract.d.ts +88 -4
  130. package/dist-server/service/reference/hook-contract.js +105 -4
  131. package/dist-server/service/reference/hook-contract.js.map +1 -1
  132. package/dist-server/service/reference/hook-rejected.d.ts +25 -0
  133. package/dist-server/service/reference/hook-rejected.js +48 -0
  134. package/dist-server/service/reference/hook-rejected.js.map +1 -0
  135. package/dist-server/service/reference/index.d.ts +4 -1
  136. package/dist-server/service/reference/index.js +5 -1
  137. package/dist-server/service/reference/index.js.map +1 -1
  138. package/dist-server/service/reference/live-cadence.d.ts +68 -0
  139. package/dist-server/service/reference/live-cadence.js +44 -0
  140. package/dist-server/service/reference/live-cadence.js.map +1 -0
  141. package/dist-server/service/reference/live-feed-lease.d.ts +15 -0
  142. package/dist-server/service/reference/live-feed-lease.js +56 -0
  143. package/dist-server/service/reference/live-feed-lease.js.map +1 -0
  144. package/dist-server/service/reference/live-ingest.d.ts +21 -0
  145. package/dist-server/service/reference/live-ingest.js +66 -0
  146. package/dist-server/service/reference/live-ingest.js.map +1 -0
  147. package/dist-server/service/reference/reference-adapter.d.ts +291 -2
  148. package/dist-server/service/reference/reference-adapter.js +19 -1
  149. package/dist-server/service/reference/reference-adapter.js.map +1 -1
  150. package/dist-server/service/reference/reference-fill-run.d.ts +50 -0
  151. package/dist-server/service/reference/reference-fill-run.js +142 -0
  152. package/dist-server/service/reference/reference-fill-run.js.map +1 -0
  153. package/dist-server/service/reference/reference-fill.d.ts +90 -0
  154. package/dist-server/service/reference/reference-fill.js +141 -0
  155. package/dist-server/service/reference/reference-fill.js.map +1 -0
  156. package/dist-server/service/reference/reference-hook.d.ts +22 -0
  157. package/dist-server/service/reference/reference-hook.js +135 -55
  158. package/dist-server/service/reference/reference-hook.js.map +1 -1
  159. package/dist-server/service/reference/reference-link-groups.d.ts +10 -0
  160. package/dist-server/service/reference/reference-link-groups.js +15 -0
  161. package/dist-server/service/reference/reference-link-groups.js.map +1 -0
  162. package/dist-server/service/reference/reference-links.d.ts +14 -0
  163. package/dist-server/service/reference/reference-links.js +73 -0
  164. package/dist-server/service/reference/reference-links.js.map +1 -0
  165. package/dist-server/service/reference/reference-live.d.ts +1 -0
  166. package/dist-server/service/reference/reference-live.js +32 -6
  167. package/dist-server/service/reference/reference-live.js.map +1 -1
  168. package/dist-server/service/reference/reference-master.d.ts +56 -2
  169. package/dist-server/service/reference/reference-master.js +33 -3
  170. package/dist-server/service/reference/reference-master.js.map +1 -1
  171. package/dist-server/service/reference/reference-resolver.d.ts +27 -0
  172. package/dist-server/service/reference/reference-resolver.js +79 -1
  173. package/dist-server/service/reference/reference-resolver.js.map +1 -1
  174. package/dist-server/service/twin-event/twin-event-keys.d.ts +31 -3
  175. package/dist-server/service/twin-event/twin-event-keys.js +36 -4
  176. package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
  177. package/dist-server/service/twin-event/twin-event.d.ts +1 -0
  178. package/dist-server/service/twin-event/twin-event.js +11 -2
  179. package/dist-server/service/twin-event/twin-event.js.map +1 -1
  180. package/dist-server/service/twin-model/choose-recipe.d.ts +15 -0
  181. package/dist-server/service/twin-model/choose-recipe.js +33 -0
  182. package/dist-server/service/twin-model/choose-recipe.js.map +1 -0
  183. package/dist-server/service/twin-model/external-resolver.d.ts +76 -0
  184. package/dist-server/service/twin-model/external-resolver.js +109 -0
  185. package/dist-server/service/twin-model/external-resolver.js.map +1 -0
  186. package/dist-server/service/twin-model/project-structure.js +16 -3
  187. package/dist-server/service/twin-model/project-structure.js.map +1 -1
  188. package/dist-server/service/twin-model/twin-equipment.d.ts +17 -0
  189. package/dist-server/service/twin-model/twin-equipment.js +10 -0
  190. package/dist-server/service/twin-model/twin-equipment.js.map +1 -1
  191. package/dist-server/service/twin-model/twin-lineage-query.js +75 -1
  192. package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -1
  193. package/dist-server/service/twin-model/twin-model-item-query.js +94 -5
  194. package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -1
  195. package/dist-server/service/twin-model/twin-model-tree-query.js.map +1 -1
  196. package/dist-server/service/twin-space/twin-space.d.ts +29 -0
  197. package/dist-server/service/twin-space/twin-space.js +13 -0
  198. package/dist-server/service/twin-space/twin-space.js.map +1 -1
  199. package/dist-server/service/twin-subject/event-subjects.d.ts +3 -1
  200. package/dist-server/service/twin-subject/event-subjects.js +8 -1
  201. package/dist-server/service/twin-subject/event-subjects.js.map +1 -1
  202. package/dist-shared/entity-delta.js +1 -1
  203. package/dist-shared/entity-delta.js.map +1 -1
  204. package/package.json +7 -7
  205. package/server/engine/canonical-ingest.ts +119 -33
  206. package/server/engine/event-time.ts +96 -0
  207. package/server/engine/index.ts +20 -0
  208. package/server/engine/ingest-dedupe.ts +203 -27
  209. package/server/engine/ingest-health.ts +91 -6
  210. package/server/engine/kpi-fold.ts +24 -1
  211. package/server/engine/kpi-query.ts +23 -4
  212. package/server/engine/oee-accumulator.ts +80 -8
  213. package/server/engine/twin-engine.ts +119 -5
  214. package/server/index.ts +23 -0
  215. package/server/migrations/1786600000000-RenameOperatoMesAdapterType.ts +66 -0
  216. package/server/migrations/1786700000000-AddDispatchRetryColumns.ts +82 -0
  217. package/server/migrations/index.ts +5 -1
  218. package/server/routes.ts +12 -24
  219. package/server/service/actuation/actuation-advisor.ts +124 -0
  220. package/server/service/actuation/actuation-outcome.ts +97 -0
  221. package/server/service/actuation/actuation-rule-resolver.ts +147 -0
  222. package/server/service/actuation/actuation-rule.ts +139 -0
  223. package/server/service/actuation/approval-gateway.ts +44 -0
  224. package/server/service/actuation/catch-up-effects.ts +152 -0
  225. package/server/service/actuation/command-dispatcher.ts +401 -20
  226. package/server/service/actuation/command-store.ts +182 -8
  227. package/server/service/actuation/dispatch-after-approval.ts +71 -0
  228. package/server/service/actuation/effect-match.ts +124 -0
  229. package/server/service/actuation/index.ts +21 -2
  230. package/server/service/actuation/retry-plan.ts +145 -0
  231. package/server/service/actuation/rule-evaluate.ts +352 -0
  232. package/server/service/actuation/rule-loop.ts +160 -0
  233. package/server/service/actuation/rule-runner.ts +216 -0
  234. package/server/service/actuation/rule-wiring.ts +84 -0
  235. package/server/service/actuation/settle-effects.ts +115 -0
  236. package/server/service/actuation/sweep-dispatch.ts +179 -0
  237. package/server/service/actuation/twin-command-resolver.ts +387 -0
  238. package/server/service/actuation/twin-command.ts +128 -1
  239. package/server/service/actuation/twin-now.ts +97 -0
  240. package/server/service/index.ts +9 -1
  241. package/server/service/reference/actuation-routing.ts +165 -0
  242. package/server/service/reference/actuation-target.ts +163 -0
  243. package/server/service/reference/connection-portability-resolver.ts +171 -0
  244. package/server/service/reference/connection-portability.ts +285 -0
  245. package/server/service/reference/fill-cadence.ts +29 -0
  246. package/server/service/reference/fill-dedupe.ts +66 -0
  247. package/server/service/reference/fill-loop.ts +153 -0
  248. package/server/service/reference/fill-span.ts +31 -0
  249. package/server/service/reference/hook-contract.ts +185 -5
  250. package/server/service/reference/hook-rejected.ts +53 -0
  251. package/server/service/reference/index.ts +5 -1
  252. package/server/service/reference/live-cadence.ts +129 -0
  253. package/server/service/reference/live-feed-lease.ts +56 -0
  254. package/server/service/reference/live-ingest.ts +94 -0
  255. package/server/service/reference/reference-adapter.ts +327 -5
  256. package/server/service/reference/reference-fill-run.ts +165 -0
  257. package/server/service/reference/reference-fill.ts +223 -0
  258. package/server/service/reference/reference-hook.ts +162 -56
  259. package/server/service/reference/reference-link-groups.ts +21 -0
  260. package/server/service/reference/reference-links.ts +77 -0
  261. package/server/service/reference/reference-live.ts +30 -5
  262. package/server/service/reference/reference-master.ts +90 -5
  263. package/server/service/reference/reference-resolver.ts +77 -2
  264. package/server/service/twin-event/twin-event-keys.ts +37 -4
  265. package/server/service/twin-event/twin-event.ts +18 -1
  266. package/server/service/twin-model/choose-recipe.ts +31 -0
  267. package/server/service/twin-model/external-resolver.ts +161 -0
  268. package/server/service/twin-model/project-structure.ts +16 -3
  269. package/server/service/twin-model/twin-equipment.ts +22 -0
  270. package/server/service/twin-model/twin-lineage-query.ts +82 -1
  271. package/server/service/twin-model/twin-model-item-query.ts +95 -4
  272. package/server/service/twin-model/twin-model-tree-query.ts +1 -1
  273. package/server/service/twin-space/twin-space.ts +39 -0
  274. package/server/service/twin-subject/event-subjects.ts +8 -1
  275. package/shared/entity-delta.ts +1 -1
  276. package/test/ack-shape-alignment.test.ts +97 -0
  277. package/test/actuation-approval-door.test.ts +421 -0
  278. package/test/actuation-dispatch.test.ts +16 -3
  279. package/test/actuation-effect.test.ts +162 -0
  280. package/test/actuation-outcome.test.ts +152 -0
  281. package/test/actuation-rule-runner.test.ts +281 -0
  282. package/test/actuation-rule.test.ts +266 -0
  283. package/test/actuation-seam.test.ts +249 -0
  284. package/test/actuation-settle.test.ts +164 -0
  285. package/test/actuation-target.test.ts +145 -0
  286. package/test/canonical-ingest-vocabularies.test.ts +49 -2
  287. package/test/capability-mapping.test.ts +1 -1
  288. package/test/choose-recipe.test.ts +117 -0
  289. package/test/command-store-writes-columns.test.ts +126 -0
  290. package/test/connection-portability-doors.test.ts +87 -0
  291. package/test/connector-capability-declaration.test.ts +54 -4
  292. package/test/declaration-reaches-model.test.ts +43 -0
  293. package/test/dedupe-transformation.test.ts +112 -0
  294. package/test/entity-delta.test.ts +1 -1
  295. package/test/event-subjects.test.ts +2 -1
  296. package/test/external-resolver.test.ts +171 -0
  297. package/test/fill-dedupe.test.ts +59 -0
  298. package/test/fill-loop.test.ts +35 -0
  299. package/test/first-envelope.test.ts +153 -0
  300. package/test/fold-shift-dst.test.ts +110 -0
  301. package/test/home-in-area.test.ts +90 -0
  302. package/test/hook-rejected-shape.test.ts +86 -0
  303. package/test/hook-response-sum.test.ts +193 -0
  304. package/test/hook-sequence.test.ts +1 -1
  305. package/test/ingest-dedupe.test.ts +56 -0
  306. package/test/ingest-expected-quiet.test.ts +111 -0
  307. package/test/ingest-idempotent.test.ts +79 -0
  308. package/test/ingest-reconcile-four-ways.test.ts +97 -0
  309. package/test/ingest-rules-header.test.ts +100 -0
  310. package/test/ingest-window-rolls-unobserved.test.ts +72 -0
  311. package/test/intake-mapping-parity.test.ts +175 -0
  312. package/test/live-cadence.test.ts +170 -0
  313. package/test/live-feed-lease.test.ts +63 -0
  314. package/test/live-mirror-parity.test.ts +1 -1
  315. package/test/oee-accumulator.test.ts +3 -3
  316. package/test/oee-site-calendar.test.ts +146 -0
  317. package/test/order-and-transaction-split.test.ts +107 -0
  318. package/test/reference-fill.test.ts +235 -0
  319. package/test/reference-hook.test.ts +1 -1
  320. package/test/reference-links.test.ts +56 -0
  321. package/test/retry-plan.test.ts +139 -0
  322. package/test/sweep-dispatch.test.ts +191 -0
  323. package/test/twin-event-keys.test.ts +20 -6
  324. package/test/twin-model-item-db.test.ts +6 -6
  325. package/test/twin-model-tree-db.test.ts +6 -6
  326. package/tsconfig.shared.tsbuildinfo +1 -1
  327. package/tsconfig.tsbuildinfo +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"reference-hook.js","sourceRoot":"","sources":["../../../server/service/reference/reference-hook.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * 연결된 시스템이 **밀어 주는** 사실을 받는 자리 — 웹훅.\n *\n * 규약(상태 코드 · 비밀값 · 대상 확인)은 `hook-contract.ts` 에 있다. 여기는 그것을 써서 찾고 · 확인하고 ·\n * 커넥터에게 옮기게 하고 · 유입으로 넘기는 순서만 맡는다.\n *\n * ── 왜 필요한가 ─────────────────────────────────────────────────────────────\n * 지금 모든 커넥터가 주기적으로 물어봅니다(폴링). 그 방식은 두 가지를 치릅니다. 연결된 시스템이 값을\n * 갱신하는 주기보다 자주 물으면 같은 값을 되풀이 받고, 그쪽 서버에 그만큼 부담을 줍니다. 실제로\n * 태양광 발전소에 10초마다 물으면서 10분마다 바뀌는 값을 받아 왔습니다.\n *\n * 연결된 시스템이 「바뀌었다」를 말해 줄 수 있으면 그것을 받는 것이 낫습니다. 이 파일이 그 입구입니다.\n *\n * ── 규약 ────────────────────────────────────────────────────────────────────\n *\n * 주소 POST /domain/<도메인>/twin/hook/<연결이름>/<트윈>\n * 인증 연결 설정에 저장한 비밀값(`hookSecret`)을 헤더로 보낸다\n * X-Twin-Hook-Secret: <비밀값> 또는 Authorization: Bearer <비밀값>\n * 본문 연결된 시스템이 정한 모양 그대로. 우리 어휘로 옮기는 것은 커넥터의 일이다\n * 응답 아래 §`hookStatus`\n *\n * ── 왜 사람 인증(JWT)이 아닌가 ──────────────────────────────────────────────\n * 밀어 주는 쪽은 사람이 아닙니다. 우리 토큰을 발급받아 갱신하며 관리하라고 요구하면 대부분의 현장에서\n * 연동이 서지 않습니다. 그래서 연결마다 비밀값을 두고 그것으로 확인합니다.\n *\n * **비밀값이 없는 연결은 훅을 받지 않습니다.** 기본이 거부입니다 — 인증 없는 입구를 열어 두는 것보다\n * 훅을 못 쓰는 것이 낫습니다.\n *\n * ── 왜 커넥터가 옮기나 (시나리오가 아니라) ──────────────────────────────────\n * 연결된 시스템의 웹훅은 그쪽이 정한 고정된 모양으로 옵니다. 그 모양을 아는 것은 그 커넥터이고,\n * 옮기는 판단은 코드여야 테스트로 지킬 수 있습니다. 실제로 지금 커넥터들이 코드로 판단합니다 —\n * 통신이 끊긴 설비의 값은 보내지 않고, 잰 시각이 나아가지 않은 줄은 거릅니다. 그 판단을 변환식으로\n * 옮기면 약해지고 테스트가 없어집니다.\n *\n * ── 밀어 주기만으로는 안 된다 ───────────────────────────────────────────────\n * 훅은 반드시 놓칩니다 — 우리가 내려가 있을 때, 그쪽이 못 보냈을 때, 네트워크가 끊겼을 때. 그래서\n * 훅을 쓰는 연결도 **드문 폴링을 유지합니다.** 폴링의 역할이 「값을 가져오는 것」에서 「놓친 것이 있나\n * 확인하는 것」으로 바뀌는 것입니다. 그 주기는 연결 설정이 정합니다.\n *\n * ── 같은 것이 두 번 오는 것 ─────────────────────────────────────────────────\n * 밀어 주는 방식은 재전송이 정상입니다(그쪽이 우리 응답을 못 받으면 다시 보냅니다). 그것은 유입\n * 경계가 이미 거릅니다(§`FactDeduper`) — 이 파일이 따로 하지 않습니다. 응답에 몇 건이 중복이었는지\n * 함께 알립니다.\n */\nimport { WEBHOOK_STATUS, checkSequence } from '@operato/ops-contract'\n\nimport { twinWarn } from '../../engine/log.js'\n\nimport { getAdapter, type InboundBatch } from './reference-adapter.js'\nimport { authorizeHook, producedInstance, pushCursorKey, seqOf, withSeq, type HookOutcome } from './hook-contract.js'\n\n/**\n * 훅 하나를 처리한다 — 찾고 · 확인하고 · 커넥터에게 옮기게 하고 · 유입으로 넘긴다.\n *\n * 유입 자체(`ingest`)는 부르는 쪽이 넘겨 준다: 이 파일이 엔진을 직접 부르면 시험할 때 엔진이 필요해지고,\n * 그러면 규약을 확인하는 시험을 쓸 수 없다.\n */\nexport async function handleHook(args: {\n domainId: string\n source: string\n instanceId: string\n headers: Record<string, unknown>\n body: unknown\n /**\n * 받은 바이트 그대로 — 서명을 확인하려면 이것이 있어야 한다.\n *\n * 파싱한 객체를 다시 문자열로 만들면 키 순서와 공백이 달라져 서명이 맞지 않는다. 없으면 서명을 쓰는\n * 요청은 거절된다(§`authorizeHook`).\n */\n rawBody?: string\n /** 서명 창을 재는 기준 시각. 부르는 쪽이 준다 — 여기서 읽으면 시험이 시계에 매인다. */\n nowMs?: number\n /** 레코드를 트윈에 넣는다 — 받은 수와 **떨어진 것들**을 돌려준다. */\n ingest: (instanceId: string, records: unknown[]) => IngestResult\n /**\n * 저장소를 대신 쓰는 자리 — **시험이 재기동을 넣을 수 있게.**\n *\n * `ingest` 를 부르는 쪽이 넘겨 주는 것과 같은 이유다(위 §). 커서는 재기동을 넘어 살아야 하는데, 한\n * 프로세스 안에서만 확인하면 「적히나」는 보이지만 「다시 세우면 읽히나」는 안 보인다. 그 둘이 갈린\n * 결함을 이 저장소가 이미 겪었다.\n */\n store: HookStore\n}): Promise<HookOutcome> {\n const { domainId, source, instanceId, headers, body, rawBody, ingest } = args\n const nowMs = args.nowMs ?? Date.now()\n const { store } = args\n\n const ref = await store.load(domainId, source)\n if (!ref) {\n return { status: WEBHOOK_STATUS.unknownTarget, body: { ok: false, error: `unknown connection \"${source}\"` } }\n }\n\n const cfg: any = (ref as any).connectionConfig ?? {}\n const expected = String(cfg.hookSecret ?? '').trim()\n if (!expected) {\n /* 비밀값을 선언하지 않은 연결은 훅을 받지 않는다 — 기본이 거부다. */\n return {\n status: WEBHOOK_STATUS.badSecret,\n body: {\n ok: false,\n error: `connection \"${source}\" declares no hookSecret — set one in the connection settings before pushing to it`\n }\n }\n }\n const auth = authorizeHook({ headers, rawBody, secret: expected, nowMs })\n if (auth.ok === false) {\n return { status: WEBHOOK_STATUS.badSecret, body: { ok: false, error: auth.reason } }\n }\n\n const site = producedInstance(ref as any, instanceId)\n if (!site) {\n return {\n status: WEBHOOK_STATUS.unknownTarget,\n body: { ok: false, error: `twin \"${instanceId}\" was not created by connection \"${source}\"` }\n }\n }\n\n const adapter: any = getAdapter(String((ref as any).adapterType ?? ''))\n if (typeof adapter?.handleInbound !== 'function') {\n return {\n status: WEBHOOK_STATUS.unsupported,\n body: { ok: false, error: `connector \"${(ref as any).adapterType}\" cannot take pushed events (no handleInbound)` }\n }\n }\n\n let batch: InboundBatch\n try {\n batch = adapter.handleInbound(cfg, site, body, headers)\n } catch (e: any) {\n /* 옮기지 못한 것은 그쪽 모양이 바뀐 것이다 — 다시 보내도 같으므로 재시도를 부르지 않는다. */\n return { status: WEBHOOK_STATUS.badPayload, body: { ok: false, error: e?.message ?? 'cannot translate the payload' } }\n }\n const items = batch?.items\n if (!Array.isArray(items)) {\n return {\n status: WEBHOOK_STATUS.badPayload,\n body: { ok: false, error: 'the connector did not return a list of items' }\n }\n }\n\n /*\n * **번호 확인** — 이 자리가 프레임워크인 이유는 밀어 주는 모든 연결에 같은 규율이라야 하기 때문이다.\n * 커넥터마다 만들면 한 곳이 빠지고, 빠진 그 연결에서만 사실이 없어진다.\n *\n * 번호를 매기지 않는 원본(chef · ppms)은 `seq` 를 주지 않고, 그때는 확인하지 않는다. 확인하지 않았다는\n * 것은 응답에 남긴다 — 「확인했고 이상 없다」와 구별되어야 한다.\n */\n const scope = typeof batch.scope === 'string' && batch.scope.trim() ? batch.scope.trim() : undefined\n const withSeqCount = items.filter(i => typeof i?.seq === 'number').length\n if (withSeqCount && withSeqCount !== items.length) {\n /* 섞이면 빠진 것이 있는지 판단할 근거가 없다. 반쯤 확인한 것을 확인했다고 말하지 않는다. */\n return {\n status: WEBHOOK_STATUS.badPayload,\n body: { ok: false, error: `mixed batch — ${withSeqCount} of ${items.length} items carry a sequence number` }\n }\n }\n const numbered = withSeqCount > 0 && scope !== undefined\n\n /*\n * **첫 구멍에서 멈춘다.** 구멍 뒤의 봉투를 받아들이면 그 구멍이 영영 메워지지 않는다 — 보내는 쪽은\n * `expectedSeq` 부터 다시 보내고, 뒤엣것은 그때 함께 온다.\n */\n let take = items.length\n let advanced: { scope: string; seq: number } | undefined\n let gap: { expectedSeq: number; lastSeq: number } | undefined\n if (numbered) {\n /* 열쇠에 트윈을 넣는다 — 연결 하나가 트윈 여럿을 만들고 커서는 연결마다 한 행이다. */\n const key = pushCursorKey(instanceId, scope as string)\n let cursor = seqOf((ref as any).liveCursor, key)\n take = 0\n for (const item of items) {\n let verdict\n try {\n verdict = checkSequence(cursor, item.seq as number)\n } catch (e: any) {\n return { status: WEBHOOK_STATUS.badPayload, body: { ok: false, error: e?.message ?? 'bad sequence number' } }\n }\n if (verdict.kind === 'gap') {\n gap = { expectedSeq: verdict.expectedSeq, lastSeq: verdict.lastSeq }\n break\n }\n cursor = verdict.lastSeq\n take++\n }\n /* 앞의 것을 하나도 못 받았으면 커서를 올릴 것이 없다. */\n if (take > 0 && typeof cursor === 'number') advanced = { scope: key, seq: cursor }\n }\n\n const records = items.slice(0, take).map(i => i.record)\n\n if (!records.length) {\n /*\n * 옮길 것이 없는 것은 오류가 아니다 — 그쪽이 우리와 무관한 것을 보낸 것일 수 있다. 다만 첫 봉투부터\n * 구멍이면 그것은 받은 것이 없는 것이고, 그 앞부터 달라고 해야 한다.\n */\n if (gap) return gapOutcome(scope, gap)\n if (advanced) await saveSeq(store, ref as any, advanced.scope, advanced.seq)\n return {\n status: WEBHOOK_STATUS.ok,\n body: { ok: true, offered: 0, applied: 0, duplicates: 0, rejected: [], ...seqBody(scope, advanced, numbered) }\n }\n }\n\n let result: IngestResult\n try {\n result = ingest(instanceId, records)\n } catch (e: any) {\n return { status: WEBHOOK_STATUS.failed, body: { ok: false, error: e?.message ?? 'ingest failed' } }\n }\n const rejected = result.rejected ?? []\n\n if (result.applied === 0 && result.duplicates === 0 && rejected.length === 0) {\n /*\n * 넣었는데 한 건도 반영되지 않았고 중복도 거부도 아니면, 그 트윈이 실시간으로 돌지 않는 것이다.\n * 200 을 주면 그쪽은 전달된 줄 알고 그 사실을 버린다.\n *\n * **커서를 올리지 않는다.** 한 건도 안 들어갔는데 번호를 올리면 그 번호는 다시 오지 않고, 트윈이\n * 뜬 뒤에 그 구간이 비어 있게 된다.\n */\n return {\n status: WEBHOOK_STATUS.notLive,\n body: {\n ok: false,\n error: `twin \"${instanceId}\" is not running live — nothing was applied`,\n offered: records.length,\n retryAfterMs: NOT_LIVE_RETRY_MS\n }\n }\n }\n\n if (advanced) await saveSeq(store, ref as any, advanced.scope, advanced.seq)\n\n /*\n * **구멍이 있으면 그것이 답을 정한다.** 앞의 것은 이미 들어갔으므로 `lastSeq` 까지는 보냄으로 표시하게\n * 하고, 떨어진 것이 있으면 그것도 함께 싣는다 — 두 소식이 한 응답에 들어가야 보내는 쪽이 둘 다 한다.\n */\n if (gap) return gapOutcome(scope, gap, rejected, advanced)\n\n if (rejected.length) {\n /*\n * **다시 보내라고 하지 않는다.** 떨어진 것은 모양이 틀린 것이라 같은 것을 다시 보내도 또 떨어지고,\n * 함께 간 나머지는 이미 들어갔으므로 두 번 들어간다.\n */\n return {\n status: WEBHOOK_STATUS.partial,\n body: {\n ok: false,\n error: `${rejected.length} of ${records.length} records did not pass validation`,\n offered: records.length,\n applied: result.applied,\n duplicates: result.duplicates,\n rejected,\n ...seqBody(scope, advanced, numbered)\n }\n }\n }\n\n return {\n status: WEBHOOK_STATUS.ok,\n body: {\n ok: true,\n offered: records.length,\n applied: result.applied,\n duplicates: result.duplicates,\n rejected: [],\n ...seqBody(scope, advanced, numbered)\n }\n }\n}\n\n/**\n * 유입이 돌려주는 것.\n *\n * 떨어진 것을 **수가 아니라 목록으로** 받는다. 수만 받으면 보내는 쪽에 「몇 건이 떨어졌다」밖에 말할 수\n * 없고, 그러면 무엇을 따로 두어야 할지 알 수 없어 배치 전체를 다시 보내거나 전부 버린다.\n */\nexport interface IngestResult {\n applied: number\n duplicates: number\n rejected: { record: unknown; errors: string[] }[]\n}\n\n/**\n * 구멍을 알리는 응답 — **앞의 것은 받았다는 것을 함께 말한다.**\n *\n * `lastSeq` 를 빼면 보내는 쪽은 이미 들어간 것까지 다시 보내고, 그것이 매번 중복으로 걸러지면서\n * 처리량만 먹는다.\n */\nfunction gapOutcome(\n scope: string | undefined,\n gap: { expectedSeq: number; lastSeq: number },\n rejected: { record: unknown; errors: string[] }[] = [],\n advanced?: { scope: string; seq: number }\n): HookOutcome {\n return {\n status: WEBHOOK_STATUS.sequenceGap,\n body: {\n ok: false,\n error: 'sequence gap',\n scope,\n expectedSeq: gap.expectedSeq,\n lastSeq: advanced?.seq ?? gap.lastSeq,\n ...(rejected.length ? { rejected } : {})\n }\n }\n}\n\n/**\n * 트윈이 안 돌 때 얼마나 쉬라고 할 것인가.\n *\n * 사람이 트윈을 띄워야 풀리므로 초 단위로 다시 두드릴 이유가 없다. 그렇다고 길게 잡으면 트윈이 뜬 뒤\n * 그만큼 늦게 이어진다.\n */\nconst NOT_LIVE_RETRY_MS = 30_000\n\n/**\n * 응답에 번호를 어떻게 적나 — **확인한 것과 확인하지 않은 것을 구별한다.**\n *\n * 번호를 매기지 않는 원본은 확인할 것이 없다. 그것을 응답에서 「이상 없다」와 같아 보이게 두면, 어느\n * 연결이 연속성을 지키고 있는지 아무도 알 수 없다.\n */\nfunction seqBody(\n scope: string | undefined,\n advanced: { scope: string; seq: number } | undefined,\n numbered: boolean\n): Record<string, unknown> {\n if (!numbered) return { sequenceChecked: false }\n return { sequenceChecked: true, scope, lastSeq: advanced?.seq }\n}\n\n/**\n * 커서에 번호를 적는다 — **반영한 뒤에만.**\n *\n * 넣기 전에 올리면, 넣다가 실패했을 때 그 번호는 다시 오지 않는다. 보내는 쪽은 200 을 못 받았으니 다시\n * 보내겠지만, 그때는 커서가 이미 지나가 있어 `behind` 로 읽히고 버려진다.\n *\n * 적지 못하는 것은 사실 유입을 무르는 이유가 아니다 — 이미 들어간 사실은 그대로 두고, 다음 번호가 오면\n * `gap` 으로 드러난다. 그래서 실패를 던지지 않고 남긴다.\n */\nasync function saveSeq(\n store: HookStore,\n ref: { id: string; liveCursor?: unknown },\n scope: string,\n seq: number\n): Promise<void> {\n const next = withSeq(ref.liveCursor, scope, seq)\n ref.liveCursor = next\n await store\n .saveCursor(ref.id, next)\n .catch(err => twinWarn(`[twin-hook] 번호를 커서에 적지 못했다 (${scope}=${seq}) — ${err?.message ?? err}`))\n}\n\n/**\n * 훅이 저장소에 닿는 두 자리 — 연결을 찾는 것과 커서를 적는 것.\n *\n * 이 둘만 밖으로 낸다. 나머지는 인자만 보고 답하므로 시험이 데이터베이스를 세우지 않아도 된다.\n */\nexport interface HookStore {\n load(domainId: string, source: string): Promise<any | null>\n saveCursor(refId: string, cursor: unknown): Promise<void>\n}\n\n"]}
1
+ {"version":3,"file":"reference-hook.js","sourceRoot":"","sources":["../../../server/service/reference/reference-hook.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * 연결된 시스템이 **밀어 주는** 사실을 받는 자리 — 웹훅.\n *\n * 규약(상태 코드 · 비밀값 · 대상 확인)은 `hook-contract.ts` 에 있다. 여기는 그것을 써서 찾고 · 확인하고 ·\n * 커넥터에게 옮기게 하고 · 유입으로 넘기는 순서만 맡는다.\n *\n * ── 왜 필요한가 ─────────────────────────────────────────────────────────────\n * 지금 모든 커넥터가 주기적으로 물어봅니다(폴링). 그 방식은 두 가지를 치릅니다. 연결된 시스템이 값을\n * 갱신하는 주기보다 자주 물으면 같은 값을 되풀이 받고, 그쪽 서버에 그만큼 부담을 줍니다. 실제로\n * 태양광 발전소에 10초마다 물으면서 10분마다 바뀌는 값을 받아 왔습니다.\n *\n * 연결된 시스템이 「바뀌었다」를 말해 줄 수 있으면 그것을 받는 것이 낫습니다. 이 파일이 그 입구입니다.\n *\n * ── 규약 ────────────────────────────────────────────────────────────────────\n *\n * 주소 POST /domain/<도메인>/twin/hook/<연결이름>/<트윈>\n * 인증 연결 설정에 저장한 비밀값(`hookSecret`)을 헤더로 보낸다\n * X-Twin-Hook-Secret: <비밀값> 또는 Authorization: Bearer <비밀값>\n * 본문 연결된 시스템이 정한 모양 그대로. 우리 어휘로 옮기는 것은 커넥터의 일이다\n * 응답 아래 §`hookStatus`\n *\n * ── 왜 사람 인증(JWT)이 아닌가 ──────────────────────────────────────────────\n * 밀어 주는 쪽은 사람이 아닙니다. 우리 토큰을 발급받아 갱신하며 관리하라고 요구하면 대부분의 현장에서\n * 연동이 서지 않습니다. 그래서 연결마다 비밀값을 두고 그것으로 확인합니다.\n *\n * **비밀값이 없는 연결은 훅을 받지 않습니다.** 기본이 거부입니다 — 인증 없는 입구를 열어 두는 것보다\n * 훅을 못 쓰는 것이 낫습니다.\n *\n * ── 왜 커넥터가 옮기나 (시나리오가 아니라) ──────────────────────────────────\n * 연결된 시스템의 웹훅은 그쪽이 정한 고정된 모양으로 옵니다. 그 모양을 아는 것은 그 커넥터이고,\n * 옮기는 판단은 코드여야 테스트로 지킬 수 있습니다. 실제로 지금 커넥터들이 코드로 판단합니다 —\n * 통신이 끊긴 설비의 값은 보내지 않고, 잰 시각이 나아가지 않은 줄은 거릅니다. 그 판단을 변환식으로\n * 옮기면 약해지고 테스트가 없어집니다.\n *\n * ── 밀어 주기만으로는 안 된다 ───────────────────────────────────────────────\n * 훅은 반드시 놓칩니다 — 우리가 내려가 있을 때, 그쪽이 못 보냈을 때, 네트워크가 끊겼을 때. 그래서\n * 훅을 쓰는 연결도 **드문 폴링을 유지합니다.** 폴링의 역할이 「값을 가져오는 것」에서 「놓친 것이 있나\n * 확인하는 것」으로 바뀌는 것입니다. 그 주기는 연결 설정이 정합니다.\n *\n * ── 같은 것이 두 번 오는 것 ─────────────────────────────────────────────────\n * 밀어 주는 방식은 재전송이 정상입니다(그쪽이 우리 응답을 못 받으면 다시 보냅니다). 그것은 유입\n * 경계가 이미 거릅니다(§`FactDeduper`) — 이 파일이 따로 하지 않습니다. 응답에 몇 건이 중복이었는지\n * 함께 알립니다.\n */\nimport { WEBHOOK_STATUS, type WebhookResponse } from '@operato/ops-contract'\n\nimport { twinWarn } from '../../engine/log.js'\n\nimport { getAdapter, type InboundBatch } from './reference-adapter.js'\nimport { rejectedForCaller, type RejectedForCaller } from './hook-rejected.js'\nexport { rejectedForCaller, type RejectedForCaller } from './hook-rejected.js'\nimport { authorizeHook, producedInstance, pushCursorKey, seqOf, takeInSequence, withSeq, type HookOutcome } from './hook-contract.js'\n\n/**\n * 훅 하나를 처리한다 — 찾고 · 확인하고 · 커넥터에게 옮기게 하고 · 유입으로 넘긴다.\n *\n * 유입 자체(`ingest`)는 부르는 쪽이 넘겨 준다: 이 파일이 엔진을 직접 부르면 시험할 때 엔진이 필요해지고,\n * 그러면 규약을 확인하는 시험을 쓸 수 없다.\n */\nexport async function handleHook(args: {\n domainId: string\n source: string\n instanceId: string\n headers: Record<string, unknown>\n body: unknown\n /**\n * 받은 바이트 그대로 — 서명을 확인하려면 이것이 있어야 한다.\n *\n * 파싱한 객체를 다시 문자열로 만들면 키 순서와 공백이 달라져 서명이 맞지 않는다. 없으면 서명을 쓰는\n * 요청은 거절된다(§`authorizeHook`).\n */\n rawBody?: string\n /** 서명 창을 재는 기준 시각. 부르는 쪽이 준다 — 여기서 읽으면 시험이 시계에 매인다. */\n nowMs?: number\n /** 레코드를 트윈에 넣는다 — 받은 수와 **떨어진 것들**을 돌려준다. */\n ingest: (instanceId: string, records: unknown[]) => IngestResult\n /**\n * 구멍을 봤다 — **부르는 쪽이 무엇을 할지 정한다.**\n *\n * 여기서 backfill 을 직접 부르지 않는다. 이 파일은 `ingest`·`store` 를 인자로 받아 가볍게 유지한다\n * (그래야 테스트가 엔진 없이 이 판정을 확인할 수 있다). backfill 을 import 하면 그 규율이 깨진다.\n *\n * 기다리지 않는 것도 부르는 쪽의 몫이다 — 훅의 응답이 늦으면 보내는 쪽이 되풀이한다.\n */\n onGap?: (scope: string) => void\n /**\n * 저장소를 대신 쓰는 자리 — **시험이 재기동을 넣을 수 있게.**\n *\n * `ingest` 를 부르는 쪽이 넘겨 주는 것과 같은 이유다(위 §). 커서는 재기동을 넘어 살아야 하는데, 한\n * 프로세스 안에서만 확인하면 「적히나」는 보이지만 「다시 세우면 읽히나」는 안 보인다. 그 둘이 갈린\n * 결함을 이 저장소가 이미 겪었다.\n */\n store: HookStore\n}): Promise<HookOutcome> {\n const { domainId, source, instanceId, headers, body, rawBody, ingest, onGap } = args\n const nowMs = args.nowMs ?? Date.now()\n const { store } = args\n\n const ref = await store.load(domainId, source)\n if (!ref) {\n return { status: WEBHOOK_STATUS.unknownTarget, body: { ok: false, error: `unknown connection \"${source}\"` } }\n }\n\n const cfg: any = (ref as any).connectionConfig ?? {}\n const expected = String(cfg.hookSecret ?? '').trim()\n if (!expected) {\n /* 비밀값을 선언하지 않은 연결은 훅을 받지 않는다 — 기본이 거부다. */\n return {\n status: WEBHOOK_STATUS.badSecret,\n body: {\n ok: false,\n error: `connection \"${source}\" declares no hookSecret — set one in the connection settings before pushing to it`\n }\n }\n }\n const auth = authorizeHook({ headers, rawBody, secret: expected, nowMs })\n if (auth.ok === false) {\n return { status: WEBHOOK_STATUS.badSecret, body: { ok: false, error: auth.reason } }\n }\n\n const site = producedInstance(ref as any, instanceId)\n if (!site) {\n return {\n status: WEBHOOK_STATUS.unknownTarget,\n body: { ok: false, error: `twin \"${instanceId}\" was not created by connection \"${source}\"` }\n }\n }\n\n const adapter: any = getAdapter(String((ref as any).adapterType ?? ''))\n if (typeof adapter?.handleInbound !== 'function') {\n return {\n status: WEBHOOK_STATUS.unsupported,\n body: { ok: false, error: `connector \"${(ref as any).adapterType}\" cannot take pushed events (no handleInbound)` }\n }\n }\n\n let batch: InboundBatch\n try {\n batch = adapter.handleInbound(cfg, site, body, headers)\n } catch (e: any) {\n /* 옮기지 못한 것은 그쪽 모양이 바뀐 것이다 — 다시 보내도 같으므로 재시도를 부르지 않는다. */\n return { status: WEBHOOK_STATUS.badPayload, body: { ok: false, error: e?.message ?? 'cannot translate the payload' } }\n }\n const items = batch?.items\n if (!Array.isArray(items)) {\n return {\n status: WEBHOOK_STATUS.badPayload,\n body: { ok: false, error: 'the connector did not return a list of items' }\n }\n }\n\n /*\n * **번호 확인** — 이 자리가 프레임워크인 이유는 밀어 주는 모든 연결에 같은 규율이라야 하기 때문이다.\n * 커넥터마다 만들면 한 곳이 빠지고, 빠진 그 연결에서만 사실이 없어진다.\n *\n * 판정은 `takeInSequence` 하나가 한다 — **backfill 하는 길도 같은 함수를 지난다.** 여기서 따로 만들면\n * 두 길이 다른 규칙으로 받고, 한쪽만 고쳐지는 날이 온다.\n */\n const scope = typeof batch.scope === 'string' && batch.scope.trim() ? batch.scope.trim() : undefined\n /* 열쇠에 트윈을 넣는다 — 연결 하나가 트윈 여럿을 만들고 커서는 연결마다 한 행이다. */\n const key = scope ? pushCursorKey(instanceId, scope) : undefined\n const taken = takeInSequence({\n items,\n scope,\n lastSeq: key ? seqOf((ref as any).liveCursor, key) : undefined\n })\n if (taken.refused) {\n /* 반쯤 확인한 것을 확인했다고 말하지 않는다. */\n return { status: WEBHOOK_STATUS.badPayload, body: { ok: false, error: taken.refused } }\n }\n const numbered = taken.numbered\n const gap = taken.gap\n /*\n * 첫 봉투가 1번이 아니면 **그 사실을 남긴다.** 커서가 없는 연결에 21번이 오면 커서가 21 로 뛰고,\n * 그 앞 스무 건은 구멍으로도 안 세어진다. 실제로 plant 에서 그렇게 스무 건이 조용히 사라졌다.\n *\n * 손실이라고 말하지 않는다 — 늦게 붙은 트윈은 그 앞을 안 받기로 한 것이다. 수만 말하고 판단은\n * 사람이 한다.\n */\n if (taken.firstUnseenBefore) {\n twinWarn(\n `[twin-ingest] \"${instanceId}\" 의 ${scope ?? '(단위 없음)'} 를 ${taken.advancedSeq} 번부터 받기 시작했습니다 — ` +\n `그 앞 ${taken.firstUnseenBefore}건은 받은 적이 없습니다. 원본이 오래 돌고 있었으면 정상이고, ` +\n `보냈는데 못 받은 것이면 그만큼이 빠진 것입니다`\n )\n }\n const advanced = taken.advancedSeq !== undefined && key ? { scope: key, seq: taken.advancedSeq } : undefined\n\n const records = taken.records\n\n if (!records.length) {\n /*\n * 옮길 것이 없는 것은 오류가 아니다 — 그쪽이 우리와 무관한 것을 보낸 것일 수 있다. 다만 첫 봉투부터\n * 구멍이면 그것은 받은 것이 없는 것이고, 그 앞부터 달라고 해야 한다.\n */\n if (gap) {\n if (scope) onGap?.(scope)\n return gapOutcome(scope, gap)\n }\n if (advanced) await saveSeq(store, ref as any, advanced.scope, advanced.seq, taken.firstSeq)\n return {\n status: WEBHOOK_STATUS.ok,\n body: {\n ok: true,\n offered: 0,\n applied: 0,\n duplicates: 0,\n vocabulary: 0,\n rejected: [],\n ...seqBody(scope, advanced, numbered, taken.firstUnseenBefore)\n }\n }\n }\n\n let result: IngestResult\n try {\n result = ingest(instanceId, records)\n } catch (e: any) {\n return { status: WEBHOOK_STATUS.failed, body: { ok: false, error: e?.message ?? 'ingest failed' } }\n }\n const rejected = result.rejected ?? []\n /*\n * ── 떨어진 것에 **봉투 id** 를 붙인다 (2026-09-07 측정) ──────────────────────\n * 이 응답이 `{ record, errors }` 목록이었고, plant 은 그것을 **봉투 id 문자열 목록**으로 읽고\n * 있었다. `Array.isArray` 는 통과하므로 그쪽 guard 도 안 걸렸다 — id 를 맞추는데 객체라 하나도\n * 안 맞고 **묶음 전체가 「보냈음」으로 찍혔다.**\n *\n * 422 와 `ok: false` 를 보냈는데 그쪽 행은 재시도 0 · 오류 없음 · 보냈음이었다. 우리가 말했는데\n * 상대가 못 알아듣는 모양이면, 말한 것이 아니다.\n *\n * 그래서 봉투 id 를 함께 싣는다. 보내는 쪽이 자기 행을 표시할 수 있고 까닭도 남는다.\n *\n * **id 를 못 찾으면 레코드를 그대로 싣는다.** 그때는 무엇이 떨어졌는지 알 방법이 그것뿐이고,\n * 빈 id 를 실으면 보내는 쪽이 아무 행도 못 찾으면서 「알았다」고 여긴다.\n *\n * 짝은 **객체 동일성**으로 짓는다. `records` 는 `items` 의 순서와 원소를 그대로 들고 있고\n * (§`takeInSequence`), 그래서 커넥터를 고치지 않고도 봉투로 되짚을 수 있다. 값으로 비교하면\n * 같은 모양의 레코드 둘이 서로의 id 를 가져간다.\n */\n const forCaller = rejectedForCaller(rejected, items as { record?: unknown; eventId?: unknown }[])\n\n if (result.applied === 0 && result.duplicates === 0 && rejected.length === 0 && !result.vocabulary) {\n /*\n * 넣었는데 한 건도 반영되지 않았고 중복도 거부도 아니면, 그 트윈이 실시간으로 돌지 않는 것이다.\n * 200 을 주면 그쪽은 전달된 줄 알고 그 사실을 버린다.\n *\n * **커서를 올리지 않는다.** 한 건도 안 들어갔는데 번호를 올리면 그 번호는 다시 오지 않고, 트윈이\n * 뜬 뒤에 그 구간이 비어 있게 된다.\n *\n * ── Vocabulary is why this test needs a fourth term (2026-09-07) ──────────\n * This inference reads an absence as a cause, and one absence is legitimate: a batch that was\n * entirely vocabulary applies no events, has no duplicates and rejects nothing, and yet\n * everything in it landed (§`applyMasterData`). Without the last term the receiver answers\n * 503 to correct traffic, and the sender then holds those facts and retries every thirty\n * seconds for as long as they keep arriving — the reply invents a reason (\"not running live\")\n * that the numbers do not support.\n *\n * Found by a test written for the response sum, not by reading this line.\n *\n * ── Why this branch has to say something out loud ─────────────────────────\n * Measured today: a 503 from here leaves **no trace on either side.** The sender maps it to\n * backpressure and returns without touching `attempts` or `lastError`, and this branch logged\n * nothing, so a sender re-offering the same batch every thirty seconds is invisible to both.\n *\n * That is also why \"it has not happened\" could not be established: `attempts 0` on the seven\n * vocabulary envelopes was offered as evidence and is not evidence, because this path never\n * increments that counter. Telling a sender to come back forever is worth one line.\n */\n twinWarn(\n `[twin-hook] \"${instanceId}\" 에 ${records.length}건이 왔는데 한 건도 앉지 않아 ` +\n `${NOT_LIVE_RETRY_MS / 1000}초 뒤 다시 오라고 답했습니다 — 트윈이 실시간으로 돌지 않는 것으로 봅니다. ` +\n `보내는 쪽은 이 답에 재시도 횟수를 남기지 않으므로, 되풀이되면 이 줄만 늘어납니다`\n )\n\n return {\n status: WEBHOOK_STATUS.notLive,\n body: {\n ok: false,\n error: `twin \"${instanceId}\" is not running live — nothing was applied`,\n offered: records.length,\n retryAfterMs: NOT_LIVE_RETRY_MS\n }\n }\n }\n\n if (advanced) await saveSeq(store, ref as any, advanced.scope, advanced.seq, taken.firstSeq)\n\n /*\n * **구멍이 있으면 그것이 답을 정한다.** 앞의 것은 이미 들어갔으므로 `lastSeq` 까지는 보냄으로 표시하게\n * 하고, 떨어진 것이 있으면 그것도 함께 싣는다 — 두 소식이 한 응답에 들어가야 보내는 쪽이 둘 다 한다.\n */\n if (gap) {\n if (scope) onGap?.(scope)\n return gapOutcome(scope, gap, forCaller, advanced)\n }\n\n if (rejected.length) {\n /*\n * **다시 보내라고 하지 않는다.** 떨어진 것은 모양이 틀린 것이라 같은 것을 다시 보내도 또 떨어지고,\n * 함께 간 나머지는 이미 들어갔으므로 두 번 들어간다.\n */\n return {\n status: WEBHOOK_STATUS.partial,\n body: {\n ok: false,\n error: `${rejected.length} of ${records.length} records did not pass validation`,\n offered: records.length,\n applied: result.applied,\n duplicates: result.duplicates,\n vocabulary: result.vocabulary ?? 0,\n rejected: forCaller,\n ...seqBody(scope, advanced, numbered, taken.firstUnseenBefore)\n }\n }\n }\n\n return {\n status: WEBHOOK_STATUS.ok,\n body: {\n ok: true,\n offered: records.length,\n applied: result.applied,\n duplicates: result.duplicates,\n /*\n * Always reported, even as zero — the sender's sum has a term for it either way, and a field\n * that appears only sometimes makes an absent one indistinguishable from a receiver that\n * does not count vocabulary at all.\n */\n vocabulary: result.vocabulary ?? 0,\n rejected: [],\n ...seqBody(scope, advanced, numbered, taken.firstUnseenBefore)\n }\n }\n}\n\n/**\n * 유입이 돌려주는 것.\n *\n * 떨어진 것을 **수가 아니라 목록으로** 받는다. 수만 받으면 보내는 쪽에 「몇 건이 떨어졌다」밖에 말할 수\n * 없고, 그러면 무엇을 따로 두어야 할지 알 수 없어 배치 전체를 다시 보내거나 전부 버린다.\n */\nexport interface IngestResult {\n applied: number\n duplicates: number\n rejected: { record: unknown; errors: string[] }[]\n /**\n * How many records were vocabulary rather than events — **needed so the reply's sum closes.**\n *\n * `liveIngest` has counted this all along and this boundary dropped it, because the callback was\n * typed as `IngestResult` and vocabulary was not on it. So `offered` counted those records and\n * none of the other three did, and a sender checking the sum would have seen a loss on ordinary\n * traffic (§`WebhookResponse.offered`).\n *\n * Optional because an ingest that handles no vocabulary need not say zero, and a caller that\n * simply does not know must not be made to claim none.\n */\n vocabulary?: number\n}\n\n/**\n * 구멍을 알리는 응답 — **앞의 것은 받았다는 것을 함께 말한다.**\n *\n * `lastSeq` 를 빼면 보내는 쪽은 이미 들어간 것까지 다시 보내고, 그것이 매번 중복으로 걸러지면서\n * 처리량만 먹는다.\n */\n/*\n * 구멍을 본 자리에서 **바로 받는다** — 번호가 비었다는 것을 아는 가장 이른 순간이다.\n *\n * 기다리지 않는다. 훅의 응답이 이것 때문에 늦으면 보내는 쪽이 되풀이하고, 그러면 같은 봉투가 두 번\n * 온다. 그리고 같은 단위를 1분 안에 두 번 부르지 않는다(§`FILL_DEBOUNCE_MS`) — 밀어 받는 쪽이\n * 구멍 뒤의 봉투를 계속 보내면 그때마다 구멍 판정이 나기 때문이다.\n */\nfunction gapOutcome(\n scope: string | undefined,\n gap: { expectedSeq: number; lastSeq: number },\n /*\n * 떨어진 것 — 봉투 id 를 알면 그것을, 모르면 레코드를 싣는다(§`InboundItem.eventId`).\n * 보내는 쪽이 자기 행을 찾을 수 있어야 하고, 못 찾으면 무엇이 떨어졌는지라도 보여야 한다.\n */\n rejected: RejectedForCaller[] = [],\n advanced?: { scope: string; seq: number }\n): HookOutcome {\n return {\n status: WEBHOOK_STATUS.sequenceGap,\n body: {\n ok: false,\n error: 'sequence gap',\n scope,\n expectedSeq: gap.expectedSeq,\n lastSeq: advanced?.seq ?? gap.lastSeq,\n ...(rejected.length ? { rejected } : {})\n }\n }\n}\n\n/**\n * 트윈이 안 돌 때 얼마나 쉬라고 할 것인가.\n *\n * 사람이 트윈을 띄워야 풀리므로 초 단위로 다시 두드릴 이유가 없다. 그렇다고 길게 잡으면 트윈이 뜬 뒤\n * 그만큼 늦게 이어진다.\n */\nconst NOT_LIVE_RETRY_MS = 30_000\n\n/**\n * 응답에 번호를 어떻게 적나 — **확인한 것과 확인하지 않은 것을 구별한다.**\n *\n * 번호를 매기지 않는 원본은 확인할 것이 없다. 그것을 응답에서 「이상 없다」와 같아 보이게 두면, 어느\n * 연결이 연속성을 지키고 있는지 아무도 알 수 없다.\n */\n/*\n * The return type is the contract's, narrowed to the fields this function fills.\n *\n * It was `Record<string, unknown>`, and because the result is **spread** into the reply, an index\n * signature slips past the excess-property check entirely — the four fields below were unverified\n * even after `HookOutcome.body` was typed. Naming them here is what makes the check reach them.\n */\nfunction seqBody(\n scope: string | undefined,\n advanced: { scope: string; seq: number } | undefined,\n numbered: boolean,\n firstUnseenBefore?: number\n): Pick<WebhookResponse, 'sequenceChecked' | 'scope' | 'lastSeq' | 'firstUnseenBefore'> {\n if (!numbered) return { sequenceChecked: false }\n return {\n sequenceChecked: true,\n scope,\n lastSeq: advanced?.seq,\n /*\n * 첫 봉투였고 그 앞을 못 봤으면 **그 수를 보내는 쪽에 돌려준다.** 안 돌려주면 그 사실이 아무 데도\n * 안 남는다 — 구멍으로도 안 세어지고 backfill 대상도 아니다(backfill 은 커서부터 묻는다).\n */\n ...(firstUnseenBefore ? { firstUnseenBefore } : {})\n }\n}\n\n/**\n * 커서에 번호를 적는다 — **반영한 뒤에만.**\n *\n * 넣기 전에 올리면, 넣다가 실패했을 때 그 번호는 다시 오지 않는다. 보내는 쪽은 200 을 못 받았으니 다시\n * 보내겠지만, 그때는 커서가 이미 지나가 있어 `behind` 로 읽히고 버려진다.\n *\n * 적지 못하는 것은 사실 유입을 무르는 이유가 아니다 — 이미 들어간 사실은 그대로 두고, 다음 번호가 오면\n * `gap` 으로 드러난다. 그래서 실패를 던지지 않고 남긴다.\n */\nasync function saveSeq(\n store: HookStore,\n ref: { id: string; liveCursor?: unknown },\n scope: string,\n seq: number,\n firstSeq?: number\n): Promise<void> {\n const next = withSeq(ref.liveCursor, scope, seq, firstSeq)\n ref.liveCursor = next\n await store\n .saveCursor(ref.id, next)\n .catch(err => twinWarn(`[twin-hook] 번호를 커서에 적지 못했다 (${scope}=${seq}) — ${err?.message ?? err}`))\n}\n\n/**\n * 훅이 저장소에 닿는 두 자리 — 연결을 찾는 것과 커서를 적는 것.\n *\n * 이 둘만 밖으로 낸다. 나머지는 인자만 보고 답하므로 시험이 데이터베이스를 세우지 않아도 된다.\n */\nexport interface HookStore {\n load(domainId: string, source: string): Promise<any | null>\n saveCursor(refId: string, cursor: unknown): Promise<void>\n}\n\n"]}
@@ -0,0 +1,10 @@
1
+ import type { ExternalIncoming } from '../twin-model/external-resolver.js';
2
+ /**
3
+ * 이름(`via`)이 없으면 화면에 이름 없는 줄이 된다. `total` 이 0 이면 낼 내용이 없다 — 원본이 그
4
+ * 축을 취급하지 않는다는 뜻이고, 트윈이 선언한 관계가 비어 있는 것과 다르다. 후자는 그대로
5
+ * 표시한다(`twin-model-item-query.ts`).
6
+ *
7
+ * `total` 이 없는 그룹도 버린다. 목록만 보내고 전체 수를 빼면 화면이 목록 길이를 전체 수로 쓰게
8
+ * 되고, 원본이 20개만 보낸 날 실제 200건이 20건으로 표시된다.
9
+ */
10
+ export declare function usableLinkGroups(groups: ExternalIncoming[] | undefined | null): ExternalIncoming[];
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.usableLinkGroups = usableLinkGroups;
4
+ /**
5
+ * 이름(`via`)이 없으면 화면에 이름 없는 줄이 된다. `total` 이 0 이면 낼 내용이 없다 — 원본이 그
6
+ * 축을 취급하지 않는다는 뜻이고, 트윈이 선언한 관계가 비어 있는 것과 다르다. 후자는 그대로
7
+ * 표시한다(`twin-model-item-query.ts`).
8
+ *
9
+ * `total` 이 없는 그룹도 버린다. 목록만 보내고 전체 수를 빼면 화면이 목록 길이를 전체 수로 쓰게
10
+ * 되고, 원본이 20개만 보낸 날 실제 200건이 20건으로 표시된다.
11
+ */
12
+ function usableLinkGroups(groups) {
13
+ return (groups ?? []).filter(g => !!g && typeof g.via === 'string' && !!g.via.trim() && typeof g.total === 'number' && g.total > 0);
14
+ }
15
+ //# sourceMappingURL=reference-link-groups.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reference-link-groups.js","sourceRoot":"","sources":["../../../server/service/reference/reference-link-groups.ts"],"names":[],"mappings":";;AAgBA,4CAIC;AAZD;;;;;;;GAOG;AACH,SAAgB,gBAAgB,CAAC,MAA6C;IAC5E,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,MAAM,CAC1B,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,GAAG,KAAK,QAAQ,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,OAAO,CAAC,CAAC,KAAK,KAAK,QAAQ,IAAI,CAAC,CAAC,KAAK,GAAG,CAAC,CACtG,CAAA;AACH,CAAC","sourcesContent":["/*\n * 원본이 준 관계 그룹 중 화면에 낼 수 있는 것만 남기는 판정.\n *\n * 조회 코드(`reference-links.ts`)와 나누어 둔다. 그쪽은 TypeORM 엔티티를 물어서 테스트 프로세스가\n * 적재할 수 없다. 판정만 따로 두면 테스트가 가벼운 import 로 확인할 수 있다.\n */\nimport type { ExternalIncoming } from '../twin-model/external-resolver.js'\n\n/**\n * 이름(`via`)이 없으면 화면에 이름 없는 줄이 된다. `total` 이 0 이면 낼 내용이 없다 — 원본이 그\n * 축을 취급하지 않는다는 뜻이고, 트윈이 선언한 관계가 비어 있는 것과 다르다. 후자는 그대로\n * 표시한다(`twin-model-item-query.ts`).\n *\n * `total` 이 없는 그룹도 버린다. 목록만 보내고 전체 수를 빼면 화면이 목록 길이를 전체 수로 쓰게\n * 되고, 원본이 20개만 보낸 날 실제 200건이 20건으로 표시된다.\n */\nexport function usableLinkGroups(groups: ExternalIncoming[] | undefined | null): ExternalIncoming[] {\n return (groups ?? []).filter(\n g => !!g && typeof g.via === 'string' && !!g.via.trim() && typeof g.total === 'number' && g.total > 0\n )\n}\n"]}
@@ -0,0 +1,14 @@
1
+ import { type ExternalIncoming } from '../twin-model/external-resolver.js';
2
+ /**
3
+ * 이 트윈의 원본에 관계를 물어본다.
4
+ *
5
+ * 물어볼 수 없는 경우가 여럿이고 **전부 빈 배열입니다** — 원본을 모르는 트윈, 등록되지 않은 어댑터,
6
+ * `fetchLinks` 를 구현하지 않은 어댑터. 이 셋은 결함이 아닙니다. 가상 원본과 시뮬레이터는 조회할
7
+ * 관계가 없습니다.
8
+ *
9
+ * 실패는 다릅니다 — 예외를 그대로 올립니다. 호출하는 쪽(`externalIncoming`)이 잡아서 경고를 남기고
10
+ * 나머지 관계는 그대로 표시합니다.
11
+ */
12
+ export declare function fetchReferenceLinks(domainId: string, instanceId: string, axis: string, itemId: string): Promise<ExternalIncoming[]>;
13
+ /** 부팅에서 한 번 등록한다. */
14
+ export declare function installReferenceLinkResolver(): void;
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fetchReferenceLinks = fetchReferenceLinks;
4
+ exports.installReferenceLinkResolver = installReferenceLinkResolver;
5
+ /*
6
+ * **원본에만 있는 관계를 조회해서 트윈 상세 화면에 붙인다.**
7
+ *
8
+ * ── 무엇을 해결하나 ────────────────────────────────────────────────────────
9
+ * 설비 상세 화면에서 사람이 실제로 묻는 것은 「이 설비로 무엇을 얼마나 만들었나」, 「지금 정비
10
+ * 중인가」, 「예정된 정비가 있나」다. 그 정보는 트윈에 없고 MES 에 있다.
11
+ *
12
+ * 트윈으로 복제하면 원본의 테이블을 전부 미러링하게 됩니다. 조회용 참조는 복제하지 않고 필요할 때
13
+ * 물어보는 것이 맞습니다.
14
+ *
15
+ * ── 왜 프로세스가 나뉘어 있는가 ────────────────────────────────────────────
16
+ * 측정 결과입니다(2026-09-06).
17
+ *
18
+ * operato-plant 포트 4000 자기 DB
19
+ * operato-twin 포트 3000 자기 DB
20
+ *
21
+ * 별개 프로세스이므로 plant 안에서 `registerExternalResolver` 를 불러도 twin 프로세스에는 등록되지
22
+ * 않습니다. 그래서 resolver 는 twin 안에 있고, 본체는 어댑터를 통해 원본에 HTTP 로 물어봅니다.
23
+ *
24
+ * 주소와 인증은 이미 연결 설정(`TwinReference.connectionConfig`)에 있습니다. 새로 만들 것이 없습니다.
25
+ *
26
+ * ── 왜 부팅에 한 번만 등록하나 ──────────────────────────────────────────────
27
+ * 연결은 나중에 추가될 수 있습니다. 연결마다 resolver 를 등록하면 새 연결이 생길 때 재기동해야
28
+ * 합니다. resolver 하나가 호출될 때 연결을 조회하므로 재기동이 필요 없습니다.
29
+ */
30
+ const shell_1 = require("@things-factory/shell");
31
+ const twin_instance_js_1 = require("../twin-instance/twin-instance.js");
32
+ const twin_reference_js_1 = require("./twin-reference.js");
33
+ const reference_adapter_js_1 = require("./reference-adapter.js");
34
+ const external_resolver_js_1 = require("../twin-model/external-resolver.js");
35
+ const reference_link_groups_js_1 = require("./reference-link-groups.js");
36
+ /**
37
+ * 이 트윈의 원본에 관계를 물어본다.
38
+ *
39
+ * 물어볼 수 없는 경우가 여럿이고 **전부 빈 배열입니다** — 원본을 모르는 트윈, 등록되지 않은 어댑터,
40
+ * `fetchLinks` 를 구현하지 않은 어댑터. 이 셋은 결함이 아닙니다. 가상 원본과 시뮬레이터는 조회할
41
+ * 관계가 없습니다.
42
+ *
43
+ * 실패는 다릅니다 — 예외를 그대로 올립니다. 호출하는 쪽(`externalIncoming`)이 잡아서 경고를 남기고
44
+ * 나머지 관계는 그대로 표시합니다.
45
+ */
46
+ async function fetchReferenceLinks(domainId, instanceId, axis, itemId) {
47
+ const ref = await (0, shell_1.getRepository)(twin_reference_js_1.TwinReference).findOne({ where: { domain: { id: domainId }, source: instanceId } });
48
+ if (!ref)
49
+ return [];
50
+ const adapter = (0, reference_adapter_js_1.getAdapter)(ref.adapterType);
51
+ if (!adapter?.fetchLinks)
52
+ return [];
53
+ /*
54
+ * 사이트를 함께 넘깁니다 — 원본 하나가 여러 사이트를 낼 수 있습니다(`discoverSites`). 이 트윈이
55
+ * 어느 사이트에서 왔는지는 `origin.siteId` 에 있고, 없으면 넘기지 않습니다.
56
+ */
57
+ const inst = await (0, shell_1.getRepository)(twin_instance_js_1.TwinInstance).findOne({ where: { domain: { id: domainId }, instanceId } });
58
+ const siteId = inst?.origin?.siteId;
59
+ const site = siteId ? { siteId } : undefined;
60
+ return (0, reference_link_groups_js_1.usableLinkGroups)(await adapter.fetchLinks(ref.connectionConfig ?? {}, site, axis, itemId));
61
+ }
62
+ /** 부팅에서 한 번 등록한다. */
63
+ function installReferenceLinkResolver() {
64
+ (0, external_resolver_js_1.registerExternalResolver)({
65
+ /*
66
+ * `entity` 는 계약의 `target.entity` 와 맞추는 값입니다. 이 resolver 는 들어오는 관계만 답하고
67
+ * 특정 관계에 붙지 않으므로, 계약에 없는 이름을 씁니다.
68
+ */
69
+ entity: 'reference.links',
70
+ incoming: (domainId, instanceId, axis, itemId) => fetchReferenceLinks(domainId, instanceId, axis, itemId)
71
+ });
72
+ }
73
+ //# sourceMappingURL=reference-links.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reference-links.js","sourceRoot":"","sources":["../../../server/service/reference/reference-links.ts"],"names":[],"mappings":";;AA2CA,kDAqBC;AAGD,oEASC;AA5ED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,iDAAqD;AAErD,wEAAgE;AAChE,2DAAmD;AACnD,iEAAwE;AACxE,6EAAoG;AACpG,yEAA6D;AAE7D;;;;;;;;;GASG;AACI,KAAK,UAAU,mBAAmB,CACvC,QAAgB,EAChB,UAAkB,EAClB,IAAY,EACZ,MAAc;IAEd,MAAM,GAAG,GAAG,MAAM,IAAA,qBAAa,EAAC,iCAAa,CAAC,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,EAAE,CAAC,CAAA;IACnH,IAAI,CAAC,GAAG;QAAE,OAAO,EAAE,CAAA;IAEnB,MAAM,OAAO,GAAG,IAAA,iCAAU,EAAC,GAAG,CAAC,WAAW,CAAC,CAAA;IAC3C,IAAI,CAAC,OAAO,EAAE,UAAU;QAAE,OAAO,EAAE,CAAA;IAEnC;;;OAGG;IACH,MAAM,IAAI,GAAG,MAAM,IAAA,qBAAa,EAAC,+BAAY,CAAC,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,UAAU,EAAE,EAAE,CAAC,CAAA;IAC3G,MAAM,MAAM,GAAI,IAAI,EAAE,MAAc,EAAE,MAAM,CAAA;IAC5C,MAAM,IAAI,GAA+B,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,SAAS,CAAA;IAExE,OAAO,IAAA,2CAAgB,EAAC,MAAM,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,gBAAgB,IAAI,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC,CAAA;AACnG,CAAC;AAED,qBAAqB;AACrB,SAAgB,4BAA4B;IAC1C,IAAA,+CAAwB,EAAC;QACvB;;;WAGG;QACH,MAAM,EAAE,iBAAiB;QACzB,QAAQ,EAAE,CAAC,QAAQ,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC,mBAAmB,CAAC,QAAQ,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,CAAC;KAC1G,CAAC,CAAA;AACJ,CAAC","sourcesContent":["/*\n * **원본에만 있는 관계를 조회해서 트윈 상세 화면에 붙인다.**\n *\n * ── 무엇을 해결하나 ────────────────────────────────────────────────────────\n * 설비 상세 화면에서 사람이 실제로 묻는 것은 「이 설비로 무엇을 얼마나 만들었나」, 「지금 정비\n * 중인가」, 「예정된 정비가 있나」다. 그 정보는 트윈에 없고 MES 에 있다.\n *\n * 트윈으로 복제하면 원본의 테이블을 전부 미러링하게 됩니다. 조회용 참조는 복제하지 않고 필요할 때\n * 물어보는 것이 맞습니다.\n *\n * ── 왜 프로세스가 나뉘어 있는가 ────────────────────────────────────────────\n * 측정 결과입니다(2026-09-06).\n *\n * operato-plant 포트 4000 자기 DB\n * operato-twin 포트 3000 자기 DB\n *\n * 별개 프로세스이므로 plant 안에서 `registerExternalResolver` 를 불러도 twin 프로세스에는 등록되지\n * 않습니다. 그래서 resolver 는 twin 안에 있고, 본체는 어댑터를 통해 원본에 HTTP 로 물어봅니다.\n *\n * 주소와 인증은 이미 연결 설정(`TwinReference.connectionConfig`)에 있습니다. 새로 만들 것이 없습니다.\n *\n * ── 왜 부팅에 한 번만 등록하나 ──────────────────────────────────────────────\n * 연결은 나중에 추가될 수 있습니다. 연결마다 resolver 를 등록하면 새 연결이 생길 때 재기동해야\n * 합니다. resolver 하나가 호출될 때 연결을 조회하므로 재기동이 필요 없습니다.\n */\nimport { getRepository } from '@things-factory/shell'\n\nimport { TwinInstance } from '../twin-instance/twin-instance.js'\nimport { TwinReference } from './twin-reference.js'\nimport { getAdapter, type SiteDescriptor } from './reference-adapter.js'\nimport { registerExternalResolver, type ExternalIncoming } from '../twin-model/external-resolver.js'\nimport { usableLinkGroups } from './reference-link-groups.js'\n\n/**\n * 이 트윈의 원본에 관계를 물어본다.\n *\n * 물어볼 수 없는 경우가 여럿이고 **전부 빈 배열입니다** — 원본을 모르는 트윈, 등록되지 않은 어댑터,\n * `fetchLinks` 를 구현하지 않은 어댑터. 이 셋은 결함이 아닙니다. 가상 원본과 시뮬레이터는 조회할\n * 관계가 없습니다.\n *\n * 실패는 다릅니다 — 예외를 그대로 올립니다. 호출하는 쪽(`externalIncoming`)이 잡아서 경고를 남기고\n * 나머지 관계는 그대로 표시합니다.\n */\nexport async function fetchReferenceLinks(\n domainId: string,\n instanceId: string,\n axis: string,\n itemId: string\n): Promise<ExternalIncoming[]> {\n const ref = await getRepository(TwinReference).findOne({ where: { domain: { id: domainId }, source: instanceId } })\n if (!ref) return []\n\n const adapter = getAdapter(ref.adapterType)\n if (!adapter?.fetchLinks) return []\n\n /*\n * 사이트를 함께 넘깁니다 — 원본 하나가 여러 사이트를 낼 수 있습니다(`discoverSites`). 이 트윈이\n * 어느 사이트에서 왔는지는 `origin.siteId` 에 있고, 없으면 넘기지 않습니다.\n */\n const inst = await getRepository(TwinInstance).findOne({ where: { domain: { id: domainId }, instanceId } })\n const siteId = (inst?.origin as any)?.siteId\n const site: SiteDescriptor | undefined = siteId ? { siteId } : undefined\n\n return usableLinkGroups(await adapter.fetchLinks(ref.connectionConfig ?? {}, site, axis, itemId))\n}\n\n/** 부팅에서 한 번 등록한다. */\nexport function installReferenceLinkResolver(): void {\n registerExternalResolver({\n /*\n * `entity` 는 계약의 `target.entity` 와 맞추는 값입니다. 이 resolver 는 들어오는 관계만 답하고\n * 특정 관계에 붙지 않으므로, 계약에 없는 이름을 씁니다.\n */\n entity: 'reference.links',\n incoming: (domainId, instanceId, axis, itemId) => fetchReferenceLinks(domainId, instanceId, axis, itemId)\n })\n}\n"]}
@@ -1,4 +1,5 @@
1
1
  import { type ConnectionConfig, type SiteDescriptor } from './reference-adapter.js';
2
+ export { liveFeedLeaseName } from './live-feed-lease.js';
2
3
  /**
3
4
  * 커넥터 라이브 기동 — 어댑터 openLiveFeed 를 인스턴스에 결선.
4
5
  * 반환: 라이브 결선 성공 여부(어댑터가 openLiveFeed 미지원이면 false → 호출측이 sim 폴백).
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.liveFeedLeaseName = void 0;
3
4
  exports.startReferenceLiveFeed = startReferenceLiveFeed;
4
5
  exports.stopReferenceLiveFeed = stopReferenceLiveFeed;
5
6
  exports.listReferenceLiveFeeds = listReferenceLiveFeeds;
@@ -7,6 +8,12 @@ exports.resumeReferenceLiveFeeds = resumeReferenceLiveFeeds;
7
8
  const log_js_1 = require("../../engine/log.js");
8
9
  const shell_1 = require("@things-factory/shell");
9
10
  const reference_adapter_js_1 = require("./reference-adapter.js");
11
+ /* 이름 짓는 규칙은 가벼운 파일에 있다 — 테스트가 붙게(§`live-feed-lease.ts`). */
12
+ const live_feed_lease_js_1 = require("./live-feed-lease.js");
13
+ var live_feed_lease_js_2 = require("./live-feed-lease.js");
14
+ Object.defineProperty(exports, "liveFeedLeaseName", { enumerable: true, get: function () { return live_feed_lease_js_2.liveFeedLeaseName; } });
15
+ const command_store_js_1 = require("../actuation/command-store.js");
16
+ const settle_effects_js_1 = require("../actuation/settle-effects.js");
10
17
  const index_js_1 = require("../../engine/index.js");
11
18
  const twin_instance_js_1 = require("../twin-instance/twin-instance.js");
12
19
  const twin_reference_js_1 = require("./twin-reference.js");
@@ -52,10 +59,20 @@ source) {
52
59
  * 동작한다. 그리고 커널이 이미 갈라 두었다: 「원천이 애초에 다시 말해 주지 않는 축」은 재기동
53
60
  * 연속성으로 이어받는다(§`hydrateContinuity`) — 읽기 커서가 정확히 그 성질이다.
54
61
  *
55
- * 없으면 `undefined` 를 건넨다 — 그것이 「첫 붙음」이라는 사실이고, 그때만 어댑터가 되돌아볼 날수로
56
- * 첫 창을 만든다. 빈 객체로 메우지 않는다(그러면 첫 붙음과 이어 붙음이 같아진다).
62
+ * The object itself is handed over even on a first attach, because the feed needs its lease name
63
+ * before it has read anything. `cursor` staying absent is what says "first attach" — which is how
64
+ * every connector already reads it (`continuity?.cursor`), so nothing changes for them. What is
65
+ * never filled in with an empty value is the cursor: an empty one would make a first attach look
66
+ * like a resumed one, and the adapter would build no look-back window.
67
+ */
68
+ const carried = await loadLiveCursor(domainId, instanceId, source);
69
+ const continuity = { ...carried, leaseName: (0, live_feed_lease_js_1.liveFeedLeaseName)(domainId, instanceId, source) };
70
+ /*
71
+ * The lease name is composed here, not in the adapter, because it has to be unique across
72
+ * domains and an adapter is handed neither `domainId` nor `instanceId` (§`LiveFeedContinuity`).
73
+ * Built from `source` rather than `site.siteId`: one reference is one connection to one system,
74
+ * and that is the thing a second instance must not open twice.
57
75
  */
58
- const continuity = await loadLiveCursor(domainId, instanceId, source);
59
76
  /*
60
77
  * ── **트윈이 아는 것을 함께 건넨다** (2026-08-28) ───────────────────────────
61
78
  *
@@ -65,8 +82,7 @@ source) {
65
82
  * 트윈은 저장본에서 되세우므로 잃지 않았다. 그래서 그 사실을 건네면 첫 주기부터 바뀐 것만 나간다.
66
83
  * 커널 어휘로 건넨다 — 원본 낱말로 되돌리는 일은 여러 낱말이 한 값으로 모여 불가능하고, 필요도 없다.
67
84
  */
68
- if (continuity)
69
- continuity.observedItems = observedItemsOf(domainId, instanceId);
85
+ continuity.observedItems = observedItemsOf(domainId, instanceId);
70
86
  const unsubscribe = adapter.openLiveFeed(cfg, site, (records) => {
71
87
  if (!index_js_1.TwinEngine.owns(domainId, instanceId)) {
72
88
  stopReferenceLiveFeed(instanceId); // 인스턴스 사라지면 자가 정지(안전망)
@@ -94,6 +110,11 @@ source) {
94
110
  */
95
111
  /* 어떤 길로 들어왔는지 함께 적는다 — 화면이 「이 트윈은 무엇을 통해 받나」를 말할 수 있게. */
96
112
  const applied = accepted.length ? index_js_1.TwinEngine.ingestLive(domainId, instanceId, accepted, 'connector') : 0;
113
+ /*
114
+ * **나간 지시와 잇는다** — 폐루프. 훅과 같은 자리다(§`routes.ts`). 기다리지 않는다: 라이브 묶음
115
+ * 처리가 이것 때문에 늦으면 뒤의 묶음이 밀리고, 밀린 것이 크래시로 이어진 적이 있다.
116
+ */
117
+ void (0, settle_effects_js_1.settleEffects)({ domainId, envelopes: accepted, store: (0, command_store_js_1.databaseCommandStore)() });
97
118
  const undelivered = accepted.length - applied;
98
119
  /*
99
120
  * **거부한 것을 알리지 않고 버리지 않는다.** 예전에는 `rejected` 를 받지도 않았다 — 커넥터의 매핑이
@@ -155,7 +176,12 @@ function observedItemsOf(domainId, instanceId) {
155
176
  */
156
177
  async function loadLiveCursor(domainId,
157
178
  /** 읽기 실패를 적을 트윈 — 커서는 레퍼런스의 것이지만 **건강은 인스턴스의 것**이다(한 레퍼런스가 여럿을 낳는다). */
158
- instanceId, source) {
179
+ instanceId, source
180
+ /*
181
+ * `leaseName` is not this function's to know — it is composed from the domain and instance by the
182
+ * caller. Returning the rest keeps that rule in one place instead of two.
183
+ */
184
+ ) {
159
185
  if (!source) {
160
186
  /* 원본을 가리킬 수 없으면 저장할 자리도 없다 — 그 사실을 말한다(알리지 않고 커서 없이 돌지 않게). */
161
187
  (0, log_js_1.twinWarn)('[twin-live] live cursor not carried — the caller did not say which reference this feed reads from');
@@ -1 +1 @@
1
- {"version":3,"file":"reference-live.js","sourceRoot":"","sources":["../../../server/service/reference/reference-live.ts"],"names":[],"mappings":"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","sourcesContent":["import { twinLog, twinWarn, twinError } from '../../engine/log.js'\nimport { getRepository } from '@things-factory/shell'\n\nimport { getAdapter, type ConnectionConfig, type LiveFeedContinuity, type ObservedItemFact, type SiteDescriptor } from './reference-adapter.js'\nimport { TwinEngine, ingestCanonicalRecords, registerLiveFeedProbe } from '../../engine/index.js'\nimport { type CanonicalRecord } from '@operato/ops-contract'\nimport { TwinInstance } from '../twin-instance/twin-instance.js'\nimport { TwinReference } from './twin-reference.js'\n\n/*\n * 커넥터 기반 라이브 피드 결선(reference → live, §0 프레임 ① restartPolicy='resync').\n * 흐름: adapter.openLiveFeed(cfg, site, records) → ingestCanonicalRecords(정규→봉투) → TwinEngine.ingestLive(projector 미러).\n * 커넥터가 \"벤더 이벤트 → 정규 레코드\" 매핑을 소유(코드)한다 — preset 기반 startLiveFeed(operato-twin, http-get+jsonata)와\n * 대칭이되, 매핑 위치만 다르다. 실 시스템 전환은 connectionConfig.baseUrl 교체뿐(코드 경로 동일).\n * 생명주기: 인스턴스 정지(TwinEngine.stop)에 훅으로 묶여 피드도 함께 unsubscribe.\n */\n\n/** 활성 라이브 피드 — instanceId → unsubscribe(어댑터 openLiveFeed 반환). */\nconst feeds = new Map<string, () => void>()\n\n/**\n * 커넥터 라이브 기동 — 어댑터 openLiveFeed 를 인스턴스에 결선.\n * 반환: 라이브 결선 성공 여부(어댑터가 openLiveFeed 미지원이면 false → 호출측이 sim 폴백).\n */\nexport async function startReferenceLiveFeed(\n domainId: string,\n adapterType: string,\n cfg: ConnectionConfig,\n site: SiteDescriptor,\n instanceId: string,\n /**\n * **어느 레퍼런스에서 읽는 피드인가** — 읽기 커서를 저장할 자리를 가리킨다(§`loadLiveCursor`).\n *\n * 부르는 쪽이 `TwinReference` 를 이미 들고 있으므로 그 이름을 넘긴다. 처음에는 `cfg`·`site` 에서\n * 찾으려 했는데 **둘 다 그 값을 가질 수 없다**: `cfg` 는 사람이 화면에서 입력한 접속 설정이고\n * (`endpoint`·`token`…), `site` 는 커넥터가 원본에서 발견한 것이다 — **레퍼런스의 이름은 호스트가\n * 붙이는 것이고 원본은 그것을 모른다.** 커넥터가 채울 수 없는 값을 커넥터에서 읽으려 한 것이 잘못이었다.\n *\n * 주지 않으면 커서를 잇지 않고 **그 사실을 말한다**(알리지 않고 창이 미끄러지지 않게).\n */\n source?: string\n): Promise<boolean> {\n const adapter = getAdapter(adapterType)\n if (!adapter?.openLiveFeed) return false\n\n const det = await TwinEngine.detail(domainId, instanceId)\n if (!det?.model) throw new Error(`instance \"${instanceId}\" not provisioned (no model) — ingest master before live start`)\n\n // 관측 기동 — 커널 tick 없이 projector 가 실 이벤트를 반영한다(startLive 가 restartPolicy='resync' 로 적는다).\n // 시각 기준은 **공간**이 갖는다(테넌트 아님) — 교대의 HH:MM 을 어느 기준으로 읽나.\n TwinEngine.startLive(instanceId, domainId, det.kind, await TwinEngine.withSpaceTimeBase(det.model, domainId))\n\n stopReferenceLiveFeed(instanceId) // 재기동 시 이전 피드 정리\n /*\n * **어디까지 읽었나를 이 층이 들고 건넨다**(§`LiveFeedContinuity`, 2026-08-23).\n *\n * 어댑터가 이 값을 자기 사물함에 두면 원본이 늘 때마다 저장 기제가 늘고, 그중 하나가 알리지 않고 다르게\n * 동작한다. 그리고 커널이 이미 갈라 두었다: 「원천이 애초에 다시 말해 주지 않는 축」은 재기동\n * 연속성으로 이어받는다(§`hydrateContinuity`) — 읽기 커서가 정확히 그 성질이다.\n *\n * 없으면 `undefined` 를 건넨다 — 그것이 「첫 붙음」이라는 사실이고, 그때만 어댑터가 되돌아볼 날수로\n * 첫 창을 만든다. 빈 객체로 메우지 않는다(그러면 첫 붙음과 이어 붙음이 같아진다).\n */\n const continuity = await loadLiveCursor(domainId, instanceId, source)\n /*\n * ── **트윈이 아는 것을 함께 건넨다** (2026-08-28) ───────────────────────────\n *\n * 전량 절대값을 읽는 커넥터는 지난 주기와 견주어 바뀐 것만 낸다. 그 비교 표가 프로세스 안에만\n * 있어서 재기동하면 커넥터가 전량을 「바뀐 것」으로 판정했다 — 승화푸드에서 2,665건이다.\n *\n * 트윈은 저장본에서 되세우므로 잃지 않았다. 그래서 그 사실을 건네면 첫 주기부터 바뀐 것만 나간다.\n * 커널 어휘로 건넨다 — 원본 낱말로 되돌리는 일은 여러 낱말이 한 값으로 모여 불가능하고, 필요도 없다.\n */\n if (continuity) continuity.observedItems = observedItemsOf(domainId, instanceId)\n const unsubscribe = adapter.openLiveFeed(cfg, site, (records: unknown[]) => {\n if (!TwinEngine.owns(domainId, instanceId)) {\n stopReferenceLiveFeed(instanceId) // 인스턴스 사라지면 자가 정지(안전망)\n return\n }\n const offered = Array.isArray(records) ? records.length : records ? 1 : 0\n const { accepted, rejected, masterData } = ingestCanonicalRecords(\n records as CanonicalRecord[],\n domainId,\n new Date().toISOString(),\n TwinEngine.factScope(domainId, instanceId)\n )\n /*\n * ── **사건이 아닌 것은 상태만 세운다** (2026-08-28) ─────────────────────────\n *\n * 마스터데이터 — 변하지 않는 속성(유통기한·로트번호). 시각축이 없는 값이므로 지난 기록에 적지\n * 않는다(§`applyMasterData`). 이 문이 없던 동안 그 값은 사건에 실려 왔고, 표준이 그것을\n * 「들어왔다」에만 허용하므로 전량을 읽는 원본은 **들어오지 않은 것을 들어왔다고** 말해야 했다.\n *\n * 반영된 건수를 함께 센다 — 라이브로 돌지 않는 트윈은 0 을 내므로, 그 차이가 「바닥에 버려진 수」다.\n */\n const masterApplied = masterData.length ? TwinEngine.applyMasterData(domainId, instanceId, masterData) : 0\n if (masterData.length && masterApplied < masterData.length) {\n twinWarn(\n `[twin-live] \"${instanceId}\": ${masterData.length - masterApplied}/${masterData.length} master data element(s) NOT applied — ` +\n 'the twin is not running live; lot attributes such as expiry will stay empty.'\n )\n }\n /*\n * 커널이 **몇 건을 실제로 받았나**를 함께 본다. 트윈이 라이브로 돌지 않으면 `ingestLive` 가 0 을\n * 돌려주고, 그 차이가 곧 「바닥에 버려진 수」다. 이것을 통과로 세면 멈춘 트윈이 100% 로 보인다.\n */\n /* 어떤 길로 들어왔는지 함께 적는다 — 화면이 「이 트윈은 무엇을 통해 받나」를 말할 수 있게. */\n const applied = accepted.length ? TwinEngine.ingestLive(domainId, instanceId, accepted, 'connector') : 0\n const undelivered = accepted.length - applied\n /*\n * **거부한 것을 알리지 않고 버리지 않는다.** 예전에는 `rejected` 를 받지도 않았다 — 커넥터의 매핑이\n * 틀리면 트윈은 「아무 일도 일어나지 않는」 모습이 되고, 로그에도 아무 흔적이 없었다.\n *\n * 그다음 자리였던 「화면까지 나르기」를 이제 채운다. 건마다 브로드캐스팅하지 않고 **집계**한다 — 통과분까지\n * 함께 세는 이유는, 거부만 세면 「유입 없음」과 「정상」이 둘 다 거부 0 으로 같아 보이기 때문이다.\n * 조회는 `twinIngestHealth`.\n */\n /*\n * **읽었다는 것 자체가 사실이다** — 빈 읽기도 성공이므로 단절 기록을 지운다(§`onReadFailure`).\n * 그 둘을 섞으면 들어온 것이 없는 연결이 끊긴 원본으로 보인다.\n */\n TwinEngine.clearIngestReadFailure(domainId, instanceId)\n TwinEngine.recordIngestResult(domainId, instanceId, offered, rejected, Date.now(), undelivered)\n if (rejected.length) {\n const why = rejected[0]?.errors?.join(' · ') ?? 'unknown'\n twinWarn(`[twin-live] \"${instanceId}\": ${rejected.length}/${offered} record(s) rejected — e.g. ${why}`)\n }\n /* 버려진 것은 로그에도 남긴다 — 화면을 보지 않는 사람도 알아야 한다(사실이 사라지는 중이다). */\n if (undelivered > 0) {\n twinWarn(\n `[twin-live] \"${instanceId}\": ${undelivered}/${offered} record(s) NOT delivered — the twin is not running live; ` +\n `records are being discarded. Start the twin, or disconnect the feed.`\n )\n }\n }, continuity)\n feeds.set(instanceId, unsubscribe)\n return true\n}\n\n/**\n * 트윈이 지금 들고 있는 물품을 커널 어휘로 낸다 — 없으면 `undefined`.\n *\n * 상태가 없으면(트윈이 아직 아무것도 모르면) `undefined` 다. 빈 배열로 메우지 않는다 — 「모른다」와\n * 「하나도 없다」는 다른 사실이고, 커넥터가 그 둘에 다르게 반응해야 한다.\n */\nfunction observedItemsOf(domainId: string, instanceId: string): ObservedItemFact[] | undefined {\n const state = TwinEngine.snapshot(domainId, instanceId)\n const items = (state as any)?.items\n if (!Array.isArray(items)) return undefined\n return items.map((i: any) => ({\n id: String(i?.subLotId ?? i?.epc ?? ''),\n ...(i?.gtin ? { epcClass: String(i.gtin) } : {}),\n ...(i?.location ? { location: String(i.location) } : {}),\n ...(typeof i?.qty === 'number' ? { qty: i.qty } : {}),\n ...(i?.uom ? { uom: String(i.uom) } : {}),\n ...(i?.disposition ? { disposition: String(i.disposition) } : {}),\n ...(typeof i?.expiry === 'number' ? { expiry: i.expiry } : {}),\n ...(i?.lot ? { lot: String(i.lot) } : {})\n })).filter((i: ObservedItemFact) => !!i.id)\n}\n\n/**\n * 그 원본의 읽기 커서를 불러오고, 움직이면 저장하는 통로를 만든다.\n *\n * 커서는 **원본(`TwinReference`)의 것**이지 인스턴스의 것이 아니다 — 한 원본이 여러 인스턴스를 낳고\n * (`scopeSpec.produced`) 읽는 것은 한 번이다. 인스턴스마다 커서를 두면 같은 표를 여러 번 읽는다.\n *\n * 저장 실패는 **삼키지 않고 말한다.** 오류 없이 실패하면 다음 재기동에서 창이 다시 미끄러지고, 그 손실은\n * 오류 없이 사라진다 — 이 컬럼을 만든 이유가 바로 그것이다.\n */\nasync function loadLiveCursor(\n domainId: string,\n /** 읽기 실패를 적을 트윈 — 커서는 레퍼런스의 것이지만 **건강은 인스턴스의 것**이다(한 레퍼런스가 여럿을 낳는다). */\n instanceId: string,\n source?: string\n): Promise<LiveFeedContinuity | undefined> {\n if (!source) {\n /* 원본을 가리킬 수 없으면 저장할 자리도 없다 — 그 사실을 말한다(알리지 않고 커서 없이 돌지 않게). */\n twinWarn('[twin-live] live cursor not carried — the caller did not say which reference this feed reads from')\n return undefined\n }\n const repo = getRepository(TwinReference)\n const row = await repo.findOne({ where: { domain: { id: domainId } as any, source } }).catch(() => null)\n if (!row) {\n twinWarn(`[twin-live] live cursor not carried — no reference row for source \"${source}\"`)\n return undefined\n }\n const stored = row.liveCursor\n if (stored?.firstAttachedAt) {\n twinLog(`[twin-live] \"${source}\": resuming from stored cursor — knowledge starts ${stored.firstAttachedAt}`)\n }\n return {\n cursor: stored ?? undefined,\n /*\n * **원본에 닿지 못했다** — 어댑터가 알리면 상태에 적는다(§`onReadFailure`).\n *\n * 로그로는 어댑터가 이미 말하고 있었다(재시도 경고). 그러나 **로그는 사람이 볼 때만 값이 있다** —\n * 화면이 말하려면 조회 가능한 상태에 있어야 한다.\n *\n * ── `status` 를 건드리지 않는다 (2026-08-23, 되돌린 자리) ────────────────────\n * 처음에는 레퍼런스 행의 `status` 를 `'error'` 로도 적었다 — 「목록이 「연결됨」이라 말하면서 실제로는\n * 못 읽는 상태를 없애려고」. **그것이 피드를 영구히 껐다.**\n *\n * 부팅 재부착은 `status: 'connected'` 인 레퍼런스만 되살린다(§`resumeReferenceLiveFeeds`). 그래서\n * 원본이 **잠깐** 끊긴 것이 그 행을 `error` 로 만들고, 다음 재기동에서 그 피드는 아예 붙지 않았다 —\n * 실측으로 그렇게 났다(09:25 판에 `[twin-live]` 줄이 한 줄도 없었다).\n *\n * 두 축을 한 칸에 섞은 것이 원인이다:\n * `status` **연결 설정이 성립하나** — 접속 시험·마스터 동기가 정하는 축(재부착의 자격이다)\n * 읽기 실패 **지금 닿나** — 오갈 수 있는 사실이고, 유입 장부가 그 축을 갖는다\n *\n * 그러니 여기서는 `lastError` 에만 적는다(사람이 목록에서 이유를 볼 수 있게). 「지금 닿나」는\n * `ingestHealth().readFailure` 가 답하고, 성공한 주기가 그것을 지운다.\n */\n onReadFailure: ({ reason, stream, nextRetryMs }) => {\n TwinEngine.recordIngestReadFailure(domainId, instanceId, reason, Date.now(), stream, nextRetryMs)\n twinWarn(`[twin-live] \"${source}\": cannot read from the source${stream ? ` (${stream})` : ''} — ${reason}`)\n repo\n .update({ id: row.id }, { lastError: `live: ${reason}` } as any)\n .catch(err => twinWarn(`[twin-live] \"${source}\": read failure not recorded — ${err?.message ?? err}`))\n },\n /*\n * **끊겼다가 돌아왔다** — 그 사실을 장부에 남긴다 (2026-08-27).\n *\n * 실패 기록을 지우기만 하면 「끊긴 적이 있었다」가 화면에서 사라진다. 그러면 밤새 두 시간 끊겼던\n * 연결과 한 번도 끊기지 않은 연결이 아침에 똑같이 보인다.\n *\n * `lastError` 도 함께 지운다 — 목록에 남은 옛 이유가 지금의 상태로 읽힌다.\n */\n onRecovered: ({ afterFailures, downMs }) => {\n TwinEngine.recordIngestRecovered(domainId, instanceId, afterFailures, downMs, Date.now())\n twinLog(\n `[twin-live] \"${source}\": reading again after ${afterFailures} failed attempt(s) — it was out for ${Math.round(downMs / 1000)}s`\n )\n repo\n .update({ id: row.id }, { lastError: null } as any)\n .catch(err => twinWarn(`[twin-live] \"${source}\": recovery not recorded — ${err?.message ?? err}`))\n },\n /**\n * **읽었는데 창을 넘길 수 없다** — 위와 조치가 반대인 사실이다 (2026-08-24).\n *\n * 이 문을 만들기 전에는 이 사실이 `onReadFailure` 로 나갔다. 그래서 화면이 「원본에 닿지 못한다」고\n * 말했는데 실제로는 원본이 **답한** 상태였고, 그 답의 모양이 커서를 이긴 것이었다. 사람은 원본을\n * 의심하고 기다리는데 기다림으로는 영원히 풀리지 않는다 — **원인을 반대 방향으로 가리켰다.**\n *\n * `status` 를 건드리지 않는 규율은 위와 같다(재부착의 자격이다). `lastError` 에는 적는다 — 목록에서\n * 이유를 볼 수 있어야 하고, 「닿지 못한다」와 다른 문장이어야 한다.\n */\n /*\n * **세우지 않은 것을 장부에 적는다** — 원본에 있는데 트윈에 세우지 않은 것(이유와 수).\n *\n * 로그로도 남기지만 **로그는 사람이 볼 때만 값이 있다.** 이 사실은 상태의 성질이라(세우지 않은 것은\n * 지금도 없다) 조회되는 자리에 있어야 한다 — 그러지 않으면 프로비저닝 화면에서 한 번 스쳐 지나간다.\n *\n * `status`·`lastError` 를 건드리지 않는다: 이것은 **실패가 아니다.** 어댑터가 옳은 판단으로 안 받은\n * 것이고, 오류로 적으면 화면이 원본을 의심하게 만든다.\n */\n onWithheld: ({ reason, count }) => {\n TwinEngine.recordIngestWithheld(domainId, instanceId, reason, count)\n },\n onCursorStall: ({ reason, stream }) => {\n TwinEngine.recordIngestCursorStall(domainId, instanceId, reason, Date.now(), stream)\n twinWarn(\n `[twin-live] \"${source}\": read the source but the cursor cannot advance` +\n `${stream ? ` (${stream})` : ''} — ${reason}. Waiting will not clear this.`\n )\n repo\n .update({ id: row.id }, { lastError: `live cursor stalled: ${reason}` } as any)\n .catch(err => twinWarn(`[twin-live] \"${source}\": cursor stall not recorded — ${err?.message ?? err}`))\n },\n onCursor: next => {\n /* 어댑터가 준 모양을 그대로 적는다 — 이 층은 흐름의 뜻을 모른다(열쇠는 어댑터가 정한다). */\n repo\n .update({ id: row.id }, { liveCursor: next } as any)\n .catch(err => twinWarn(`[twin-live] \"${source}\": live cursor not saved — ${err?.message ?? err}`))\n }\n }\n}\n\n/** 라이브 피드 중지(unsubscribe). */\nexport function stopReferenceLiveFeed(instanceId: string): boolean {\n const unsub = feeds.get(instanceId)\n if (!unsub) return false\n try { unsub() } catch { /* noop */ }\n feeds.delete(instanceId)\n return true\n}\n\n/** 활성 피드 목록(관측). */\nexport function listReferenceLiveFeeds(): string[] {\n return [...feeds.keys()]\n}\n\n/**\n * 부팅 때 **끊어진 피드를 다시 붙인다** — 도는 미러 트윈에 계측이 계속 들어오게.\n *\n * ── 무엇이 알리지 않고 끊겼나 (2026-08-14) ───────────────────────────────────────\n * `startReferenceLiveFeed` 를 부르는 곳은 `importTwinReference` **하나뿐**이었다. 그래서 서버를 한 번\n * 재기동하면 미러 트윈은 「도는 중」이라 말하면서 아무 계측도 받지 않았다 — 오류도 로그도 없이,\n * 사람은 「값이 안 변하네」로 알게 된다. 실 배포에서 재기동 한 번에 계측이 멈추는 셈이다.\n *\n * ── 되살릴 재료는 이미 영속돼 있다 ─────────────────────────────────────────\n * `TwinReference` 가 어댑터 종류·접속 설정·그 레퍼런스가 만든 인스턴스(`scopeSpec.produced`)를 들고 있다.\n * 사이트 서술자만 없는데(`hint` 는 남기지 않았다) **다시 찾아서** 얻는다(`discoverSites`) — 인제스트가\n * 걷는 것과 같은 길이므로 규칙이 갈라지지 않는다. 되살릴 인스턴스가 없으면 그 호출도 하지 않는다.\n *\n * 재개 대상은 **등록부가 도는 미러라고 말하는 것**뿐이다: 멈춘 트윈을 되살리면 사람이 멈춘 것을\n * 우리가 다시 켜는 것이고, 시뮬 트윈에 실 계측을 부으면 두 현실이 섞인다.\n */\nexport async function resumeReferenceLiveFeeds(): Promise<{ resumed: string[]; failed: { instanceId: string; error: string }[] }> {\n const resumed: string[] = []\n const failed: { instanceId: string; error: string }[] = []\n /*\n * **한 트윈에 피드는 하나다** (2026-08-18).\n *\n * 같은 사이트를 두 레퍼런스가 각각 인제스트하면 둘 다 그 인스턴스를 자기가 만든 것(`produced`)으로\n * 들고 있다. 기동 때 그 목록을 그대로 돌면 같은 트윈에 피드를 두 번 붙이는데, 붙이는 함수가 미러를\n * 다시 세우므로(`startLive`) **먼저 쌓인 수요 구간이 리셋된다** — 실제로 그렇게 났다\n * (로그: `reattached 2 feed(s): ems-plant-1, ems-plant-1`).\n *\n * 그러니 한 번만 붙이고, **두 레퍼런스가 같은 트윈을 주장한다는 사실은 말한다** — 알리지 않고 하나를\n * 고르면 어느 접속이 그 트윈을 먹이고 있는지 아무도 모르게 된다.\n */\n const attached = new Map<string, string>() // instanceId → 먼저 붙인 레퍼런스\n try {\n const refs = await getRepository(TwinReference).find({ where: { status: 'connected' }, relations: ['domain'] })\n for (const ref of refs) {\n const domainId = (ref as any).domain?.id ?? (ref as any).domainId\n const adapter = getAdapter(ref.adapterType)\n if (!domainId || !adapter?.openLiveFeed) continue // 관측만 하는 어댑터에는 되살릴 피드가 없다\n\n const produced: any[] = ((ref.scopeSpec as any)?.produced ?? []).filter((p: any) => p?.instanceId && p?.siteId)\n if (!produced.length) continue\n\n /* 등록부가 「도는 미러」라고 말하는 것만 고른다. */\n const rows = await getRepository(TwinInstance).find({\n where: produced.map((p: any) => ({ domain: { id: domainId }, instanceId: p.instanceId })) as any\n })\n const live = produced.filter((p: any) =>\n rows.some(r => r.instanceId === p.instanceId && r.status === 'running' && r.restartPolicy === 'resync')\n )\n if (!live.length) continue\n\n let sites: SiteDescriptor[]\n try {\n sites = await adapter.discoverSites(ref.connectionConfig ?? {})\n } catch (e: any) {\n /* 원 시스템이 아직 안 떴을 수 있다 — 지어내지 않고 그렇게 적는다(트윈은 계속 「도는 미러」다). */\n for (const p of live) failed.push({ instanceId: p.instanceId, error: `discover failed: ${e?.message ?? 'unknown'}` })\n continue\n }\n\n for (const p of live) {\n const site = sites.find(s => s.siteId === p.siteId)\n if (!site) {\n failed.push({ instanceId: p.instanceId, error: `site \"${p.siteId}\" no longer discoverable` })\n continue\n }\n const owner = attached.get(p.instanceId)\n if (owner) {\n /* 두 접속이 같은 트윈을 주장한다 — 먼저 붙은 것을 지키고, 그 사실을 남긴다(둘 다 붙이면 미러가 리셋된다). */\n failed.push({\n instanceId: p.instanceId,\n error: `also claimed by reference \"${ref.source}\" — kept the feed from \"${owner}\" (two connections feeding one twin would reset the mirror)`\n })\n continue\n }\n attached.set(p.instanceId, ref.source)\n try {\n const ok = await startReferenceLiveFeed(domainId, ref.adapterType, ref.connectionConfig ?? {}, site, p.instanceId, ref.source)\n if (ok) resumed.push(p.instanceId)\n else failed.push({ instanceId: p.instanceId, error: 'adapter has no openLiveFeed' })\n } catch (e: any) {\n failed.push({ instanceId: p.instanceId, error: e?.message ?? 'reattach failed' })\n }\n }\n }\n } catch (e: any) {\n twinError('[twin-live] feed resume scan failed', e?.message)\n }\n\n if (resumed.length) twinLog(`[twin-live] reattached ${resumed.length} feed(s): ${resumed.join(', ')}`)\n /* **알리지 않고 넘기지 않는다** — 붙지 않은 미러는 「도는데 아무것도 오지 않는」 트윈이다. */\n for (const f of failed) twinWarn(`[twin-live] \"${f.instanceId}\" feed NOT reattached — ${f.error}`)\n return { resumed, failed }\n}\n\n/*\n * 이 계층의 피드 장부를 등록한다 — 화면이 「끊김」을 말할 근거는 **묻는 자리 하나**여야 한다.\n * (프리셋 기반 피드는 앱이 자기 장부를 따로 등록한다. 한쪽만 보면 흐르는 트윈을 끊겼다고 부른다.)\n */\nregisterLiveFeedProbe(() => [...feeds.keys()])\n\n/* 인스턴스 정지 시 피드 자동 정리(생명주기 대칭). */\nTwinEngine.onStop(instanceId => stopReferenceLiveFeed(instanceId))\n"]}
1
+ {"version":3,"file":"reference-live.js","sourceRoot":"","sources":["../../../server/service/reference/reference-live.ts"],"names":[],"mappings":"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","sourcesContent":["import { twinLog, twinWarn, twinError } from '../../engine/log.js'\nimport { getRepository } from '@things-factory/shell'\n\nimport { getAdapter, type ConnectionConfig, type LiveFeedContinuity, type ObservedItemFact, type SiteDescriptor } from './reference-adapter.js'\n/* 이름 짓는 규칙은 가벼운 파일에 있다 — 테스트가 붙게(§`live-feed-lease.ts`). */\nimport { liveFeedLeaseName } from './live-feed-lease.js'\nexport { liveFeedLeaseName } from './live-feed-lease.js'\nimport { databaseCommandStore } from '../actuation/command-store.js'\nimport { settleEffects } from '../actuation/settle-effects.js'\nimport { TwinEngine, ingestCanonicalRecords, registerLiveFeedProbe } from '../../engine/index.js'\nimport { type CanonicalRecord } from '@operato/ops-contract'\nimport { TwinInstance } from '../twin-instance/twin-instance.js'\nimport { TwinReference } from './twin-reference.js'\n\n/*\n * 커넥터 기반 라이브 피드 결선(reference → live, §0 프레임 ① restartPolicy='resync').\n * 흐름: adapter.openLiveFeed(cfg, site, records) → ingestCanonicalRecords(정규→봉투) → TwinEngine.ingestLive(projector 미러).\n * 커넥터가 \"벤더 이벤트 → 정규 레코드\" 매핑을 소유(코드)한다 — preset 기반 startLiveFeed(operato-twin, http-get+jsonata)와\n * 대칭이되, 매핑 위치만 다르다. 실 시스템 전환은 connectionConfig.baseUrl 교체뿐(코드 경로 동일).\n * 생명주기: 인스턴스 정지(TwinEngine.stop)에 훅으로 묶여 피드도 함께 unsubscribe.\n */\n\n/** 활성 라이브 피드 — instanceId → unsubscribe(어댑터 openLiveFeed 반환). */\nconst feeds = new Map<string, () => void>()\n\n/**\n * 커넥터 라이브 기동 — 어댑터 openLiveFeed 를 인스턴스에 결선.\n * 반환: 라이브 결선 성공 여부(어댑터가 openLiveFeed 미지원이면 false → 호출측이 sim 폴백).\n */\nexport async function startReferenceLiveFeed(\n domainId: string,\n adapterType: string,\n cfg: ConnectionConfig,\n site: SiteDescriptor,\n instanceId: string,\n /**\n * **어느 레퍼런스에서 읽는 피드인가** — 읽기 커서를 저장할 자리를 가리킨다(§`loadLiveCursor`).\n *\n * 부르는 쪽이 `TwinReference` 를 이미 들고 있으므로 그 이름을 넘긴다. 처음에는 `cfg`·`site` 에서\n * 찾으려 했는데 **둘 다 그 값을 가질 수 없다**: `cfg` 는 사람이 화면에서 입력한 접속 설정이고\n * (`endpoint`·`token`…), `site` 는 커넥터가 원본에서 발견한 것이다 — **레퍼런스의 이름은 호스트가\n * 붙이는 것이고 원본은 그것을 모른다.** 커넥터가 채울 수 없는 값을 커넥터에서 읽으려 한 것이 잘못이었다.\n *\n * 주지 않으면 커서를 잇지 않고 **그 사실을 말한다**(알리지 않고 창이 미끄러지지 않게).\n */\n source?: string\n): Promise<boolean> {\n const adapter = getAdapter(adapterType)\n if (!adapter?.openLiveFeed) return false\n\n const det = await TwinEngine.detail(domainId, instanceId)\n if (!det?.model) throw new Error(`instance \"${instanceId}\" not provisioned (no model) — ingest master before live start`)\n\n // 관측 기동 — 커널 tick 없이 projector 가 실 이벤트를 반영한다(startLive 가 restartPolicy='resync' 로 적는다).\n // 시각 기준은 **공간**이 갖는다(테넌트 아님) — 교대의 HH:MM 을 어느 기준으로 읽나.\n TwinEngine.startLive(instanceId, domainId, det.kind, await TwinEngine.withSpaceTimeBase(det.model, domainId))\n\n stopReferenceLiveFeed(instanceId) // 재기동 시 이전 피드 정리\n /*\n * **어디까지 읽었나를 이 층이 들고 건넨다**(§`LiveFeedContinuity`, 2026-08-23).\n *\n * 어댑터가 이 값을 자기 사물함에 두면 원본이 늘 때마다 저장 기제가 늘고, 그중 하나가 알리지 않고 다르게\n * 동작한다. 그리고 커널이 이미 갈라 두었다: 「원천이 애초에 다시 말해 주지 않는 축」은 재기동\n * 연속성으로 이어받는다(§`hydrateContinuity`) — 읽기 커서가 정확히 그 성질이다.\n *\n * The object itself is handed over even on a first attach, because the feed needs its lease name\n * before it has read anything. `cursor` staying absent is what says \"first attach\" — which is how\n * every connector already reads it (`continuity?.cursor`), so nothing changes for them. What is\n * never filled in with an empty value is the cursor: an empty one would make a first attach look\n * like a resumed one, and the adapter would build no look-back window.\n */\n const carried = await loadLiveCursor(domainId, instanceId, source)\n const continuity: LiveFeedContinuity = { ...carried, leaseName: liveFeedLeaseName(domainId, instanceId, source) }\n /*\n * The lease name is composed here, not in the adapter, because it has to be unique across\n * domains and an adapter is handed neither `domainId` nor `instanceId` (§`LiveFeedContinuity`).\n * Built from `source` rather than `site.siteId`: one reference is one connection to one system,\n * and that is the thing a second instance must not open twice.\n */\n /*\n * ── **트윈이 아는 것을 함께 건넨다** (2026-08-28) ───────────────────────────\n *\n * 전량 절대값을 읽는 커넥터는 지난 주기와 견주어 바뀐 것만 낸다. 그 비교 표가 프로세스 안에만\n * 있어서 재기동하면 커넥터가 전량을 「바뀐 것」으로 판정했다 — 승화푸드에서 2,665건이다.\n *\n * 트윈은 저장본에서 되세우므로 잃지 않았다. 그래서 그 사실을 건네면 첫 주기부터 바뀐 것만 나간다.\n * 커널 어휘로 건넨다 — 원본 낱말로 되돌리는 일은 여러 낱말이 한 값으로 모여 불가능하고, 필요도 없다.\n */\n continuity.observedItems = observedItemsOf(domainId, instanceId)\n const unsubscribe = adapter.openLiveFeed(cfg, site, (records: unknown[]) => {\n if (!TwinEngine.owns(domainId, instanceId)) {\n stopReferenceLiveFeed(instanceId) // 인스턴스 사라지면 자가 정지(안전망)\n return\n }\n const offered = Array.isArray(records) ? records.length : records ? 1 : 0\n const { accepted, rejected, masterData } = ingestCanonicalRecords(\n records as CanonicalRecord[],\n domainId,\n new Date().toISOString(),\n TwinEngine.factScope(domainId, instanceId)\n )\n /*\n * ── **사건이 아닌 것은 상태만 세운다** (2026-08-28) ─────────────────────────\n *\n * 마스터데이터 — 변하지 않는 속성(유통기한·로트번호). 시각축이 없는 값이므로 지난 기록에 적지\n * 않는다(§`applyMasterData`). 이 문이 없던 동안 그 값은 사건에 실려 왔고, 표준이 그것을\n * 「들어왔다」에만 허용하므로 전량을 읽는 원본은 **들어오지 않은 것을 들어왔다고** 말해야 했다.\n *\n * 반영된 건수를 함께 센다 — 라이브로 돌지 않는 트윈은 0 을 내므로, 그 차이가 「바닥에 버려진 수」다.\n */\n const masterApplied = masterData.length ? TwinEngine.applyMasterData(domainId, instanceId, masterData) : 0\n if (masterData.length && masterApplied < masterData.length) {\n twinWarn(\n `[twin-live] \"${instanceId}\": ${masterData.length - masterApplied}/${masterData.length} master data element(s) NOT applied — ` +\n 'the twin is not running live; lot attributes such as expiry will stay empty.'\n )\n }\n /*\n * 커널이 **몇 건을 실제로 받았나**를 함께 본다. 트윈이 라이브로 돌지 않으면 `ingestLive` 가 0 을\n * 돌려주고, 그 차이가 곧 「바닥에 버려진 수」다. 이것을 통과로 세면 멈춘 트윈이 100% 로 보인다.\n */\n /* 어떤 길로 들어왔는지 함께 적는다 — 화면이 「이 트윈은 무엇을 통해 받나」를 말할 수 있게. */\n const applied = accepted.length ? TwinEngine.ingestLive(domainId, instanceId, accepted, 'connector') : 0\n\n /*\n * **나간 지시와 잇는다** — 폐루프. 훅과 같은 자리다(§`routes.ts`). 기다리지 않는다: 라이브 묶음\n * 처리가 이것 때문에 늦으면 뒤의 묶음이 밀리고, 밀린 것이 크래시로 이어진 적이 있다.\n */\n void settleEffects({ domainId, envelopes: accepted, store: databaseCommandStore() })\n const undelivered = accepted.length - applied\n /*\n * **거부한 것을 알리지 않고 버리지 않는다.** 예전에는 `rejected` 를 받지도 않았다 — 커넥터의 매핑이\n * 틀리면 트윈은 「아무 일도 일어나지 않는」 모습이 되고, 로그에도 아무 흔적이 없었다.\n *\n * 그다음 자리였던 「화면까지 나르기」를 이제 채운다. 건마다 브로드캐스팅하지 않고 **집계**한다 — 통과분까지\n * 함께 세는 이유는, 거부만 세면 「유입 없음」과 「정상」이 둘 다 거부 0 으로 같아 보이기 때문이다.\n * 조회는 `twinIngestHealth`.\n */\n /*\n * **읽었다는 것 자체가 사실이다** — 빈 읽기도 성공이므로 단절 기록을 지운다(§`onReadFailure`).\n * 그 둘을 섞으면 들어온 것이 없는 연결이 끊긴 원본으로 보인다.\n */\n TwinEngine.clearIngestReadFailure(domainId, instanceId)\n TwinEngine.recordIngestResult(domainId, instanceId, offered, rejected, Date.now(), undelivered)\n if (rejected.length) {\n const why = rejected[0]?.errors?.join(' · ') ?? 'unknown'\n twinWarn(`[twin-live] \"${instanceId}\": ${rejected.length}/${offered} record(s) rejected — e.g. ${why}`)\n }\n /* 버려진 것은 로그에도 남긴다 — 화면을 보지 않는 사람도 알아야 한다(사실이 사라지는 중이다). */\n if (undelivered > 0) {\n twinWarn(\n `[twin-live] \"${instanceId}\": ${undelivered}/${offered} record(s) NOT delivered — the twin is not running live; ` +\n `records are being discarded. Start the twin, or disconnect the feed.`\n )\n }\n }, continuity)\n feeds.set(instanceId, unsubscribe)\n return true\n}\n\n/**\n * 트윈이 지금 들고 있는 물품을 커널 어휘로 낸다 — 없으면 `undefined`.\n *\n * 상태가 없으면(트윈이 아직 아무것도 모르면) `undefined` 다. 빈 배열로 메우지 않는다 — 「모른다」와\n * 「하나도 없다」는 다른 사실이고, 커넥터가 그 둘에 다르게 반응해야 한다.\n */\nfunction observedItemsOf(domainId: string, instanceId: string): ObservedItemFact[] | undefined {\n const state = TwinEngine.snapshot(domainId, instanceId)\n const items = (state as any)?.items\n if (!Array.isArray(items)) return undefined\n return items.map((i: any) => ({\n id: String(i?.subLotId ?? i?.epc ?? ''),\n ...(i?.gtin ? { epcClass: String(i.gtin) } : {}),\n ...(i?.location ? { location: String(i.location) } : {}),\n ...(typeof i?.qty === 'number' ? { qty: i.qty } : {}),\n ...(i?.uom ? { uom: String(i.uom) } : {}),\n ...(i?.disposition ? { disposition: String(i.disposition) } : {}),\n ...(typeof i?.expiry === 'number' ? { expiry: i.expiry } : {}),\n ...(i?.lot ? { lot: String(i.lot) } : {})\n })).filter((i: ObservedItemFact) => !!i.id)\n}\n\n/**\n * 그 원본의 읽기 커서를 불러오고, 움직이면 저장하는 통로를 만든다.\n *\n * 커서는 **원본(`TwinReference`)의 것**이지 인스턴스의 것이 아니다 — 한 원본이 여러 인스턴스를 낳고\n * (`scopeSpec.produced`) 읽는 것은 한 번이다. 인스턴스마다 커서를 두면 같은 표를 여러 번 읽는다.\n *\n * 저장 실패는 **삼키지 않고 말한다.** 오류 없이 실패하면 다음 재기동에서 창이 다시 미끄러지고, 그 손실은\n * 오류 없이 사라진다 — 이 컬럼을 만든 이유가 바로 그것이다.\n */\nasync function loadLiveCursor(\n domainId: string,\n /** 읽기 실패를 적을 트윈 — 커서는 레퍼런스의 것이지만 **건강은 인스턴스의 것**이다(한 레퍼런스가 여럿을 낳는다). */\n instanceId: string,\n source?: string\n /*\n * `leaseName` is not this function's to know — it is composed from the domain and instance by the\n * caller. Returning the rest keeps that rule in one place instead of two.\n */\n): Promise<Omit<LiveFeedContinuity, 'leaseName'> | undefined> {\n if (!source) {\n /* 원본을 가리킬 수 없으면 저장할 자리도 없다 — 그 사실을 말한다(알리지 않고 커서 없이 돌지 않게). */\n twinWarn('[twin-live] live cursor not carried — the caller did not say which reference this feed reads from')\n return undefined\n }\n const repo = getRepository(TwinReference)\n const row = await repo.findOne({ where: { domain: { id: domainId } as any, source } }).catch(() => null)\n if (!row) {\n twinWarn(`[twin-live] live cursor not carried — no reference row for source \"${source}\"`)\n return undefined\n }\n const stored = row.liveCursor\n if (stored?.firstAttachedAt) {\n twinLog(`[twin-live] \"${source}\": resuming from stored cursor — knowledge starts ${stored.firstAttachedAt}`)\n }\n return {\n cursor: stored ?? undefined,\n /*\n * **원본에 닿지 못했다** — 어댑터가 알리면 상태에 적는다(§`onReadFailure`).\n *\n * 로그로는 어댑터가 이미 말하고 있었다(재시도 경고). 그러나 **로그는 사람이 볼 때만 값이 있다** —\n * 화면이 말하려면 조회 가능한 상태에 있어야 한다.\n *\n * ── `status` 를 건드리지 않는다 (2026-08-23, 되돌린 자리) ────────────────────\n * 처음에는 레퍼런스 행의 `status` 를 `'error'` 로도 적었다 — 「목록이 「연결됨」이라 말하면서 실제로는\n * 못 읽는 상태를 없애려고」. **그것이 피드를 영구히 껐다.**\n *\n * 부팅 재부착은 `status: 'connected'` 인 레퍼런스만 되살린다(§`resumeReferenceLiveFeeds`). 그래서\n * 원본이 **잠깐** 끊긴 것이 그 행을 `error` 로 만들고, 다음 재기동에서 그 피드는 아예 붙지 않았다 —\n * 실측으로 그렇게 났다(09:25 판에 `[twin-live]` 줄이 한 줄도 없었다).\n *\n * 두 축을 한 칸에 섞은 것이 원인이다:\n * `status` **연결 설정이 성립하나** — 접속 시험·마스터 동기가 정하는 축(재부착의 자격이다)\n * 읽기 실패 **지금 닿나** — 오갈 수 있는 사실이고, 유입 장부가 그 축을 갖는다\n *\n * 그러니 여기서는 `lastError` 에만 적는다(사람이 목록에서 이유를 볼 수 있게). 「지금 닿나」는\n * `ingestHealth().readFailure` 가 답하고, 성공한 주기가 그것을 지운다.\n */\n onReadFailure: ({ reason, stream, nextRetryMs }) => {\n TwinEngine.recordIngestReadFailure(domainId, instanceId, reason, Date.now(), stream, nextRetryMs)\n twinWarn(`[twin-live] \"${source}\": cannot read from the source${stream ? ` (${stream})` : ''} — ${reason}`)\n repo\n .update({ id: row.id }, { lastError: `live: ${reason}` } as any)\n .catch(err => twinWarn(`[twin-live] \"${source}\": read failure not recorded — ${err?.message ?? err}`))\n },\n /*\n * **끊겼다가 돌아왔다** — 그 사실을 장부에 남긴다 (2026-08-27).\n *\n * 실패 기록을 지우기만 하면 「끊긴 적이 있었다」가 화면에서 사라진다. 그러면 밤새 두 시간 끊겼던\n * 연결과 한 번도 끊기지 않은 연결이 아침에 똑같이 보인다.\n *\n * `lastError` 도 함께 지운다 — 목록에 남은 옛 이유가 지금의 상태로 읽힌다.\n */\n onRecovered: ({ afterFailures, downMs }) => {\n TwinEngine.recordIngestRecovered(domainId, instanceId, afterFailures, downMs, Date.now())\n twinLog(\n `[twin-live] \"${source}\": reading again after ${afterFailures} failed attempt(s) — it was out for ${Math.round(downMs / 1000)}s`\n )\n repo\n .update({ id: row.id }, { lastError: null } as any)\n .catch(err => twinWarn(`[twin-live] \"${source}\": recovery not recorded — ${err?.message ?? err}`))\n },\n /**\n * **읽었는데 창을 넘길 수 없다** — 위와 조치가 반대인 사실이다 (2026-08-24).\n *\n * 이 문을 만들기 전에는 이 사실이 `onReadFailure` 로 나갔다. 그래서 화면이 「원본에 닿지 못한다」고\n * 말했는데 실제로는 원본이 **답한** 상태였고, 그 답의 모양이 커서를 이긴 것이었다. 사람은 원본을\n * 의심하고 기다리는데 기다림으로는 영원히 풀리지 않는다 — **원인을 반대 방향으로 가리켰다.**\n *\n * `status` 를 건드리지 않는 규율은 위와 같다(재부착의 자격이다). `lastError` 에는 적는다 — 목록에서\n * 이유를 볼 수 있어야 하고, 「닿지 못한다」와 다른 문장이어야 한다.\n */\n /*\n * **세우지 않은 것을 장부에 적는다** — 원본에 있는데 트윈에 세우지 않은 것(이유와 수).\n *\n * 로그로도 남기지만 **로그는 사람이 볼 때만 값이 있다.** 이 사실은 상태의 성질이라(세우지 않은 것은\n * 지금도 없다) 조회되는 자리에 있어야 한다 — 그러지 않으면 프로비저닝 화면에서 한 번 스쳐 지나간다.\n *\n * `status`·`lastError` 를 건드리지 않는다: 이것은 **실패가 아니다.** 어댑터가 옳은 판단으로 안 받은\n * 것이고, 오류로 적으면 화면이 원본을 의심하게 만든다.\n */\n onWithheld: ({ reason, count }) => {\n TwinEngine.recordIngestWithheld(domainId, instanceId, reason, count)\n },\n onCursorStall: ({ reason, stream }) => {\n TwinEngine.recordIngestCursorStall(domainId, instanceId, reason, Date.now(), stream)\n twinWarn(\n `[twin-live] \"${source}\": read the source but the cursor cannot advance` +\n `${stream ? ` (${stream})` : ''} — ${reason}. Waiting will not clear this.`\n )\n repo\n .update({ id: row.id }, { lastError: `live cursor stalled: ${reason}` } as any)\n .catch(err => twinWarn(`[twin-live] \"${source}\": cursor stall not recorded — ${err?.message ?? err}`))\n },\n onCursor: next => {\n /* 어댑터가 준 모양을 그대로 적는다 — 이 층은 흐름의 뜻을 모른다(열쇠는 어댑터가 정한다). */\n repo\n .update({ id: row.id }, { liveCursor: next } as any)\n .catch(err => twinWarn(`[twin-live] \"${source}\": live cursor not saved — ${err?.message ?? err}`))\n }\n }\n}\n\n/** 라이브 피드 중지(unsubscribe). */\nexport function stopReferenceLiveFeed(instanceId: string): boolean {\n const unsub = feeds.get(instanceId)\n if (!unsub) return false\n try { unsub() } catch { /* noop */ }\n feeds.delete(instanceId)\n return true\n}\n\n/** 활성 피드 목록(관측). */\nexport function listReferenceLiveFeeds(): string[] {\n return [...feeds.keys()]\n}\n\n/**\n * 부팅 때 **끊어진 피드를 다시 붙인다** — 도는 미러 트윈에 계측이 계속 들어오게.\n *\n * ── 무엇이 알리지 않고 끊겼나 (2026-08-14) ───────────────────────────────────────\n * `startReferenceLiveFeed` 를 부르는 곳은 `importTwinReference` **하나뿐**이었다. 그래서 서버를 한 번\n * 재기동하면 미러 트윈은 「도는 중」이라 말하면서 아무 계측도 받지 않았다 — 오류도 로그도 없이,\n * 사람은 「값이 안 변하네」로 알게 된다. 실 배포에서 재기동 한 번에 계측이 멈추는 셈이다.\n *\n * ── 되살릴 재료는 이미 영속돼 있다 ─────────────────────────────────────────\n * `TwinReference` 가 어댑터 종류·접속 설정·그 레퍼런스가 만든 인스턴스(`scopeSpec.produced`)를 들고 있다.\n * 사이트 서술자만 없는데(`hint` 는 남기지 않았다) **다시 찾아서** 얻는다(`discoverSites`) — 인제스트가\n * 걷는 것과 같은 길이므로 규칙이 갈라지지 않는다. 되살릴 인스턴스가 없으면 그 호출도 하지 않는다.\n *\n * 재개 대상은 **등록부가 도는 미러라고 말하는 것**뿐이다: 멈춘 트윈을 되살리면 사람이 멈춘 것을\n * 우리가 다시 켜는 것이고, 시뮬 트윈에 실 계측을 부으면 두 현실이 섞인다.\n */\nexport async function resumeReferenceLiveFeeds(): Promise<{ resumed: string[]; failed: { instanceId: string; error: string }[] }> {\n const resumed: string[] = []\n const failed: { instanceId: string; error: string }[] = []\n /*\n * **한 트윈에 피드는 하나다** (2026-08-18).\n *\n * 같은 사이트를 두 레퍼런스가 각각 인제스트하면 둘 다 그 인스턴스를 자기가 만든 것(`produced`)으로\n * 들고 있다. 기동 때 그 목록을 그대로 돌면 같은 트윈에 피드를 두 번 붙이는데, 붙이는 함수가 미러를\n * 다시 세우므로(`startLive`) **먼저 쌓인 수요 구간이 리셋된다** — 실제로 그렇게 났다\n * (로그: `reattached 2 feed(s): ems-plant-1, ems-plant-1`).\n *\n * 그러니 한 번만 붙이고, **두 레퍼런스가 같은 트윈을 주장한다는 사실은 말한다** — 알리지 않고 하나를\n * 고르면 어느 접속이 그 트윈을 먹이고 있는지 아무도 모르게 된다.\n */\n const attached = new Map<string, string>() // instanceId → 먼저 붙인 레퍼런스\n try {\n const refs = await getRepository(TwinReference).find({ where: { status: 'connected' }, relations: ['domain'] })\n for (const ref of refs) {\n const domainId = (ref as any).domain?.id ?? (ref as any).domainId\n const adapter = getAdapter(ref.adapterType)\n if (!domainId || !adapter?.openLiveFeed) continue // 관측만 하는 어댑터에는 되살릴 피드가 없다\n\n const produced: any[] = ((ref.scopeSpec as any)?.produced ?? []).filter((p: any) => p?.instanceId && p?.siteId)\n if (!produced.length) continue\n\n /* 등록부가 「도는 미러」라고 말하는 것만 고른다. */\n const rows = await getRepository(TwinInstance).find({\n where: produced.map((p: any) => ({ domain: { id: domainId }, instanceId: p.instanceId })) as any\n })\n const live = produced.filter((p: any) =>\n rows.some(r => r.instanceId === p.instanceId && r.status === 'running' && r.restartPolicy === 'resync')\n )\n if (!live.length) continue\n\n let sites: SiteDescriptor[]\n try {\n sites = await adapter.discoverSites(ref.connectionConfig ?? {})\n } catch (e: any) {\n /* 원 시스템이 아직 안 떴을 수 있다 — 지어내지 않고 그렇게 적는다(트윈은 계속 「도는 미러」다). */\n for (const p of live) failed.push({ instanceId: p.instanceId, error: `discover failed: ${e?.message ?? 'unknown'}` })\n continue\n }\n\n for (const p of live) {\n const site = sites.find(s => s.siteId === p.siteId)\n if (!site) {\n failed.push({ instanceId: p.instanceId, error: `site \"${p.siteId}\" no longer discoverable` })\n continue\n }\n const owner = attached.get(p.instanceId)\n if (owner) {\n /* 두 접속이 같은 트윈을 주장한다 — 먼저 붙은 것을 지키고, 그 사실을 남긴다(둘 다 붙이면 미러가 리셋된다). */\n failed.push({\n instanceId: p.instanceId,\n error: `also claimed by reference \"${ref.source}\" — kept the feed from \"${owner}\" (two connections feeding one twin would reset the mirror)`\n })\n continue\n }\n attached.set(p.instanceId, ref.source)\n try {\n const ok = await startReferenceLiveFeed(domainId, ref.adapterType, ref.connectionConfig ?? {}, site, p.instanceId, ref.source)\n if (ok) resumed.push(p.instanceId)\n else failed.push({ instanceId: p.instanceId, error: 'adapter has no openLiveFeed' })\n } catch (e: any) {\n failed.push({ instanceId: p.instanceId, error: e?.message ?? 'reattach failed' })\n }\n }\n }\n } catch (e: any) {\n twinError('[twin-live] feed resume scan failed', e?.message)\n }\n\n if (resumed.length) twinLog(`[twin-live] reattached ${resumed.length} feed(s): ${resumed.join(', ')}`)\n /* **알리지 않고 넘기지 않는다** — 붙지 않은 미러는 「도는데 아무것도 오지 않는」 트윈이다. */\n for (const f of failed) twinWarn(`[twin-live] \"${f.instanceId}\" feed NOT reattached — ${f.error}`)\n return { resumed, failed }\n}\n\n/*\n * 이 계층의 피드 장부를 등록한다 — 화면이 「끊김」을 말할 근거는 **묻는 자리 하나**여야 한다.\n * (프리셋 기반 피드는 앱이 자기 장부를 따로 등록한다. 한쪽만 보면 흐르는 트윈을 끊겼다고 부른다.)\n */\nregisterLiveFeedProbe(() => [...feeds.keys()])\n\n/* 인스턴스 정지 시 피드 자동 정리(생명주기 대칭). */\nTwinEngine.onStop(instanceId => stopReferenceLiveFeed(instanceId))\n"]}