@things-factory/headless-twin 10.0.18 → 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.
- package/dist-server/engine/canonical-ingest.d.ts +10 -1
- package/dist-server/engine/canonical-ingest.js +110 -33
- package/dist-server/engine/canonical-ingest.js.map +1 -1
- package/dist-server/engine/event-time.d.ts +25 -0
- package/dist-server/engine/event-time.js +98 -0
- package/dist-server/engine/event-time.js.map +1 -0
- package/dist-server/engine/index.d.ts +2 -0
- package/dist-server/engine/index.js +23 -0
- package/dist-server/engine/index.js.map +1 -1
- package/dist-server/engine/ingest-dedupe.d.ts +18 -6
- package/dist-server/engine/ingest-dedupe.js +201 -15
- package/dist-server/engine/ingest-dedupe.js.map +1 -1
- package/dist-server/engine/ingest-health.d.ts +60 -1
- package/dist-server/engine/ingest-health.js +43 -8
- package/dist-server/engine/ingest-health.js.map +1 -1
- package/dist-server/engine/kpi-fold.d.ts +19 -0
- package/dist-server/engine/kpi-fold.js +4 -1
- package/dist-server/engine/kpi-fold.js.map +1 -1
- package/dist-server/engine/kpi-query.js +23 -3
- package/dist-server/engine/kpi-query.js.map +1 -1
- package/dist-server/engine/oee-accumulator.d.ts +35 -2
- package/dist-server/engine/oee-accumulator.js +56 -4
- package/dist-server/engine/oee-accumulator.js.map +1 -1
- package/dist-server/engine/twin-engine.d.ts +72 -3
- package/dist-server/engine/twin-engine.js +118 -5
- package/dist-server/engine/twin-engine.js.map +1 -1
- package/dist-server/index.js +21 -0
- package/dist-server/index.js.map +1 -1
- package/dist-server/migrations/1786600000000-RenameOperatoMesAdapterType.d.ts +5 -0
- package/dist-server/migrations/1786600000000-RenameOperatoMesAdapterType.js +66 -0
- package/dist-server/migrations/1786600000000-RenameOperatoMesAdapterType.js.map +1 -0
- package/dist-server/migrations/1786700000000-AddDispatchRetryColumns.d.ts +5 -0
- package/dist-server/migrations/1786700000000-AddDispatchRetryColumns.js +79 -0
- package/dist-server/migrations/1786700000000-AddDispatchRetryColumns.js.map +1 -0
- package/dist-server/migrations/index.js +5 -1
- package/dist-server/migrations/index.js.map +1 -1
- package/dist-server/routes.js +19 -16
- package/dist-server/routes.js.map +1 -1
- package/dist-server/service/actuation/actuation-advisor.d.ts +60 -0
- package/dist-server/service/actuation/actuation-advisor.js +53 -0
- package/dist-server/service/actuation/actuation-advisor.js.map +1 -0
- package/dist-server/service/actuation/actuation-outcome.d.ts +52 -0
- package/dist-server/service/actuation/actuation-outcome.js +34 -0
- package/dist-server/service/actuation/actuation-outcome.js.map +1 -0
- package/dist-server/service/actuation/actuation-rule-resolver.d.ts +19 -0
- package/dist-server/service/actuation/actuation-rule-resolver.js +161 -0
- package/dist-server/service/actuation/actuation-rule-resolver.js.map +1 -0
- package/dist-server/service/actuation/actuation-rule.d.ts +55 -0
- package/dist-server/service/actuation/actuation-rule.js +130 -0
- package/dist-server/service/actuation/actuation-rule.js.map +1 -0
- package/dist-server/service/actuation/approval-gateway.d.ts +20 -0
- package/dist-server/service/actuation/approval-gateway.js +14 -0
- package/dist-server/service/actuation/approval-gateway.js.map +1 -0
- package/dist-server/service/actuation/catch-up-effects.d.ts +16 -0
- package/dist-server/service/actuation/catch-up-effects.js +120 -0
- package/dist-server/service/actuation/catch-up-effects.js.map +1 -0
- package/dist-server/service/actuation/command-dispatcher.d.ts +230 -0
- package/dist-server/service/actuation/command-dispatcher.js +269 -0
- package/dist-server/service/actuation/command-dispatcher.js.map +1 -0
- package/dist-server/service/actuation/command-store.d.ts +11 -0
- package/dist-server/service/actuation/command-store.js +219 -0
- package/dist-server/service/actuation/command-store.js.map +1 -0
- package/dist-server/service/actuation/dispatch-after-approval.d.ts +11 -0
- package/dist-server/service/actuation/dispatch-after-approval.js +73 -0
- package/dist-server/service/actuation/dispatch-after-approval.js.map +1 -0
- package/dist-server/service/actuation/effect-match.d.ts +53 -0
- package/dist-server/service/actuation/effect-match.js +65 -0
- package/dist-server/service/actuation/effect-match.js.map +1 -0
- package/dist-server/service/actuation/index.d.ts +25 -0
- package/dist-server/service/actuation/index.js +30 -0
- package/dist-server/service/actuation/index.js.map +1 -0
- package/dist-server/service/actuation/retry-plan.d.ts +63 -0
- package/dist-server/service/actuation/retry-plan.js +99 -0
- package/dist-server/service/actuation/retry-plan.js.map +1 -0
- package/dist-server/service/actuation/rule-evaluate.d.ts +127 -0
- package/dist-server/service/actuation/rule-evaluate.js +205 -0
- package/dist-server/service/actuation/rule-evaluate.js.map +1 -0
- package/dist-server/service/actuation/rule-loop.d.ts +5 -0
- package/dist-server/service/actuation/rule-loop.js +156 -0
- package/dist-server/service/actuation/rule-loop.js.map +1 -0
- package/dist-server/service/actuation/rule-runner.d.ts +52 -0
- package/dist-server/service/actuation/rule-runner.js +135 -0
- package/dist-server/service/actuation/rule-runner.js.map +1 -0
- package/dist-server/service/actuation/rule-wiring.d.ts +24 -0
- package/dist-server/service/actuation/rule-wiring.js +80 -0
- package/dist-server/service/actuation/rule-wiring.js.map +1 -0
- package/dist-server/service/actuation/settle-effects.d.ts +21 -0
- package/dist-server/service/actuation/settle-effects.js +101 -0
- package/dist-server/service/actuation/settle-effects.js.map +1 -0
- package/dist-server/service/actuation/sweep-dispatch.d.ts +37 -0
- package/dist-server/service/actuation/sweep-dispatch.js +125 -0
- package/dist-server/service/actuation/sweep-dispatch.js.map +1 -0
- package/dist-server/service/actuation/twin-command-resolver.d.ts +120 -0
- package/dist-server/service/actuation/twin-command-resolver.js +406 -0
- package/dist-server/service/actuation/twin-command-resolver.js.map +1 -0
- package/dist-server/service/actuation/twin-command.d.ts +111 -0
- package/dist-server/service/actuation/twin-command.js +170 -0
- package/dist-server/service/actuation/twin-command.js.map +1 -0
- package/dist-server/service/actuation/twin-now.d.ts +41 -0
- package/dist-server/service/actuation/twin-now.js +65 -0
- package/dist-server/service/actuation/twin-now.js.map +1 -0
- package/dist-server/service/index.d.ts +5 -2
- package/dist-server/service/index.js +28 -17
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/reference/actuation-routing.d.ts +60 -0
- package/dist-server/service/reference/actuation-routing.js +126 -0
- package/dist-server/service/reference/actuation-routing.js.map +1 -0
- package/dist-server/service/reference/actuation-target.d.ts +65 -0
- package/dist-server/service/reference/actuation-target.js +94 -0
- package/dist-server/service/reference/actuation-target.js.map +1 -0
- package/dist-server/service/reference/connection-portability-resolver.d.ts +29 -0
- package/dist-server/service/reference/connection-portability-resolver.js +181 -0
- package/dist-server/service/reference/connection-portability-resolver.js.map +1 -0
- package/dist-server/service/reference/connection-portability.d.ts +138 -0
- package/dist-server/service/reference/connection-portability.js +230 -0
- package/dist-server/service/reference/connection-portability.js.map +1 -0
- package/dist-server/service/reference/fill-cadence.d.ts +17 -0
- package/dist-server/service/reference/fill-cadence.js +31 -0
- package/dist-server/service/reference/fill-cadence.js.map +1 -0
- package/dist-server/service/reference/fill-dedupe.d.ts +16 -0
- package/dist-server/service/reference/fill-dedupe.js +63 -0
- package/dist-server/service/reference/fill-dedupe.js.map +1 -0
- package/dist-server/service/reference/fill-loop.d.ts +35 -0
- package/dist-server/service/reference/fill-loop.js +187 -0
- package/dist-server/service/reference/fill-loop.js.map +1 -0
- package/dist-server/service/reference/fill-span.d.ts +10 -0
- package/dist-server/service/reference/fill-span.js +37 -0
- package/dist-server/service/reference/fill-span.js.map +1 -0
- package/dist-server/service/reference/hook-contract.d.ts +149 -22
- package/dist-server/service/reference/hook-contract.js +236 -23
- package/dist-server/service/reference/hook-contract.js.map +1 -1
- package/dist-server/service/reference/hook-rejected.d.ts +25 -0
- package/dist-server/service/reference/hook-rejected.js +48 -0
- package/dist-server/service/reference/hook-rejected.js.map +1 -0
- package/dist-server/service/reference/hook-store.d.ts +3 -0
- package/dist-server/service/reference/hook-store.js +28 -0
- package/dist-server/service/reference/hook-store.js.map +1 -0
- package/dist-server/service/reference/index.d.ts +4 -1
- package/dist-server/service/reference/index.js +5 -1
- package/dist-server/service/reference/index.js.map +1 -1
- package/dist-server/service/reference/live-cadence.d.ts +68 -0
- package/dist-server/service/reference/live-cadence.js +44 -0
- package/dist-server/service/reference/live-cadence.js.map +1 -0
- package/dist-server/service/reference/live-feed-lease.d.ts +15 -0
- package/dist-server/service/reference/live-feed-lease.js +56 -0
- package/dist-server/service/reference/live-feed-lease.js.map +1 -0
- package/dist-server/service/reference/live-ingest.d.ts +21 -0
- package/dist-server/service/reference/live-ingest.js +66 -0
- package/dist-server/service/reference/live-ingest.js.map +1 -0
- package/dist-server/service/reference/reference-adapter.d.ts +355 -2
- package/dist-server/service/reference/reference-adapter.js +19 -1
- package/dist-server/service/reference/reference-adapter.js.map +1 -1
- package/dist-server/service/reference/reference-fill-run.d.ts +50 -0
- package/dist-server/service/reference/reference-fill-run.js +142 -0
- package/dist-server/service/reference/reference-fill-run.js.map +1 -0
- package/dist-server/service/reference/reference-fill.d.ts +90 -0
- package/dist-server/service/reference/reference-fill.js +141 -0
- package/dist-server/service/reference/reference-fill.js.map +1 -0
- package/dist-server/service/reference/reference-hook.d.ts +64 -6
- package/dist-server/service/reference/reference-hook.js +265 -27
- package/dist-server/service/reference/reference-hook.js.map +1 -1
- package/dist-server/service/reference/reference-link-groups.d.ts +10 -0
- package/dist-server/service/reference/reference-link-groups.js +15 -0
- package/dist-server/service/reference/reference-link-groups.js.map +1 -0
- package/dist-server/service/reference/reference-links.d.ts +14 -0
- package/dist-server/service/reference/reference-links.js +73 -0
- package/dist-server/service/reference/reference-links.js.map +1 -0
- package/dist-server/service/reference/reference-live.d.ts +1 -0
- package/dist-server/service/reference/reference-live.js +32 -6
- package/dist-server/service/reference/reference-live.js.map +1 -1
- package/dist-server/service/reference/reference-master.d.ts +56 -2
- package/dist-server/service/reference/reference-master.js +33 -3
- package/dist-server/service/reference/reference-master.js.map +1 -1
- package/dist-server/service/reference/reference-resolver.d.ts +27 -0
- package/dist-server/service/reference/reference-resolver.js +79 -1
- package/dist-server/service/reference/reference-resolver.js.map +1 -1
- package/dist-server/service/twin-event/twin-event-keys.d.ts +31 -3
- package/dist-server/service/twin-event/twin-event-keys.js +36 -4
- package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
- package/dist-server/service/twin-event/twin-event.d.ts +1 -0
- package/dist-server/service/twin-event/twin-event.js +11 -2
- package/dist-server/service/twin-event/twin-event.js.map +1 -1
- package/dist-server/service/twin-model/choose-recipe.d.ts +15 -0
- package/dist-server/service/twin-model/choose-recipe.js +33 -0
- package/dist-server/service/twin-model/choose-recipe.js.map +1 -0
- package/dist-server/service/twin-model/external-resolver.d.ts +76 -0
- package/dist-server/service/twin-model/external-resolver.js +109 -0
- package/dist-server/service/twin-model/external-resolver.js.map +1 -0
- package/dist-server/service/twin-model/project-structure.js +16 -3
- package/dist-server/service/twin-model/project-structure.js.map +1 -1
- package/dist-server/service/twin-model/twin-equipment.d.ts +17 -0
- package/dist-server/service/twin-model/twin-equipment.js +10 -0
- package/dist-server/service/twin-model/twin-equipment.js.map +1 -1
- package/dist-server/service/twin-model/twin-lineage-query.js +75 -1
- package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -1
- package/dist-server/service/twin-model/twin-model-item-query.js +94 -5
- package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -1
- package/dist-server/service/twin-model/twin-model-tree-query.js.map +1 -1
- package/dist-server/service/twin-space/twin-space.d.ts +29 -0
- package/dist-server/service/twin-space/twin-space.js +13 -0
- package/dist-server/service/twin-space/twin-space.js.map +1 -1
- package/dist-server/service/twin-subject/event-subjects.d.ts +3 -1
- package/dist-server/service/twin-subject/event-subjects.js +8 -1
- package/dist-server/service/twin-subject/event-subjects.js.map +1 -1
- package/dist-shared/entity-delta.js +1 -1
- package/dist-shared/entity-delta.js.map +1 -1
- package/package.json +7 -7
- package/server/engine/canonical-ingest.ts +119 -33
- package/server/engine/event-time.ts +96 -0
- package/server/engine/index.ts +20 -0
- package/server/engine/ingest-dedupe.ts +203 -27
- package/server/engine/ingest-health.ts +91 -6
- package/server/engine/kpi-fold.ts +24 -1
- package/server/engine/kpi-query.ts +23 -4
- package/server/engine/oee-accumulator.ts +80 -8
- package/server/engine/twin-engine.ts +119 -5
- package/server/index.ts +23 -0
- package/server/migrations/1786600000000-RenameOperatoMesAdapterType.ts +66 -0
- package/server/migrations/1786700000000-AddDispatchRetryColumns.ts +82 -0
- package/server/migrations/index.ts +5 -1
- package/server/routes.ts +19 -20
- package/server/service/actuation/actuation-advisor.ts +124 -0
- package/server/service/actuation/actuation-outcome.ts +97 -0
- package/server/service/actuation/actuation-rule-resolver.ts +147 -0
- package/server/service/actuation/actuation-rule.ts +139 -0
- package/server/service/actuation/approval-gateway.ts +44 -0
- package/server/service/actuation/catch-up-effects.ts +152 -0
- package/server/service/actuation/command-dispatcher.ts +511 -0
- package/server/service/actuation/command-store.ts +236 -0
- package/server/service/actuation/dispatch-after-approval.ts +71 -0
- package/server/service/actuation/effect-match.ts +124 -0
- package/server/service/actuation/index.ts +27 -0
- package/server/service/actuation/retry-plan.ts +145 -0
- package/server/service/actuation/rule-evaluate.ts +352 -0
- package/server/service/actuation/rule-loop.ts +160 -0
- package/server/service/actuation/rule-runner.ts +216 -0
- package/server/service/actuation/rule-wiring.ts +84 -0
- package/server/service/actuation/settle-effects.ts +115 -0
- package/server/service/actuation/sweep-dispatch.ts +179 -0
- package/server/service/actuation/twin-command-resolver.ts +387 -0
- package/server/service/actuation/twin-command.ts +232 -0
- package/server/service/actuation/twin-now.ts +97 -0
- package/server/service/index.ts +11 -0
- package/server/service/reference/actuation-routing.ts +165 -0
- package/server/service/reference/actuation-target.ts +163 -0
- package/server/service/reference/connection-portability-resolver.ts +171 -0
- package/server/service/reference/connection-portability.ts +285 -0
- package/server/service/reference/fill-cadence.ts +29 -0
- package/server/service/reference/fill-dedupe.ts +66 -0
- package/server/service/reference/fill-loop.ts +153 -0
- package/server/service/reference/fill-span.ts +31 -0
- package/server/service/reference/hook-contract.ts +280 -22
- package/server/service/reference/hook-rejected.ts +53 -0
- package/server/service/reference/hook-store.ts +28 -0
- package/server/service/reference/index.ts +5 -1
- package/server/service/reference/live-cadence.ts +129 -0
- package/server/service/reference/live-feed-lease.ts +56 -0
- package/server/service/reference/live-ingest.ts +94 -0
- package/server/service/reference/reference-adapter.ts +392 -5
- package/server/service/reference/reference-fill-run.ts +165 -0
- package/server/service/reference/reference-fill.ts +223 -0
- package/server/service/reference/reference-hook.ts +355 -32
- package/server/service/reference/reference-link-groups.ts +21 -0
- package/server/service/reference/reference-links.ts +77 -0
- package/server/service/reference/reference-live.ts +30 -5
- package/server/service/reference/reference-master.ts +90 -5
- package/server/service/reference/reference-resolver.ts +77 -2
- package/server/service/twin-event/twin-event-keys.ts +37 -4
- package/server/service/twin-event/twin-event.ts +18 -1
- package/server/service/twin-model/choose-recipe.ts +31 -0
- package/server/service/twin-model/external-resolver.ts +161 -0
- package/server/service/twin-model/project-structure.ts +16 -3
- package/server/service/twin-model/twin-equipment.ts +22 -0
- package/server/service/twin-model/twin-lineage-query.ts +82 -1
- package/server/service/twin-model/twin-model-item-query.ts +95 -4
- package/server/service/twin-model/twin-model-tree-query.ts +1 -1
- package/server/service/twin-space/twin-space.ts +39 -0
- package/server/service/twin-subject/event-subjects.ts +8 -1
- package/shared/entity-delta.ts +1 -1
- package/test/ack-shape-alignment.test.ts +97 -0
- package/test/actuation-approval-door.test.ts +421 -0
- package/test/actuation-dispatch.test.ts +209 -0
- package/test/actuation-effect.test.ts +162 -0
- package/test/actuation-outcome.test.ts +152 -0
- package/test/actuation-rule-runner.test.ts +281 -0
- package/test/actuation-rule.test.ts +266 -0
- package/test/actuation-seam.test.ts +249 -0
- package/test/actuation-settle.test.ts +164 -0
- package/test/actuation-target.test.ts +145 -0
- package/test/canonical-ingest-vocabularies.test.ts +49 -2
- package/test/capability-mapping.test.ts +1 -1
- package/test/choose-recipe.test.ts +117 -0
- package/test/command-store-writes-columns.test.ts +126 -0
- package/test/connection-portability-doors.test.ts +87 -0
- package/test/connector-capability-declaration.test.ts +54 -4
- package/test/declaration-reaches-model.test.ts +43 -0
- package/test/dedupe-transformation.test.ts +112 -0
- package/test/entity-delta.test.ts +1 -1
- package/test/event-subjects.test.ts +2 -1
- package/test/external-resolver.test.ts +171 -0
- package/test/fill-dedupe.test.ts +59 -0
- package/test/fill-loop.test.ts +35 -0
- package/test/first-envelope.test.ts +153 -0
- package/test/fold-shift-dst.test.ts +110 -0
- package/test/home-in-area.test.ts +90 -0
- package/test/hook-rejected-shape.test.ts +86 -0
- package/test/hook-response-sum.test.ts +193 -0
- package/test/hook-sequence.test.ts +333 -0
- package/test/ingest-dedupe.test.ts +56 -0
- package/test/ingest-expected-quiet.test.ts +111 -0
- package/test/ingest-idempotent.test.ts +79 -0
- package/test/ingest-reconcile-four-ways.test.ts +97 -0
- package/test/ingest-rules-header.test.ts +100 -0
- package/test/ingest-window-rolls-unobserved.test.ts +72 -0
- package/test/intake-mapping-parity.test.ts +175 -0
- package/test/live-cadence.test.ts +170 -0
- package/test/live-feed-lease.test.ts +63 -0
- package/test/live-mirror-parity.test.ts +1 -1
- package/test/oee-accumulator.test.ts +3 -3
- package/test/oee-site-calendar.test.ts +146 -0
- package/test/order-and-transaction-split.test.ts +107 -0
- package/test/reference-fill.test.ts +235 -0
- package/test/reference-hook.test.ts +114 -23
- package/test/reference-links.test.ts +56 -0
- package/test/retry-plan.test.ts +139 -0
- package/test/sweep-dispatch.test.ts +191 -0
- package/test/twin-event-keys.test.ts +20 -6
- package/test/twin-model-item-db.test.ts +6 -6
- package/test/twin-model-tree-db.test.ts +6 -6
- package/tsconfig.shared.tsbuildinfo +1 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* **빠진 구간을 다시 받는 일을 실제로 부른다** — `fillSince` 에 연결과 어댑터를 물려 주는 자리.
|
|
3
|
+
*
|
|
4
|
+
* ── 왜 이 파일이 없었나 ─────────────────────────────────────────────────────
|
|
5
|
+
* `fillSince` 는 2026-08-31 에 만들어졌고 **부르는 곳이 0곳이었습니다.** 2026-09-05 에 저는 그것을
|
|
6
|
+
* 못 보고 같은 일을 하는 것을 하나 더 만들었습니다(`backfillScope`). 두 벌이 되었고 둘 다 아무도
|
|
7
|
+
* 부르지 않았습니다.
|
|
8
|
+
*
|
|
9
|
+
* 그래서 이런 상태였습니다.
|
|
10
|
+
*
|
|
11
|
+
* ```
|
|
12
|
+
* 번호가 비면 어디서부터 다시 받을지 안다 ✔ takeInSequence 가 판정한다
|
|
13
|
+
* 그 빈 것을 메운다 ✖ 부를 곳이 없다
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* 실제로 plant 아웃박스의 1~20 을 잃고 되찾을 방법이 없는 상태를 만났습니다.
|
|
17
|
+
*
|
|
18
|
+
* ── 같은 검사를 지난다 ──────────────────────────────────────────────────────
|
|
19
|
+
* `fillSince` 가 `takeInSequence` 를 지나고, 유입은 `liveIngest` 를 지납니다 — 밀어 받는 길과 같은
|
|
20
|
+
* 함수입니다. 여기서 따로 만들면 두 길이 다른 규칙으로 받고, 한쪽만 수정되는 날이 옵니다.
|
|
21
|
+
*
|
|
22
|
+
* ── 커서 열쇠는 프레임워크가 만든다 ─────────────────────────────────────────
|
|
23
|
+
* 열쇠는 `instanceId::scope` 이고 그것을 아는 것은 프레임워크입니다. 어댑터가 만들면 두 곳이 같은
|
|
24
|
+
* 규칙을 갖게 되고, 어긋나는 날 **다시 받기가 자기 커서를 0부터 새로 셉니다.**
|
|
25
|
+
*/
|
|
26
|
+
import { getRepository } from '@things-factory/shell'
|
|
27
|
+
|
|
28
|
+
import { TwinInstance } from '../twin-instance/twin-instance.js'
|
|
29
|
+
import { TwinReference } from './twin-reference.js'
|
|
30
|
+
import { getAdapter, type SeqReport, type SiteDescriptor } from './reference-adapter.js'
|
|
31
|
+
import { pushCursorKey, withSeq } from './hook-contract.js'
|
|
32
|
+
import { fillSince, type FillOutcome } from './reference-fill.js'
|
|
33
|
+
import { liveIngest } from './live-ingest.js'
|
|
34
|
+
import { journalIdentities, spanOf } from './fill-dedupe.js'
|
|
35
|
+
import { TwinEngine } from '../../engine/index.js'
|
|
36
|
+
import { twinLog, twinWarn } from '../../engine/log.js'
|
|
37
|
+
|
|
38
|
+
/** 한 연결의 한 단위를 다시 받은 결과. */
|
|
39
|
+
export interface FillRunResult extends Partial<FillOutcome> {
|
|
40
|
+
instanceId: string
|
|
41
|
+
scope: string
|
|
42
|
+
/**
|
|
43
|
+
* 못 한 이유 — 성공이면 없습니다.
|
|
44
|
+
*
|
|
45
|
+
* `not-supported` 는 결함이 아닙니다. 주기로 물어보는 원본(chef · ppms)은 다음 주기가 곧 다시
|
|
46
|
+
* 받는 것입니다.
|
|
47
|
+
*/
|
|
48
|
+
skipped?: 'unknown-twin' | 'no-source' | 'no-adapter' | 'not-supported'
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 그 연결의 한 단위를 다시 받는다.
|
|
53
|
+
*
|
|
54
|
+
* **커서를 되돌리지 않습니다.** 받은 것이 번호 검사를 지나야 커서가 나아가고, 그 판정은 밀어 받는
|
|
55
|
+
* 길과 같은 함수가 합니다.
|
|
56
|
+
*
|
|
57
|
+
* @param from 여기서부터 다시 받는다 — 주면 커서를 무시합니다. 잃은 구간을 되찾을 때만 씁니다.
|
|
58
|
+
*/
|
|
59
|
+
export async function runFill(args: {
|
|
60
|
+
domainId: string
|
|
61
|
+
instanceId: string
|
|
62
|
+
scope: string
|
|
63
|
+
from?: number
|
|
64
|
+
maxPages?: number
|
|
65
|
+
}): Promise<FillRunResult> {
|
|
66
|
+
const { domainId, instanceId, scope, from, maxPages } = args
|
|
67
|
+
const base: FillRunResult = { instanceId, scope }
|
|
68
|
+
|
|
69
|
+
const inst = await getRepository(TwinInstance).findOne({ where: { domain: { id: domainId }, instanceId } })
|
|
70
|
+
if (!inst) return { ...base, skipped: 'unknown-twin' }
|
|
71
|
+
|
|
72
|
+
const repo = getRepository(TwinReference)
|
|
73
|
+
const ref = await repo.findOne({ where: { domain: { id: domainId }, source: instanceId } })
|
|
74
|
+
if (!ref) return { ...base, skipped: 'no-source' }
|
|
75
|
+
|
|
76
|
+
const adapter = getAdapter(ref.adapterType)
|
|
77
|
+
if (!adapter) return { ...base, skipped: 'no-adapter' }
|
|
78
|
+
/* 다시 받기를 안 내는 원본이 있습니다 — 주기로 물어보는 쪽은 다음 주기가 곧 그것입니다. */
|
|
79
|
+
if (typeof adapter.fetchSince !== 'function') return { ...base, skipped: 'not-supported' }
|
|
80
|
+
|
|
81
|
+
const siteId = (inst.origin as any)?.siteId
|
|
82
|
+
const site: SiteDescriptor = siteId ? { siteId } : { siteId: instanceId }
|
|
83
|
+
const cursorKey = pushCursorKey(instanceId, scope)
|
|
84
|
+
|
|
85
|
+
/*
|
|
86
|
+
* `from` 을 주면 커서를 그 값으로 갈아 끼웁니다 — **저장된 커서를 건드리지 않습니다.** `fillSince`
|
|
87
|
+
* 가 나아간 만큼만 적으므로, 잃은 구간을 되찾은 뒤 커서는 원래 자리보다 앞서지 않습니다.
|
|
88
|
+
*/
|
|
89
|
+
const cursor = from === undefined ? ((ref as any).liveCursor ?? {}) : withSeq((ref as any).liveCursor, cursorKey, from)
|
|
90
|
+
|
|
91
|
+
const outcome = await fillSince({
|
|
92
|
+
cfg: ref.connectionConfig ?? {},
|
|
93
|
+
site,
|
|
94
|
+
scope,
|
|
95
|
+
cursorKey,
|
|
96
|
+
cursor,
|
|
97
|
+
fetch: (cfg, s, sc, since) => adapter.fetchSince!(cfg, s as SiteDescriptor, sc, since),
|
|
98
|
+
ingest: records => liveIngest(domainId, instanceId, records, 'fill'),
|
|
99
|
+
/*
|
|
100
|
+
* ── 넣기 전에 기억을 채운다 (2026-09-06) ──────────────────────────────────
|
|
101
|
+
* 중복 판정 기억은 프로세스 안에만 있다. 재기동하면 비워지고, 그러면 backfill 이 **이미 저널에
|
|
102
|
+
* 있는 사실도 다시 앉힌다.** 실측으로 39건이 통째로 다시 앉았다(저널 44 → 83).
|
|
103
|
+
*
|
|
104
|
+
* push 경로에서는 안 한다 — 거기서는 재전송이 창 안에 들어와 이미 걸리고, 봉투마다 저널을
|
|
105
|
+
* 조회하면 뜨거운 길이 느려진다.
|
|
106
|
+
*/
|
|
107
|
+
beforeIngest: async records => {
|
|
108
|
+
const span = spanOf(records)
|
|
109
|
+
if (!span) return
|
|
110
|
+
const { identities, scanned, truncated } = await journalIdentities(domainId, instanceId, span)
|
|
111
|
+
const added = TwinEngine.warmDeduper(domainId, instanceId, identities)
|
|
112
|
+
if (truncated) {
|
|
113
|
+
twinWarn(
|
|
114
|
+
`[twin-fill] "${instanceId}" 의 저널을 ${scanned}줄까지만 읽었습니다 — 그보다 앞의 사실은 ` +
|
|
115
|
+
'다시 앉을 수 있습니다. 구간을 좁혀 부르거나 정체 색인이 필요합니다'
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
if (added) twinLog(`[twin-fill] "${instanceId}" — 저널에서 정체 ${added}개를 기억에 채웠습니다`)
|
|
119
|
+
},
|
|
120
|
+
saveCursor: async next => {
|
|
121
|
+
await repo.update({ id: (ref as any).id }, { liveCursor: next } as any)
|
|
122
|
+
},
|
|
123
|
+
...(maxPages ? { maxPages } : {})
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
const say = `[twin-fill] "${instanceId}" · ${scope} — ${outcome.applied}건 넣음 · ${outcome.pages}쪽 · ${outcome.stopped}${outcome.detail ? ` (${outcome.detail})` : ''}`
|
|
127
|
+
/* 끝난 것과 막힌 것을 다른 소리로 남깁니다 — 구멍은 사람이 원본을 봐야 합니다. */
|
|
128
|
+
if (outcome.stopped === 'done' || outcome.stopped === 'no-progress') twinLog(say)
|
|
129
|
+
else twinWarn(say)
|
|
130
|
+
|
|
131
|
+
return { ...base, ...outcome }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* **그 번호들이 무엇이었나** — 구멍을 만났을 때 사람이 묻는다.
|
|
136
|
+
*
|
|
137
|
+
* 커넥터는 「418 다음이 420」까지만 말한다. 그 419 가 무엇이었는지 알려면 원본에 물어야 하고,
|
|
138
|
+
* 그 길이 없어서 사람이 아웃박스를 손으로 뒤졌다(2026-09-06).
|
|
139
|
+
*
|
|
140
|
+
* **판정하지 않는다.** 원본이 아는 것을 그대로 낸다 — 「그러니 포기해도 된다」는 되돌릴 수 없는
|
|
141
|
+
* 판단이라 사람의 몫이다.
|
|
142
|
+
*/
|
|
143
|
+
export async function seqReportOf(args: {
|
|
144
|
+
domainId: string
|
|
145
|
+
instanceId: string
|
|
146
|
+
scope: string
|
|
147
|
+
from: number
|
|
148
|
+
to: number
|
|
149
|
+
}): Promise<{ instanceId: string; scope: string; report?: SeqReport; skipped?: 'unknown-twin' | 'no-source' | 'no-adapter' | 'not-supported' }> {
|
|
150
|
+
const { domainId, instanceId, scope, from, to } = args
|
|
151
|
+
const base = { instanceId, scope }
|
|
152
|
+
|
|
153
|
+
const inst = await getRepository(TwinInstance).findOne({ where: { domain: { id: domainId }, instanceId } })
|
|
154
|
+
if (!inst) return { ...base, skipped: 'unknown-twin' }
|
|
155
|
+
const ref = await getRepository(TwinReference).findOne({ where: { domain: { id: domainId }, source: instanceId } })
|
|
156
|
+
if (!ref) return { ...base, skipped: 'no-source' }
|
|
157
|
+
const adapter = getAdapter(ref.adapterType)
|
|
158
|
+
if (!adapter) return { ...base, skipped: 'no-adapter' }
|
|
159
|
+
/* 번호를 매기지 않는 원본은 물을 것이 없다 — 결함이 아니다. */
|
|
160
|
+
if (typeof adapter.seqReport !== 'function') return { ...base, skipped: 'not-supported' }
|
|
161
|
+
|
|
162
|
+
const siteId = (inst.origin as any)?.siteId
|
|
163
|
+
const site: SiteDescriptor | undefined = siteId ? { siteId } : undefined
|
|
164
|
+
return { ...base, report: await adapter.seqReport(ref.connectionConfig ?? {}, site, scope, from, to) }
|
|
165
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* **놓친 구간을 backfill 해 채운다** — 밀어 받는 길의 짝.
|
|
3
|
+
*
|
|
4
|
+
* ── 왜 필요한가 (2026-08-31, 인티그레이션 레인 지적) ──────────────────────
|
|
5
|
+
* 훅은 반드시 놓친다 — 우리가 내려가 있을 때, 그쪽이 못 보냈을 때, 네트워크가 끊겼을 때. 받는 쪽이
|
|
6
|
+
* 번호로 그것을 **알아채게** 되었지만(§`takeInSequence`), 알아챈 뒤 **그 사이를 채우는 길이 없었다.**
|
|
7
|
+
* 설계가 그 길을 적어 두었는데 만들지 않았다.
|
|
8
|
+
*
|
|
9
|
+
* ── 같은 검사를 지난다 ───────────────────────────────────────────────────
|
|
10
|
+
* backfill 한 것도 `takeInSequence` 를 지난다. 여기서 따로 만들면 두 길이 다른 규칙으로 받고, 한쪽만
|
|
11
|
+
* 고쳐지는 날 어긋난 그 길에서만 사실이 사라진다.
|
|
12
|
+
*
|
|
13
|
+
* ── 멈추는 조건을 셋 둔다 ────────────────────────────────────────────────
|
|
14
|
+
* 되풀이해서 요청하는 경로는 **끝나지 않는 것**이 가장 흔한 결함이다.
|
|
15
|
+
*
|
|
16
|
+
* 빈 답 더 줄 것이 없다. 끝
|
|
17
|
+
* 번호가 안 나아간다 같은 것을 다시 받았다 — 무한히 요청하는 것을 막는다
|
|
18
|
+
* 구멍 원본의 아웃박스에 구멍이 있다. 이어 받으면 커서가 그 번호를 지나쳐 적힌다
|
|
19
|
+
*
|
|
20
|
+
* 그리고 **쪽 상한**을 둔다. 상한에 닿았으면 조용히 끝내지 않고 말한다 — 조용히 자르면 사람은 다 받은
|
|
21
|
+
* 줄로 읽는다.
|
|
22
|
+
*/
|
|
23
|
+
import { seqOf, takeInSequence, withSeq } from './hook-contract.js'
|
|
24
|
+
import type { InboundBatch } from './reference-adapter.js'
|
|
25
|
+
import { twinWarn } from '../../engine/log.js'
|
|
26
|
+
|
|
27
|
+
/** 한 번 채우고 나서 남는 사실. */
|
|
28
|
+
export interface FillOutcome {
|
|
29
|
+
/** 트윈에 넣은 레코드 수. */
|
|
30
|
+
applied: number
|
|
31
|
+
/**
|
|
32
|
+
* ingest 가 낸 네 갈래의 합 — **두 갈래로는 셈이 안 닫힌다.**
|
|
33
|
+
*
|
|
34
|
+
* ```
|
|
35
|
+
* 두 갈래 157 − 46 − 2 = 109 「설명 못 함」 진실은 되풀이 108 + 어휘 1
|
|
36
|
+
* 네 갈래 157 − 46 − 2 − 108 − 1 = 0
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* 이 값이 없으면 원본 쪽에서 「보낸 것 20개 중 event 가 9개인데 나머지는 어디 갔나」를 셀 수
|
|
40
|
+
* 없다. 실제로 인티그레이션 레인이 그것을 물었고, 그 수가 트윈에 저장되지 않아 답을 못 했다.
|
|
41
|
+
*/
|
|
42
|
+
duplicates: number
|
|
43
|
+
rejected: number
|
|
44
|
+
/**
|
|
45
|
+
* 왜 거절됐나 — **사유별 수.** 수만 내면 「26건이 안 들어온다」에서 멈춘다.
|
|
46
|
+
*
|
|
47
|
+
* 목록 전체가 아니라 사유별로 센다. 같은 이유로 거절된 26건이 26줄이면 읽는 사람이 그 줄을
|
|
48
|
+
* 세어야 하고, 다른 이유 하나가 그 안에 묻힌다.
|
|
49
|
+
*/
|
|
50
|
+
rejectedReasons?: Record<string, number>
|
|
51
|
+
vocabulary: number
|
|
52
|
+
/** 시각을 못 읽어 ingest 시각으로 떨어진 것 — 종류별. 비어 있으면 전부 자기 시각을 말했다. */
|
|
53
|
+
undated?: Record<string, number>
|
|
54
|
+
/** 몇 쪽을 받았나. */
|
|
55
|
+
pages: number
|
|
56
|
+
/** 커서가 어디까지 갔나 — 아무것도 못 받았으면 없다. */
|
|
57
|
+
lastSeq?: number
|
|
58
|
+
/**
|
|
59
|
+
* 이 실행에서 **가장 낮게 받아들인 번호** — 밑에서부터 다시 받았을 때 이것이 곧 「우리가 가진
|
|
60
|
+
* 가장 낮은 번호」다.
|
|
61
|
+
*/
|
|
62
|
+
lowestSeq?: number
|
|
63
|
+
/** 왜 멈췄나 — `done` · `gap` · `no-progress` · `page-limit` · `refused`. */
|
|
64
|
+
stopped: 'done' | 'gap' | 'no-progress' | 'page-limit' | 'refused'
|
|
65
|
+
/** 멈춘 이유의 사람 말. 알리지 않고 끝내지 않는다. */
|
|
66
|
+
detail?: string
|
|
67
|
+
/**
|
|
68
|
+
* 마지막으로 받은 묶음이 말한 **원본의 번호 범위** — 원본이 안 말하면 없다.
|
|
69
|
+
*
|
|
70
|
+
* 우리 커서와 견주는 자리가 이 값을 쓴다. 없으면 「맞다」가 아니라 **「모른다」**로 답해야 한다.
|
|
71
|
+
*/
|
|
72
|
+
sourceRange?: { first?: number; last?: number }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** 몇 쪽까지 받나 — 한 번의 채우기가 서버를 오래 붙잡지 않게. */
|
|
76
|
+
const MAX_PAGES = 50
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 그 단위의 놓친 구간을 채운다.
|
|
80
|
+
*
|
|
81
|
+
* `ingest` 를 부르는 쪽이 넘겨 준다 — 훅과 같은 이유다(이 파일이 엔진을 직접 부르면 시험이 엔진을
|
|
82
|
+
* 세워야 한다).
|
|
83
|
+
*/
|
|
84
|
+
export async function fillSince(args: {
|
|
85
|
+
cfg: Record<string, any>
|
|
86
|
+
site: { siteId: string }
|
|
87
|
+
/** 번호를 매기는 단위 — 커서 열쇠의 절반. 커넥터가 만들지 않는다. */
|
|
88
|
+
scope: string
|
|
89
|
+
/** 커서에 적힌 열쇠 — `pushCursorKey(instanceId, scope)`. */
|
|
90
|
+
cursorKey: string
|
|
91
|
+
/** 지금 커서 — 이 함수가 읽고, 나아간 만큼 되돌려 준다. */
|
|
92
|
+
cursor: unknown
|
|
93
|
+
fetch: (cfg: Record<string, any>, site: { siteId: string }, scope: string, since: number) => Promise<InboundBatch>
|
|
94
|
+
ingest: (records: unknown[]) => { applied: number; duplicates: number; rejected: unknown[] }
|
|
95
|
+
/**
|
|
96
|
+
* 넣기 **직전에** 부른다 — 이 쪽의 레코드를 넘긴다.
|
|
97
|
+
*
|
|
98
|
+
* 중복 판정 기억을 저널에서 채우는 자리다(§`fill-dedupe.ts`). 그 기억은 프로세스 안에만 있어서
|
|
99
|
+
* 재기동하면 비워지고, 그러면 이미 저널에 있는 사실도 「처음 본 것」이 된다.
|
|
100
|
+
*
|
|
101
|
+
* `ingest` 는 동기라 여기서 채울 수 없다. 이 훅은 비동기이므로 저널을 읽을 수 있다.
|
|
102
|
+
*
|
|
103
|
+
* 실패해도 넣기를 멈추지 않는다 — 기억을 못 채운 것이 사실을 못 받을 이유는 아니다.
|
|
104
|
+
*/
|
|
105
|
+
beforeIngest?: (records: unknown[]) => Promise<void>
|
|
106
|
+
saveCursor: (cursor: Record<string, unknown>) => Promise<void>
|
|
107
|
+
maxPages?: number
|
|
108
|
+
}): Promise<FillOutcome> {
|
|
109
|
+
const { cfg, site, scope, cursorKey, fetch, ingest, saveCursor , beforeIngest } = args
|
|
110
|
+
const maxPages = args.maxPages ?? MAX_PAGES
|
|
111
|
+
|
|
112
|
+
let cursor = args.cursor
|
|
113
|
+
let applied = 0
|
|
114
|
+
let duplicates = 0
|
|
115
|
+
let rejectedCount = 0
|
|
116
|
+
const rejectedReasons: Record<string, number> = {}
|
|
117
|
+
let vocabulary = 0
|
|
118
|
+
const undated: Record<string, number> = {}
|
|
119
|
+
/* 원본이 말한 번호 범위 — 마지막으로 받은 묶음의 것을 들고 나간다. 안 말하면 끝까지 없다. */
|
|
120
|
+
let sourceRange: { first?: number; last?: number } | undefined
|
|
121
|
+
/* 이 실행에서 가장 낮게 받아들인 번호 — 커서에 firstSeq 로 적는다. */
|
|
122
|
+
let lowestSeq: number | undefined
|
|
123
|
+
let pages = 0
|
|
124
|
+
|
|
125
|
+
for (; pages < maxPages; pages++) {
|
|
126
|
+
const since = seqOf(cursor, cursorKey) ?? 0
|
|
127
|
+
|
|
128
|
+
let batch: InboundBatch
|
|
129
|
+
try {
|
|
130
|
+
batch = await fetch(cfg, site, scope, since)
|
|
131
|
+
} catch (e: any) {
|
|
132
|
+
return { applied, duplicates, rejected: rejectedCount, ...(Object.keys(rejectedReasons).length ? { rejectedReasons } : {}), vocabulary, ...(Object.keys(undated).length ? { undated } : {}), pages, ...seqPart(cursor, cursorKey), ...(sourceRange ? { sourceRange } : {}), ...(lowestSeq !== undefined ? { lowestSeq } : {}), stopped: 'refused', detail: String(e?.message ?? e) }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (batch?.sourceRange) sourceRange = batch.sourceRange
|
|
136
|
+
|
|
137
|
+
const items = batch?.items ?? []
|
|
138
|
+
/* 빈 답은 더 줄 것이 없다는 뜻이다. 오류가 아니다. */
|
|
139
|
+
if (!items.length) return { applied, duplicates, rejected: rejectedCount, ...(Object.keys(rejectedReasons).length ? { rejectedReasons } : {}), vocabulary, ...(Object.keys(undated).length ? { undated } : {}), pages, ...seqPart(cursor, cursorKey), ...(sourceRange ? { sourceRange } : {}), ...(lowestSeq !== undefined ? { lowestSeq } : {}), stopped: 'done' }
|
|
140
|
+
|
|
141
|
+
const taken = takeInSequence({ items, scope, lastSeq: seqOf(cursor, cursorKey) })
|
|
142
|
+
if (taken.refused) {
|
|
143
|
+
return { applied, duplicates, rejected: rejectedCount, ...(Object.keys(rejectedReasons).length ? { rejectedReasons } : {}), vocabulary, ...(Object.keys(undated).length ? { undated } : {}), pages, ...seqPart(cursor, cursorKey), ...(sourceRange ? { sourceRange } : {}), ...(lowestSeq !== undefined ? { lowestSeq } : {}), stopped: 'refused', detail: taken.refused }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (taken.records.length) {
|
|
147
|
+
if (beforeIngest) {
|
|
148
|
+
await beforeIngest(taken.records).catch(e =>
|
|
149
|
+
twinWarn(`[twin-fill] 중복 판정 기억을 못 채웠다 — ${String(e?.message ?? e)}. 넣기는 그대로 간다`)
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
const result = ingest(taken.records)
|
|
153
|
+
applied += result.applied
|
|
154
|
+
duplicates += result.duplicates ?? 0
|
|
155
|
+
rejectedCount += result.rejected?.length ?? 0
|
|
156
|
+
for (const r of result.rejected ?? []) {
|
|
157
|
+
/* 사유가 여럿이면 첫 줄로 묶는다 — 뒤는 대개 같은 원인의 곁가지다. */
|
|
158
|
+
const why = String((r as any)?.errors?.[0] ?? '(사유 없음)').slice(0, 120)
|
|
159
|
+
rejectedReasons[why] = (rejectedReasons[why] ?? 0) + 1
|
|
160
|
+
}
|
|
161
|
+
vocabulary += (result as any).vocabulary ?? 0
|
|
162
|
+
for (const [k, n] of Object.entries((result as any).undated ?? {})) undated[k] = (undated[k] ?? 0) + (n as number)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/*
|
|
166
|
+
* **번호가 나아가야 다음 쪽을 요청한다.** 원본이 빈 묶음에 물어본 번호를 그대로 돌려주는 계약이면,
|
|
167
|
+
* 나아감을 보지 않고 요청하면 무한히 돈다.
|
|
168
|
+
*/
|
|
169
|
+
if (taken.advancedSeq === undefined || taken.advancedSeq <= since) {
|
|
170
|
+
if (taken.gap) return { applied, duplicates, rejected: rejectedCount, ...(Object.keys(rejectedReasons).length ? { rejectedReasons } : {}), vocabulary, ...(Object.keys(undated).length ? { undated } : {}), pages: pages + 1, ...seqPart(cursor, cursorKey), ...(sourceRange ? { sourceRange } : {}), ...(lowestSeq !== undefined ? { lowestSeq } : {}), stopped: 'gap', detail: gapWord(taken.gap) }
|
|
171
|
+
return {
|
|
172
|
+
applied,
|
|
173
|
+
duplicates,
|
|
174
|
+
rejected: rejectedCount, ...(Object.keys(rejectedReasons).length ? { rejectedReasons } : {}),
|
|
175
|
+
vocabulary,
|
|
176
|
+
...(Object.keys(undated).length ? { undated } : {}),
|
|
177
|
+
pages: pages + 1,
|
|
178
|
+
...seqPart(cursor, cursorKey),
|
|
179
|
+
...(sourceRange ? { sourceRange } : {}), ...(lowestSeq !== undefined ? { lowestSeq } : {}),
|
|
180
|
+
stopped: 'no-progress',
|
|
181
|
+
detail: `번호가 ${since} 에서 나아가지 않았다 — 같은 것을 다시 받았다`
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (lowestSeq === undefined) lowestSeq = taken.lowestSeq
|
|
186
|
+
/* 밑에서부터 다시 받았으면 그 번호가 곧 우리가 가진 가장 낮은 번호다. */
|
|
187
|
+
cursor = withSeq(cursor, cursorKey, taken.advancedSeq, taken.firstSeq ?? lowestSeq)
|
|
188
|
+
await saveCursor(cursor as Record<string, unknown>).catch(err =>
|
|
189
|
+
twinWarn(`[twin-fill] 번호를 커서에 적지 못했다 (${cursorKey}=${taken.advancedSeq}) — ${err?.message ?? err}`)
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
/*
|
|
193
|
+
* **구멍이 있으면 멈춘다.** 원본의 아웃박스에 구멍이 있는 것이고, 이어 받으면 커서가 그 번호를
|
|
194
|
+
* 지나쳐 적혀 빠진 사실이 다시 오지 않는다.
|
|
195
|
+
*/
|
|
196
|
+
if (taken.gap) {
|
|
197
|
+
return { applied, duplicates, rejected: rejectedCount, ...(Object.keys(rejectedReasons).length ? { rejectedReasons } : {}), vocabulary, ...(Object.keys(undated).length ? { undated } : {}), pages: pages + 1, ...seqPart(cursor, cursorKey), ...(sourceRange ? { sourceRange } : {}), ...(lowestSeq !== undefined ? { lowestSeq } : {}), stopped: 'gap', detail: gapWord(taken.gap) }
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/* 상한에 닿았다 — 조용히 끝내지 않는다. 사람이 다 받은 줄로 읽으면 안 된다. */
|
|
202
|
+
return {
|
|
203
|
+
applied,
|
|
204
|
+
duplicates,
|
|
205
|
+
rejected: rejectedCount, ...(Object.keys(rejectedReasons).length ? { rejectedReasons } : {}),
|
|
206
|
+
vocabulary,
|
|
207
|
+
...(Object.keys(undated).length ? { undated } : {}),
|
|
208
|
+
pages,
|
|
209
|
+
...seqPart(cursor, cursorKey),
|
|
210
|
+
...(sourceRange ? { sourceRange } : {}), ...(lowestSeq !== undefined ? { lowestSeq } : {}),
|
|
211
|
+
stopped: 'page-limit',
|
|
212
|
+
detail: `${maxPages}쪽까지 받고 멈췄다 — 남은 것이 있을 수 있다. 다음 주기가 이어 받는다`
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function seqPart(cursor: unknown, key: string): { lastSeq?: number } {
|
|
217
|
+
const seq = seqOf(cursor, key)
|
|
218
|
+
return seq === undefined ? {} : { lastSeq: seq }
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function gapWord(gap: { expectedSeq: number; lastSeq: number }): string {
|
|
222
|
+
return `번호가 비었다 — ${gap.lastSeq} 다음이 ${gap.expectedSeq} 이어야 한다. 원본의 아웃박스에 구멍이 있다`
|
|
223
|
+
}
|