@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.
- 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 +12 -20
- 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 +206 -8
- package/dist-server/service/actuation/command-dispatcher.js +191 -14
- package/dist-server/service/actuation/command-dispatcher.js.map +1 -1
- package/dist-server/service/actuation/command-store.d.ts +10 -2
- package/dist-server/service/actuation/command-store.js +168 -5
- package/dist-server/service/actuation/command-store.js.map +1 -1
- 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 +21 -2
- package/dist-server/service/actuation/index.js +21 -2
- package/dist-server/service/actuation/index.js.map +1 -1
- 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 +81 -0
- package/dist-server/service/actuation/twin-command.js +60 -0
- package/dist-server/service/actuation/twin-command.js.map +1 -1
- 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 +8 -0
- 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 +88 -4
- package/dist-server/service/reference/hook-contract.js +105 -4
- 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/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 +291 -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 +22 -0
- package/dist-server/service/reference/reference-hook.js +135 -55
- 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 +12 -24
- 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 +401 -20
- package/server/service/actuation/command-store.ts +182 -8
- 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 +21 -2
- 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 +128 -1
- package/server/service/actuation/twin-now.ts +97 -0
- package/server/service/index.ts +9 -1
- 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 +185 -5
- package/server/service/reference/hook-rejected.ts +53 -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 +327 -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 +162 -56
- 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 +16 -3
- 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 +1 -1
- 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 +1 -1
- 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,141 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.fillSince = fillSince;
|
|
4
|
+
/*
|
|
5
|
+
* **놓친 구간을 backfill 해 채운다** — 밀어 받는 길의 짝.
|
|
6
|
+
*
|
|
7
|
+
* ── 왜 필요한가 (2026-08-31, 인티그레이션 레인 지적) ──────────────────────
|
|
8
|
+
* 훅은 반드시 놓친다 — 우리가 내려가 있을 때, 그쪽이 못 보냈을 때, 네트워크가 끊겼을 때. 받는 쪽이
|
|
9
|
+
* 번호로 그것을 **알아채게** 되었지만(§`takeInSequence`), 알아챈 뒤 **그 사이를 채우는 길이 없었다.**
|
|
10
|
+
* 설계가 그 길을 적어 두었는데 만들지 않았다.
|
|
11
|
+
*
|
|
12
|
+
* ── 같은 검사를 지난다 ───────────────────────────────────────────────────
|
|
13
|
+
* backfill 한 것도 `takeInSequence` 를 지난다. 여기서 따로 만들면 두 길이 다른 규칙으로 받고, 한쪽만
|
|
14
|
+
* 고쳐지는 날 어긋난 그 길에서만 사실이 사라진다.
|
|
15
|
+
*
|
|
16
|
+
* ── 멈추는 조건을 셋 둔다 ────────────────────────────────────────────────
|
|
17
|
+
* 되풀이해서 요청하는 경로는 **끝나지 않는 것**이 가장 흔한 결함이다.
|
|
18
|
+
*
|
|
19
|
+
* 빈 답 더 줄 것이 없다. 끝
|
|
20
|
+
* 번호가 안 나아간다 같은 것을 다시 받았다 — 무한히 요청하는 것을 막는다
|
|
21
|
+
* 구멍 원본의 아웃박스에 구멍이 있다. 이어 받으면 커서가 그 번호를 지나쳐 적힌다
|
|
22
|
+
*
|
|
23
|
+
* 그리고 **쪽 상한**을 둔다. 상한에 닿았으면 조용히 끝내지 않고 말한다 — 조용히 자르면 사람은 다 받은
|
|
24
|
+
* 줄로 읽는다.
|
|
25
|
+
*/
|
|
26
|
+
const hook_contract_js_1 = require("./hook-contract.js");
|
|
27
|
+
const log_js_1 = require("../../engine/log.js");
|
|
28
|
+
/** 몇 쪽까지 받나 — 한 번의 채우기가 서버를 오래 붙잡지 않게. */
|
|
29
|
+
const MAX_PAGES = 50;
|
|
30
|
+
/**
|
|
31
|
+
* 그 단위의 놓친 구간을 채운다.
|
|
32
|
+
*
|
|
33
|
+
* `ingest` 를 부르는 쪽이 넘겨 준다 — 훅과 같은 이유다(이 파일이 엔진을 직접 부르면 시험이 엔진을
|
|
34
|
+
* 세워야 한다).
|
|
35
|
+
*/
|
|
36
|
+
async function fillSince(args) {
|
|
37
|
+
const { cfg, site, scope, cursorKey, fetch, ingest, saveCursor, beforeIngest } = args;
|
|
38
|
+
const maxPages = args.maxPages ?? MAX_PAGES;
|
|
39
|
+
let cursor = args.cursor;
|
|
40
|
+
let applied = 0;
|
|
41
|
+
let duplicates = 0;
|
|
42
|
+
let rejectedCount = 0;
|
|
43
|
+
const rejectedReasons = {};
|
|
44
|
+
let vocabulary = 0;
|
|
45
|
+
const undated = {};
|
|
46
|
+
/* 원본이 말한 번호 범위 — 마지막으로 받은 묶음의 것을 들고 나간다. 안 말하면 끝까지 없다. */
|
|
47
|
+
let sourceRange;
|
|
48
|
+
/* 이 실행에서 가장 낮게 받아들인 번호 — 커서에 firstSeq 로 적는다. */
|
|
49
|
+
let lowestSeq;
|
|
50
|
+
let pages = 0;
|
|
51
|
+
for (; pages < maxPages; pages++) {
|
|
52
|
+
const since = (0, hook_contract_js_1.seqOf)(cursor, cursorKey) ?? 0;
|
|
53
|
+
let batch;
|
|
54
|
+
try {
|
|
55
|
+
batch = await fetch(cfg, site, scope, since);
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
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) };
|
|
59
|
+
}
|
|
60
|
+
if (batch?.sourceRange)
|
|
61
|
+
sourceRange = batch.sourceRange;
|
|
62
|
+
const items = batch?.items ?? [];
|
|
63
|
+
/* 빈 답은 더 줄 것이 없다는 뜻이다. 오류가 아니다. */
|
|
64
|
+
if (!items.length)
|
|
65
|
+
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' };
|
|
66
|
+
const taken = (0, hook_contract_js_1.takeInSequence)({ items, scope, lastSeq: (0, hook_contract_js_1.seqOf)(cursor, cursorKey) });
|
|
67
|
+
if (taken.refused) {
|
|
68
|
+
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 };
|
|
69
|
+
}
|
|
70
|
+
if (taken.records.length) {
|
|
71
|
+
if (beforeIngest) {
|
|
72
|
+
await beforeIngest(taken.records).catch(e => (0, log_js_1.twinWarn)(`[twin-fill] 중복 판정 기억을 못 채웠다 — ${String(e?.message ?? e)}. 넣기는 그대로 간다`));
|
|
73
|
+
}
|
|
74
|
+
const result = ingest(taken.records);
|
|
75
|
+
applied += result.applied;
|
|
76
|
+
duplicates += result.duplicates ?? 0;
|
|
77
|
+
rejectedCount += result.rejected?.length ?? 0;
|
|
78
|
+
for (const r of result.rejected ?? []) {
|
|
79
|
+
/* 사유가 여럿이면 첫 줄로 묶는다 — 뒤는 대개 같은 원인의 곁가지다. */
|
|
80
|
+
const why = String(r?.errors?.[0] ?? '(사유 없음)').slice(0, 120);
|
|
81
|
+
rejectedReasons[why] = (rejectedReasons[why] ?? 0) + 1;
|
|
82
|
+
}
|
|
83
|
+
vocabulary += result.vocabulary ?? 0;
|
|
84
|
+
for (const [k, n] of Object.entries(result.undated ?? {}))
|
|
85
|
+
undated[k] = (undated[k] ?? 0) + n;
|
|
86
|
+
}
|
|
87
|
+
/*
|
|
88
|
+
* **번호가 나아가야 다음 쪽을 요청한다.** 원본이 빈 묶음에 물어본 번호를 그대로 돌려주는 계약이면,
|
|
89
|
+
* 나아감을 보지 않고 요청하면 무한히 돈다.
|
|
90
|
+
*/
|
|
91
|
+
if (taken.advancedSeq === undefined || taken.advancedSeq <= since) {
|
|
92
|
+
if (taken.gap)
|
|
93
|
+
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) };
|
|
94
|
+
return {
|
|
95
|
+
applied,
|
|
96
|
+
duplicates,
|
|
97
|
+
rejected: rejectedCount, ...(Object.keys(rejectedReasons).length ? { rejectedReasons } : {}),
|
|
98
|
+
vocabulary,
|
|
99
|
+
...(Object.keys(undated).length ? { undated } : {}),
|
|
100
|
+
pages: pages + 1,
|
|
101
|
+
...seqPart(cursor, cursorKey),
|
|
102
|
+
...(sourceRange ? { sourceRange } : {}), ...(lowestSeq !== undefined ? { lowestSeq } : {}),
|
|
103
|
+
stopped: 'no-progress',
|
|
104
|
+
detail: `번호가 ${since} 에서 나아가지 않았다 — 같은 것을 다시 받았다`
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
if (lowestSeq === undefined)
|
|
108
|
+
lowestSeq = taken.lowestSeq;
|
|
109
|
+
/* 밑에서부터 다시 받았으면 그 번호가 곧 우리가 가진 가장 낮은 번호다. */
|
|
110
|
+
cursor = (0, hook_contract_js_1.withSeq)(cursor, cursorKey, taken.advancedSeq, taken.firstSeq ?? lowestSeq);
|
|
111
|
+
await saveCursor(cursor).catch(err => (0, log_js_1.twinWarn)(`[twin-fill] 번호를 커서에 적지 못했다 (${cursorKey}=${taken.advancedSeq}) — ${err?.message ?? err}`));
|
|
112
|
+
/*
|
|
113
|
+
* **구멍이 있으면 멈춘다.** 원본의 아웃박스에 구멍이 있는 것이고, 이어 받으면 커서가 그 번호를
|
|
114
|
+
* 지나쳐 적혀 빠진 사실이 다시 오지 않는다.
|
|
115
|
+
*/
|
|
116
|
+
if (taken.gap) {
|
|
117
|
+
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) };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/* 상한에 닿았다 — 조용히 끝내지 않는다. 사람이 다 받은 줄로 읽으면 안 된다. */
|
|
121
|
+
return {
|
|
122
|
+
applied,
|
|
123
|
+
duplicates,
|
|
124
|
+
rejected: rejectedCount, ...(Object.keys(rejectedReasons).length ? { rejectedReasons } : {}),
|
|
125
|
+
vocabulary,
|
|
126
|
+
...(Object.keys(undated).length ? { undated } : {}),
|
|
127
|
+
pages,
|
|
128
|
+
...seqPart(cursor, cursorKey),
|
|
129
|
+
...(sourceRange ? { sourceRange } : {}), ...(lowestSeq !== undefined ? { lowestSeq } : {}),
|
|
130
|
+
stopped: 'page-limit',
|
|
131
|
+
detail: `${maxPages}쪽까지 받고 멈췄다 — 남은 것이 있을 수 있다. 다음 주기가 이어 받는다`
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function seqPart(cursor, key) {
|
|
135
|
+
const seq = (0, hook_contract_js_1.seqOf)(cursor, key);
|
|
136
|
+
return seq === undefined ? {} : { lastSeq: seq };
|
|
137
|
+
}
|
|
138
|
+
function gapWord(gap) {
|
|
139
|
+
return `번호가 비었다 — ${gap.lastSeq} 다음이 ${gap.expectedSeq} 이어야 한다. 원본의 아웃박스에 구멍이 있다`;
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=reference-fill.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reference-fill.js","sourceRoot":"","sources":["../../../server/service/reference/reference-fill.ts"],"names":[],"mappings":";;AAmFA,8BAkIC;AArND;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,yDAAmE;AAEnE,gDAA8C;AAkD9C,0CAA0C;AAC1C,MAAM,SAAS,GAAG,EAAE,CAAA;AAEpB;;;;;GAKG;AACI,KAAK,UAAU,SAAS,CAAC,IAwB/B;IACC,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAG,YAAY,EAAE,GAAG,IAAI,CAAA;IACtF,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,IAAI,SAAS,CAAA;IAE3C,IAAI,MAAM,GAAG,IAAI,CAAC,MAAM,CAAA;IACxB,IAAI,OAAO,GAAG,CAAC,CAAA;IACf,IAAI,UAAU,GAAG,CAAC,CAAA;IAClB,IAAI,aAAa,GAAG,CAAC,CAAA;IACrB,MAAM,eAAe,GAA2B,EAAE,CAAA;IAClD,IAAI,UAAU,GAAG,CAAC,CAAA;IAClB,MAAM,OAAO,GAA2B,EAAE,CAAA;IAC1C,0DAA0D;IAC1D,IAAI,WAA0D,CAAA;IAC9D,gDAAgD;IAChD,IAAI,SAA6B,CAAA;IACjC,IAAI,KAAK,GAAG,CAAC,CAAA;IAEb,OAAO,KAAK,GAAG,QAAQ,EAAE,KAAK,EAAE,EAAE,CAAC;QACjC,MAAM,KAAK,GAAG,IAAA,wBAAK,EAAC,MAAM,EAAE,SAAS,CAAC,IAAI,CAAC,CAAA;QAE3C,IAAI,KAAmB,CAAA;QACvB,IAAI,CAAC;YACH,KAAK,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,CAAA;QAC9C,CAAC;QAAC,OAAO,CAAM,EAAE,CAAC;YAChB,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,eAAe,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,SAAS,CAAC,EAAE,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,OAAO,IAAI,CAAC,CAAC,EAAE,CAAA;QACtX,CAAC;QAED,IAAI,KAAK,EAAE,WAAW;YAAE,WAAW,GAAG,KAAK,CAAC,WAAW,CAAA;QAEvD,MAAM,KAAK,GAAG,KAAK,EAAE,KAAK,IAAI,EAAE,CAAA;QAChC,mCAAmC;QACnC,IAAI,CAAC,KAAK,CAAC,MAAM;YAAE,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,eAAe,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,SAAS,CAAC,EAAE,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,CAAA;QAEnW,MAAM,KAAK,GAAG,IAAA,iCAAc,EAAC,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,IAAA,wBAAK,EAAC,MAAM,EAAE,SAAS,CAAC,EAAE,CAAC,CAAA;QACjF,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;YAClB,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,eAAe,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,SAAS,CAAC,EAAE,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,OAAO,EAAE,CAAA;QAC5W,CAAC;QAED,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;YACzB,IAAI,YAAY,EAAE,CAAC;gBACjB,MAAM,YAAY,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAC1C,IAAA,iBAAQ,EAAC,iCAAiC,MAAM,CAAC,CAAC,EAAE,OAAO,IAAI,CAAC,CAAC,cAAc,CAAC,CACjF,CAAA;YACH,CAAC;YACD,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;YACpC,OAAO,IAAI,MAAM,CAAC,OAAO,CAAA;YACzB,UAAU,IAAI,MAAM,CAAC,UAAU,IAAI,CAAC,CAAA;YACpC,aAAa,IAAI,MAAM,CAAC,QAAQ,EAAE,MAAM,IAAI,CAAC,CAAA;YAC7C,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,QAAQ,IAAI,EAAE,EAAE,CAAC;gBACtC,4CAA4C;gBAC5C,MAAM,GAAG,GAAG,MAAM,CAAE,CAAS,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,IAAI,SAAS,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAA;gBACtE,eAAe,CAAC,GAAG,CAAC,GAAG,CAAC,eAAe,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAA;YACxD,CAAC;YACD,UAAU,IAAK,MAAc,CAAC,UAAU,IAAI,CAAC,CAAA;YAC7C,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAE,MAAc,CAAC,OAAO,IAAI,EAAE,CAAC;gBAAE,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,GAAI,CAAY,CAAA;QACpH,CAAC;QAED;;;WAGG;QACH,IAAI,KAAK,CAAC,WAAW,KAAK,SAAS,IAAI,KAAK,CAAC,WAAW,IAAI,KAAK,EAAE,CAAC;YAClE,IAAI,KAAK,CAAC,GAAG;gBAAE,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,eAAe,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,KAAK,GAAG,CAAC,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,SAAS,CAAC,EAAE,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAA;YACrY,OAAO;gBACL,OAAO;gBACP,UAAU;gBACV,QAAQ,EAAE,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,eAAe,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC5F,UAAU;gBACV,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACnD,KAAK,EAAE,KAAK,GAAG,CAAC;gBAChB,GAAG,OAAO,CAAC,MAAM,EAAE,SAAS,CAAC;gBAC7B,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC1F,OAAO,EAAE,aAAa;gBACtB,MAAM,EAAE,OAAO,KAAK,6BAA6B;aAClD,CAAA;QACH,CAAC;QAED,IAAI,SAAS,KAAK,SAAS;YAAE,SAAS,GAAG,KAAK,CAAC,SAAS,CAAA;QACxD,6CAA6C;QAC7C,MAAM,GAAG,IAAA,0BAAO,EAAC,MAAM,EAAE,SAAS,EAAE,KAAK,CAAC,WAAW,EAAE,KAAK,CAAC,QAAQ,IAAI,SAAS,CAAC,CAAA;QACnF,MAAM,UAAU,CAAC,MAAiC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAC9D,IAAA,iBAAQ,EAAC,+BAA+B,SAAS,IAAI,KAAK,CAAC,WAAW,OAAO,GAAG,EAAE,OAAO,IAAI,GAAG,EAAE,CAAC,CACpG,CAAA;QAED;;;WAGG;QACH,IAAI,KAAK,CAAC,GAAG,EAAE,CAAC;YACd,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,eAAe,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,KAAK,GAAG,CAAC,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,SAAS,CAAC,EAAE,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAA;QACxX,CAAC;IACH,CAAC;IAED,kDAAkD;IAClD,OAAO;QACL,OAAO;QACP,UAAU;QACV,QAAQ,EAAE,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,eAAe,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC5F,UAAU;QACV,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACnD,KAAK;QACL,GAAG,OAAO,CAAC,MAAM,EAAE,SAAS,CAAC;QAC7B,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC1F,OAAO,EAAE,YAAY;QACrB,MAAM,EAAE,GAAG,QAAQ,2CAA2C;KAC/D,CAAA;AACH,CAAC;AAED,SAAS,OAAO,CAAC,MAAe,EAAE,GAAW;IAC3C,MAAM,GAAG,GAAG,IAAA,wBAAK,EAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC9B,OAAO,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,CAAA;AAClD,CAAC;AAED,SAAS,OAAO,CAAC,GAA6C;IAC5D,OAAO,aAAa,GAAG,CAAC,OAAO,QAAQ,GAAG,CAAC,WAAW,2BAA2B,CAAA;AACnF,CAAC","sourcesContent":["/*\n * **놓친 구간을 backfill 해 채운다** — 밀어 받는 길의 짝.\n *\n * ── 왜 필요한가 (2026-08-31, 인티그레이션 레인 지적) ──────────────────────\n * 훅은 반드시 놓친다 — 우리가 내려가 있을 때, 그쪽이 못 보냈을 때, 네트워크가 끊겼을 때. 받는 쪽이\n * 번호로 그것을 **알아채게** 되었지만(§`takeInSequence`), 알아챈 뒤 **그 사이를 채우는 길이 없었다.**\n * 설계가 그 길을 적어 두었는데 만들지 않았다.\n *\n * ── 같은 검사를 지난다 ───────────────────────────────────────────────────\n * backfill 한 것도 `takeInSequence` 를 지난다. 여기서 따로 만들면 두 길이 다른 규칙으로 받고, 한쪽만\n * 고쳐지는 날 어긋난 그 길에서만 사실이 사라진다.\n *\n * ── 멈추는 조건을 셋 둔다 ────────────────────────────────────────────────\n * 되풀이해서 요청하는 경로는 **끝나지 않는 것**이 가장 흔한 결함이다.\n *\n * 빈 답 더 줄 것이 없다. 끝\n * 번호가 안 나아간다 같은 것을 다시 받았다 — 무한히 요청하는 것을 막는다\n * 구멍 원본의 아웃박스에 구멍이 있다. 이어 받으면 커서가 그 번호를 지나쳐 적힌다\n *\n * 그리고 **쪽 상한**을 둔다. 상한에 닿았으면 조용히 끝내지 않고 말한다 — 조용히 자르면 사람은 다 받은\n * 줄로 읽는다.\n */\nimport { seqOf, takeInSequence, withSeq } from './hook-contract.js'\nimport type { InboundBatch } from './reference-adapter.js'\nimport { twinWarn } from '../../engine/log.js'\n\n/** 한 번 채우고 나서 남는 사실. */\nexport interface FillOutcome {\n /** 트윈에 넣은 레코드 수. */\n applied: number\n /**\n * ingest 가 낸 네 갈래의 합 — **두 갈래로는 셈이 안 닫힌다.**\n *\n * ```\n * 두 갈래 157 − 46 − 2 = 109 「설명 못 함」 진실은 되풀이 108 + 어휘 1\n * 네 갈래 157 − 46 − 2 − 108 − 1 = 0\n * ```\n *\n * 이 값이 없으면 원본 쪽에서 「보낸 것 20개 중 event 가 9개인데 나머지는 어디 갔나」를 셀 수\n * 없다. 실제로 인티그레이션 레인이 그것을 물었고, 그 수가 트윈에 저장되지 않아 답을 못 했다.\n */\n duplicates: number\n rejected: number\n /**\n * 왜 거절됐나 — **사유별 수.** 수만 내면 「26건이 안 들어온다」에서 멈춘다.\n *\n * 목록 전체가 아니라 사유별로 센다. 같은 이유로 거절된 26건이 26줄이면 읽는 사람이 그 줄을\n * 세어야 하고, 다른 이유 하나가 그 안에 묻힌다.\n */\n rejectedReasons?: Record<string, number>\n vocabulary: number\n /** 시각을 못 읽어 ingest 시각으로 떨어진 것 — 종류별. 비어 있으면 전부 자기 시각을 말했다. */\n undated?: Record<string, number>\n /** 몇 쪽을 받았나. */\n pages: number\n /** 커서가 어디까지 갔나 — 아무것도 못 받았으면 없다. */\n lastSeq?: number\n /**\n * 이 실행에서 **가장 낮게 받아들인 번호** — 밑에서부터 다시 받았을 때 이것이 곧 「우리가 가진\n * 가장 낮은 번호」다.\n */\n lowestSeq?: number\n /** 왜 멈췄나 — `done` · `gap` · `no-progress` · `page-limit` · `refused`. */\n stopped: 'done' | 'gap' | 'no-progress' | 'page-limit' | 'refused'\n /** 멈춘 이유의 사람 말. 알리지 않고 끝내지 않는다. */\n detail?: string\n /**\n * 마지막으로 받은 묶음이 말한 **원본의 번호 범위** — 원본이 안 말하면 없다.\n *\n * 우리 커서와 견주는 자리가 이 값을 쓴다. 없으면 「맞다」가 아니라 **「모른다」**로 답해야 한다.\n */\n sourceRange?: { first?: number; last?: number }\n}\n\n/** 몇 쪽까지 받나 — 한 번의 채우기가 서버를 오래 붙잡지 않게. */\nconst MAX_PAGES = 50\n\n/**\n * 그 단위의 놓친 구간을 채운다.\n *\n * `ingest` 를 부르는 쪽이 넘겨 준다 — 훅과 같은 이유다(이 파일이 엔진을 직접 부르면 시험이 엔진을\n * 세워야 한다).\n */\nexport async function fillSince(args: {\n cfg: Record<string, any>\n site: { siteId: string }\n /** 번호를 매기는 단위 — 커서 열쇠의 절반. 커넥터가 만들지 않는다. */\n scope: string\n /** 커서에 적힌 열쇠 — `pushCursorKey(instanceId, scope)`. */\n cursorKey: string\n /** 지금 커서 — 이 함수가 읽고, 나아간 만큼 되돌려 준다. */\n cursor: unknown\n fetch: (cfg: Record<string, any>, site: { siteId: string }, scope: string, since: number) => Promise<InboundBatch>\n ingest: (records: unknown[]) => { applied: number; duplicates: number; rejected: unknown[] }\n /**\n * 넣기 **직전에** 부른다 — 이 쪽의 레코드를 넘긴다.\n *\n * 중복 판정 기억을 저널에서 채우는 자리다(§`fill-dedupe.ts`). 그 기억은 프로세스 안에만 있어서\n * 재기동하면 비워지고, 그러면 이미 저널에 있는 사실도 「처음 본 것」이 된다.\n *\n * `ingest` 는 동기라 여기서 채울 수 없다. 이 훅은 비동기이므로 저널을 읽을 수 있다.\n *\n * 실패해도 넣기를 멈추지 않는다 — 기억을 못 채운 것이 사실을 못 받을 이유는 아니다.\n */\n beforeIngest?: (records: unknown[]) => Promise<void>\n saveCursor: (cursor: Record<string, unknown>) => Promise<void>\n maxPages?: number\n}): Promise<FillOutcome> {\n const { cfg, site, scope, cursorKey, fetch, ingest, saveCursor , beforeIngest } = args\n const maxPages = args.maxPages ?? MAX_PAGES\n\n let cursor = args.cursor\n let applied = 0\n let duplicates = 0\n let rejectedCount = 0\n const rejectedReasons: Record<string, number> = {}\n let vocabulary = 0\n const undated: Record<string, number> = {}\n /* 원본이 말한 번호 범위 — 마지막으로 받은 묶음의 것을 들고 나간다. 안 말하면 끝까지 없다. */\n let sourceRange: { first?: number; last?: number } | undefined\n /* 이 실행에서 가장 낮게 받아들인 번호 — 커서에 firstSeq 로 적는다. */\n let lowestSeq: number | undefined\n let pages = 0\n\n for (; pages < maxPages; pages++) {\n const since = seqOf(cursor, cursorKey) ?? 0\n\n let batch: InboundBatch\n try {\n batch = await fetch(cfg, site, scope, since)\n } catch (e: any) {\n 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) }\n }\n\n if (batch?.sourceRange) sourceRange = batch.sourceRange\n\n const items = batch?.items ?? []\n /* 빈 답은 더 줄 것이 없다는 뜻이다. 오류가 아니다. */\n 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' }\n\n const taken = takeInSequence({ items, scope, lastSeq: seqOf(cursor, cursorKey) })\n if (taken.refused) {\n 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 }\n }\n\n if (taken.records.length) {\n if (beforeIngest) {\n await beforeIngest(taken.records).catch(e =>\n twinWarn(`[twin-fill] 중복 판정 기억을 못 채웠다 — ${String(e?.message ?? e)}. 넣기는 그대로 간다`)\n )\n }\n const result = ingest(taken.records)\n applied += result.applied\n duplicates += result.duplicates ?? 0\n rejectedCount += result.rejected?.length ?? 0\n for (const r of result.rejected ?? []) {\n /* 사유가 여럿이면 첫 줄로 묶는다 — 뒤는 대개 같은 원인의 곁가지다. */\n const why = String((r as any)?.errors?.[0] ?? '(사유 없음)').slice(0, 120)\n rejectedReasons[why] = (rejectedReasons[why] ?? 0) + 1\n }\n vocabulary += (result as any).vocabulary ?? 0\n for (const [k, n] of Object.entries((result as any).undated ?? {})) undated[k] = (undated[k] ?? 0) + (n as number)\n }\n\n /*\n * **번호가 나아가야 다음 쪽을 요청한다.** 원본이 빈 묶음에 물어본 번호를 그대로 돌려주는 계약이면,\n * 나아감을 보지 않고 요청하면 무한히 돈다.\n */\n if (taken.advancedSeq === undefined || taken.advancedSeq <= since) {\n 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) }\n return {\n applied,\n duplicates,\n rejected: rejectedCount, ...(Object.keys(rejectedReasons).length ? { rejectedReasons } : {}),\n vocabulary,\n ...(Object.keys(undated).length ? { undated } : {}),\n pages: pages + 1,\n ...seqPart(cursor, cursorKey),\n ...(sourceRange ? { sourceRange } : {}), ...(lowestSeq !== undefined ? { lowestSeq } : {}),\n stopped: 'no-progress',\n detail: `번호가 ${since} 에서 나아가지 않았다 — 같은 것을 다시 받았다`\n }\n }\n\n if (lowestSeq === undefined) lowestSeq = taken.lowestSeq\n /* 밑에서부터 다시 받았으면 그 번호가 곧 우리가 가진 가장 낮은 번호다. */\n cursor = withSeq(cursor, cursorKey, taken.advancedSeq, taken.firstSeq ?? lowestSeq)\n await saveCursor(cursor as Record<string, unknown>).catch(err =>\n twinWarn(`[twin-fill] 번호를 커서에 적지 못했다 (${cursorKey}=${taken.advancedSeq}) — ${err?.message ?? err}`)\n )\n\n /*\n * **구멍이 있으면 멈춘다.** 원본의 아웃박스에 구멍이 있는 것이고, 이어 받으면 커서가 그 번호를\n * 지나쳐 적혀 빠진 사실이 다시 오지 않는다.\n */\n if (taken.gap) {\n 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) }\n }\n }\n\n /* 상한에 닿았다 — 조용히 끝내지 않는다. 사람이 다 받은 줄로 읽으면 안 된다. */\n return {\n applied,\n duplicates,\n rejected: rejectedCount, ...(Object.keys(rejectedReasons).length ? { rejectedReasons } : {}),\n vocabulary,\n ...(Object.keys(undated).length ? { undated } : {}),\n pages,\n ...seqPart(cursor, cursorKey),\n ...(sourceRange ? { sourceRange } : {}), ...(lowestSeq !== undefined ? { lowestSeq } : {}),\n stopped: 'page-limit',\n detail: `${maxPages}쪽까지 받고 멈췄다 — 남은 것이 있을 수 있다. 다음 주기가 이어 받는다`\n }\n}\n\nfunction seqPart(cursor: unknown, key: string): { lastSeq?: number } {\n const seq = seqOf(cursor, key)\n return seq === undefined ? {} : { lastSeq: seq }\n}\n\nfunction gapWord(gap: { expectedSeq: number; lastSeq: number }): string {\n return `번호가 비었다 — ${gap.lastSeq} 다음이 ${gap.expectedSeq} 이어야 한다. 원본의 아웃박스에 구멍이 있다`\n}\n"]}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { rejectedForCaller, type RejectedForCaller } from './hook-rejected.js';
|
|
1
2
|
import { type HookOutcome } from './hook-contract.js';
|
|
2
3
|
/**
|
|
3
4
|
* 훅 하나를 처리한다 — 찾고 · 확인하고 · 커넥터에게 옮기게 하고 · 유입으로 넘긴다.
|
|
@@ -22,6 +23,15 @@ export declare function handleHook(args: {
|
|
|
22
23
|
nowMs?: number;
|
|
23
24
|
/** 레코드를 트윈에 넣는다 — 받은 수와 **떨어진 것들**을 돌려준다. */
|
|
24
25
|
ingest: (instanceId: string, records: unknown[]) => IngestResult;
|
|
26
|
+
/**
|
|
27
|
+
* 구멍을 봤다 — **부르는 쪽이 무엇을 할지 정한다.**
|
|
28
|
+
*
|
|
29
|
+
* 여기서 backfill 을 직접 부르지 않는다. 이 파일은 `ingest`·`store` 를 인자로 받아 가볍게 유지한다
|
|
30
|
+
* (그래야 테스트가 엔진 없이 이 판정을 확인할 수 있다). backfill 을 import 하면 그 규율이 깨진다.
|
|
31
|
+
*
|
|
32
|
+
* 기다리지 않는 것도 부르는 쪽의 몫이다 — 훅의 응답이 늦으면 보내는 쪽이 되풀이한다.
|
|
33
|
+
*/
|
|
34
|
+
onGap?: (scope: string) => void;
|
|
25
35
|
/**
|
|
26
36
|
* 저장소를 대신 쓰는 자리 — **시험이 재기동을 넣을 수 있게.**
|
|
27
37
|
*
|
|
@@ -44,6 +54,18 @@ export interface IngestResult {
|
|
|
44
54
|
record: unknown;
|
|
45
55
|
errors: string[];
|
|
46
56
|
}[];
|
|
57
|
+
/**
|
|
58
|
+
* How many records were vocabulary rather than events — **needed so the reply's sum closes.**
|
|
59
|
+
*
|
|
60
|
+
* `liveIngest` has counted this all along and this boundary dropped it, because the callback was
|
|
61
|
+
* typed as `IngestResult` and vocabulary was not on it. So `offered` counted those records and
|
|
62
|
+
* none of the other three did, and a sender checking the sum would have seen a loss on ordinary
|
|
63
|
+
* traffic (§`WebhookResponse.offered`).
|
|
64
|
+
*
|
|
65
|
+
* Optional because an ingest that handles no vocabulary need not say zero, and a caller that
|
|
66
|
+
* simply does not know must not be made to claim none.
|
|
67
|
+
*/
|
|
68
|
+
vocabulary?: number;
|
|
47
69
|
}
|
|
48
70
|
/**
|
|
49
71
|
* 훅이 저장소에 닿는 두 자리 — 연결을 찾는 것과 커서를 적는 것.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.rejectedForCaller = void 0;
|
|
3
4
|
exports.handleHook = handleHook;
|
|
4
5
|
/*
|
|
5
6
|
* 연결된 시스템이 **밀어 주는** 사실을 받는 자리 — 웹훅.
|
|
@@ -48,6 +49,9 @@ exports.handleHook = handleHook;
|
|
|
48
49
|
const ops_contract_1 = require("@operato/ops-contract");
|
|
49
50
|
const log_js_1 = require("../../engine/log.js");
|
|
50
51
|
const reference_adapter_js_1 = require("./reference-adapter.js");
|
|
52
|
+
const hook_rejected_js_1 = require("./hook-rejected.js");
|
|
53
|
+
var hook_rejected_js_2 = require("./hook-rejected.js");
|
|
54
|
+
Object.defineProperty(exports, "rejectedForCaller", { enumerable: true, get: function () { return hook_rejected_js_2.rejectedForCaller; } });
|
|
51
55
|
const hook_contract_js_1 = require("./hook-contract.js");
|
|
52
56
|
/**
|
|
53
57
|
* 훅 하나를 처리한다 — 찾고 · 확인하고 · 커넥터에게 옮기게 하고 · 유입으로 넘긴다.
|
|
@@ -56,7 +60,7 @@ const hook_contract_js_1 = require("./hook-contract.js");
|
|
|
56
60
|
* 그러면 규약을 확인하는 시험을 쓸 수 없다.
|
|
57
61
|
*/
|
|
58
62
|
async function handleHook(args) {
|
|
59
|
-
const { domainId, source, instanceId, headers, body, rawBody, ingest } = args;
|
|
63
|
+
const { domainId, source, instanceId, headers, body, rawBody, ingest, onGap } = args;
|
|
60
64
|
const nowMs = args.nowMs ?? Date.now();
|
|
61
65
|
const { store } = args;
|
|
62
66
|
const ref = await store.load(domainId, source);
|
|
@@ -112,63 +116,60 @@ async function handleHook(args) {
|
|
|
112
116
|
* **번호 확인** — 이 자리가 프레임워크인 이유는 밀어 주는 모든 연결에 같은 규율이라야 하기 때문이다.
|
|
113
117
|
* 커넥터마다 만들면 한 곳이 빠지고, 빠진 그 연결에서만 사실이 없어진다.
|
|
114
118
|
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
119
|
+
* 판정은 `takeInSequence` 하나가 한다 — **backfill 하는 길도 같은 함수를 지난다.** 여기서 따로 만들면
|
|
120
|
+
* 두 길이 다른 규칙으로 받고, 한쪽만 고쳐지는 날이 온다.
|
|
117
121
|
*/
|
|
118
122
|
const scope = typeof batch.scope === 'string' && batch.scope.trim() ? batch.scope.trim() : undefined;
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
123
|
+
/* 열쇠에 트윈을 넣는다 — 연결 하나가 트윈 여럿을 만들고 커서는 연결마다 한 행이다. */
|
|
124
|
+
const key = scope ? (0, hook_contract_js_1.pushCursorKey)(instanceId, scope) : undefined;
|
|
125
|
+
const taken = (0, hook_contract_js_1.takeInSequence)({
|
|
126
|
+
items,
|
|
127
|
+
scope,
|
|
128
|
+
lastSeq: key ? (0, hook_contract_js_1.seqOf)(ref.liveCursor, key) : undefined
|
|
129
|
+
});
|
|
130
|
+
if (taken.refused) {
|
|
131
|
+
/* 반쯤 확인한 것을 확인했다고 말하지 않는다. */
|
|
132
|
+
return { status: ops_contract_1.WEBHOOK_STATUS.badPayload, body: { ok: false, error: taken.refused } };
|
|
126
133
|
}
|
|
127
|
-
const numbered =
|
|
134
|
+
const numbered = taken.numbered;
|
|
135
|
+
const gap = taken.gap;
|
|
128
136
|
/*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
137
|
+
* 첫 봉투가 1번이 아니면 **그 사실을 남긴다.** 커서가 없는 연결에 21번이 오면 커서가 21 로 뛰고,
|
|
138
|
+
* 그 앞 스무 건은 구멍으로도 안 세어진다. 실제로 plant 에서 그렇게 스무 건이 조용히 사라졌다.
|
|
139
|
+
*
|
|
140
|
+
* 손실이라고 말하지 않는다 — 늦게 붙은 트윈은 그 앞을 안 받기로 한 것이다. 수만 말하고 판단은
|
|
141
|
+
* 사람이 한다.
|
|
131
142
|
*/
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
/* 열쇠에 트윈을 넣는다 — 연결 하나가 트윈 여럿을 만들고 커서는 연결마다 한 행이다. */
|
|
137
|
-
const key = (0, hook_contract_js_1.pushCursorKey)(instanceId, scope);
|
|
138
|
-
let cursor = (0, hook_contract_js_1.seqOf)(ref.liveCursor, key);
|
|
139
|
-
take = 0;
|
|
140
|
-
for (const item of items) {
|
|
141
|
-
let verdict;
|
|
142
|
-
try {
|
|
143
|
-
verdict = (0, ops_contract_1.checkSequence)(cursor, item.seq);
|
|
144
|
-
}
|
|
145
|
-
catch (e) {
|
|
146
|
-
return { status: ops_contract_1.WEBHOOK_STATUS.badPayload, body: { ok: false, error: e?.message ?? 'bad sequence number' } };
|
|
147
|
-
}
|
|
148
|
-
if (verdict.kind === 'gap') {
|
|
149
|
-
gap = { expectedSeq: verdict.expectedSeq, lastSeq: verdict.lastSeq };
|
|
150
|
-
break;
|
|
151
|
-
}
|
|
152
|
-
cursor = verdict.lastSeq;
|
|
153
|
-
take++;
|
|
154
|
-
}
|
|
155
|
-
/* 앞의 것을 하나도 못 받았으면 커서를 올릴 것이 없다. */
|
|
156
|
-
if (take > 0 && typeof cursor === 'number')
|
|
157
|
-
advanced = { scope: key, seq: cursor };
|
|
143
|
+
if (taken.firstUnseenBefore) {
|
|
144
|
+
(0, log_js_1.twinWarn)(`[twin-ingest] "${instanceId}" 의 ${scope ?? '(단위 없음)'} 를 ${taken.advancedSeq} 번부터 받기 시작했습니다 — ` +
|
|
145
|
+
`그 앞 ${taken.firstUnseenBefore}건은 받은 적이 없습니다. 원본이 오래 돌고 있었으면 정상이고, ` +
|
|
146
|
+
`보냈는데 못 받은 것이면 그만큼이 빠진 것입니다`);
|
|
158
147
|
}
|
|
159
|
-
const
|
|
148
|
+
const advanced = taken.advancedSeq !== undefined && key ? { scope: key, seq: taken.advancedSeq } : undefined;
|
|
149
|
+
const records = taken.records;
|
|
160
150
|
if (!records.length) {
|
|
161
151
|
/*
|
|
162
152
|
* 옮길 것이 없는 것은 오류가 아니다 — 그쪽이 우리와 무관한 것을 보낸 것일 수 있다. 다만 첫 봉투부터
|
|
163
153
|
* 구멍이면 그것은 받은 것이 없는 것이고, 그 앞부터 달라고 해야 한다.
|
|
164
154
|
*/
|
|
165
|
-
if (gap)
|
|
155
|
+
if (gap) {
|
|
156
|
+
if (scope)
|
|
157
|
+
onGap?.(scope);
|
|
166
158
|
return gapOutcome(scope, gap);
|
|
159
|
+
}
|
|
167
160
|
if (advanced)
|
|
168
|
-
await saveSeq(store, ref, advanced.scope, advanced.seq);
|
|
161
|
+
await saveSeq(store, ref, advanced.scope, advanced.seq, taken.firstSeq);
|
|
169
162
|
return {
|
|
170
163
|
status: ops_contract_1.WEBHOOK_STATUS.ok,
|
|
171
|
-
body: {
|
|
164
|
+
body: {
|
|
165
|
+
ok: true,
|
|
166
|
+
offered: 0,
|
|
167
|
+
applied: 0,
|
|
168
|
+
duplicates: 0,
|
|
169
|
+
vocabulary: 0,
|
|
170
|
+
rejected: [],
|
|
171
|
+
...seqBody(scope, advanced, numbered, taken.firstUnseenBefore)
|
|
172
|
+
}
|
|
172
173
|
};
|
|
173
174
|
}
|
|
174
175
|
let result;
|
|
@@ -179,14 +180,55 @@ async function handleHook(args) {
|
|
|
179
180
|
return { status: ops_contract_1.WEBHOOK_STATUS.failed, body: { ok: false, error: e?.message ?? 'ingest failed' } };
|
|
180
181
|
}
|
|
181
182
|
const rejected = result.rejected ?? [];
|
|
182
|
-
|
|
183
|
+
/*
|
|
184
|
+
* ── 떨어진 것에 **봉투 id** 를 붙인다 (2026-09-07 측정) ──────────────────────
|
|
185
|
+
* 이 응답이 `{ record, errors }` 목록이었고, plant 은 그것을 **봉투 id 문자열 목록**으로 읽고
|
|
186
|
+
* 있었다. `Array.isArray` 는 통과하므로 그쪽 guard 도 안 걸렸다 — id 를 맞추는데 객체라 하나도
|
|
187
|
+
* 안 맞고 **묶음 전체가 「보냈음」으로 찍혔다.**
|
|
188
|
+
*
|
|
189
|
+
* 422 와 `ok: false` 를 보냈는데 그쪽 행은 재시도 0 · 오류 없음 · 보냈음이었다. 우리가 말했는데
|
|
190
|
+
* 상대가 못 알아듣는 모양이면, 말한 것이 아니다.
|
|
191
|
+
*
|
|
192
|
+
* 그래서 봉투 id 를 함께 싣는다. 보내는 쪽이 자기 행을 표시할 수 있고 까닭도 남는다.
|
|
193
|
+
*
|
|
194
|
+
* **id 를 못 찾으면 레코드를 그대로 싣는다.** 그때는 무엇이 떨어졌는지 알 방법이 그것뿐이고,
|
|
195
|
+
* 빈 id 를 실으면 보내는 쪽이 아무 행도 못 찾으면서 「알았다」고 여긴다.
|
|
196
|
+
*
|
|
197
|
+
* 짝은 **객체 동일성**으로 짓는다. `records` 는 `items` 의 순서와 원소를 그대로 들고 있고
|
|
198
|
+
* (§`takeInSequence`), 그래서 커넥터를 고치지 않고도 봉투로 되짚을 수 있다. 값으로 비교하면
|
|
199
|
+
* 같은 모양의 레코드 둘이 서로의 id 를 가져간다.
|
|
200
|
+
*/
|
|
201
|
+
const forCaller = (0, hook_rejected_js_1.rejectedForCaller)(rejected, items);
|
|
202
|
+
if (result.applied === 0 && result.duplicates === 0 && rejected.length === 0 && !result.vocabulary) {
|
|
183
203
|
/*
|
|
184
204
|
* 넣었는데 한 건도 반영되지 않았고 중복도 거부도 아니면, 그 트윈이 실시간으로 돌지 않는 것이다.
|
|
185
205
|
* 200 을 주면 그쪽은 전달된 줄 알고 그 사실을 버린다.
|
|
186
206
|
*
|
|
187
207
|
* **커서를 올리지 않는다.** 한 건도 안 들어갔는데 번호를 올리면 그 번호는 다시 오지 않고, 트윈이
|
|
188
208
|
* 뜬 뒤에 그 구간이 비어 있게 된다.
|
|
209
|
+
*
|
|
210
|
+
* ── Vocabulary is why this test needs a fourth term (2026-09-07) ──────────
|
|
211
|
+
* This inference reads an absence as a cause, and one absence is legitimate: a batch that was
|
|
212
|
+
* entirely vocabulary applies no events, has no duplicates and rejects nothing, and yet
|
|
213
|
+
* everything in it landed (§`applyMasterData`). Without the last term the receiver answers
|
|
214
|
+
* 503 to correct traffic, and the sender then holds those facts and retries every thirty
|
|
215
|
+
* seconds for as long as they keep arriving — the reply invents a reason ("not running live")
|
|
216
|
+
* that the numbers do not support.
|
|
217
|
+
*
|
|
218
|
+
* Found by a test written for the response sum, not by reading this line.
|
|
219
|
+
*
|
|
220
|
+
* ── Why this branch has to say something out loud ─────────────────────────
|
|
221
|
+
* Measured today: a 503 from here leaves **no trace on either side.** The sender maps it to
|
|
222
|
+
* backpressure and returns without touching `attempts` or `lastError`, and this branch logged
|
|
223
|
+
* nothing, so a sender re-offering the same batch every thirty seconds is invisible to both.
|
|
224
|
+
*
|
|
225
|
+
* That is also why "it has not happened" could not be established: `attempts 0` on the seven
|
|
226
|
+
* vocabulary envelopes was offered as evidence and is not evidence, because this path never
|
|
227
|
+
* increments that counter. Telling a sender to come back forever is worth one line.
|
|
189
228
|
*/
|
|
229
|
+
(0, log_js_1.twinWarn)(`[twin-hook] "${instanceId}" 에 ${records.length}건이 왔는데 한 건도 앉지 않아 ` +
|
|
230
|
+
`${NOT_LIVE_RETRY_MS / 1000}초 뒤 다시 오라고 답했습니다 — 트윈이 실시간으로 돌지 않는 것으로 봅니다. ` +
|
|
231
|
+
`보내는 쪽은 이 답에 재시도 횟수를 남기지 않으므로, 되풀이되면 이 줄만 늘어납니다`);
|
|
190
232
|
return {
|
|
191
233
|
status: ops_contract_1.WEBHOOK_STATUS.notLive,
|
|
192
234
|
body: {
|
|
@@ -198,13 +240,16 @@ async function handleHook(args) {
|
|
|
198
240
|
};
|
|
199
241
|
}
|
|
200
242
|
if (advanced)
|
|
201
|
-
await saveSeq(store, ref, advanced.scope, advanced.seq);
|
|
243
|
+
await saveSeq(store, ref, advanced.scope, advanced.seq, taken.firstSeq);
|
|
202
244
|
/*
|
|
203
245
|
* **구멍이 있으면 그것이 답을 정한다.** 앞의 것은 이미 들어갔으므로 `lastSeq` 까지는 보냄으로 표시하게
|
|
204
246
|
* 하고, 떨어진 것이 있으면 그것도 함께 싣는다 — 두 소식이 한 응답에 들어가야 보내는 쪽이 둘 다 한다.
|
|
205
247
|
*/
|
|
206
|
-
if (gap)
|
|
207
|
-
|
|
248
|
+
if (gap) {
|
|
249
|
+
if (scope)
|
|
250
|
+
onGap?.(scope);
|
|
251
|
+
return gapOutcome(scope, gap, forCaller, advanced);
|
|
252
|
+
}
|
|
208
253
|
if (rejected.length) {
|
|
209
254
|
/*
|
|
210
255
|
* **다시 보내라고 하지 않는다.** 떨어진 것은 모양이 틀린 것이라 같은 것을 다시 보내도 또 떨어지고,
|
|
@@ -218,8 +263,9 @@ async function handleHook(args) {
|
|
|
218
263
|
offered: records.length,
|
|
219
264
|
applied: result.applied,
|
|
220
265
|
duplicates: result.duplicates,
|
|
221
|
-
|
|
222
|
-
|
|
266
|
+
vocabulary: result.vocabulary ?? 0,
|
|
267
|
+
rejected: forCaller,
|
|
268
|
+
...seqBody(scope, advanced, numbered, taken.firstUnseenBefore)
|
|
223
269
|
}
|
|
224
270
|
};
|
|
225
271
|
}
|
|
@@ -230,8 +276,14 @@ async function handleHook(args) {
|
|
|
230
276
|
offered: records.length,
|
|
231
277
|
applied: result.applied,
|
|
232
278
|
duplicates: result.duplicates,
|
|
279
|
+
/*
|
|
280
|
+
* Always reported, even as zero — the sender's sum has a term for it either way, and a field
|
|
281
|
+
* that appears only sometimes makes an absent one indistinguishable from a receiver that
|
|
282
|
+
* does not count vocabulary at all.
|
|
283
|
+
*/
|
|
284
|
+
vocabulary: result.vocabulary ?? 0,
|
|
233
285
|
rejected: [],
|
|
234
|
-
...seqBody(scope, advanced, numbered)
|
|
286
|
+
...seqBody(scope, advanced, numbered, taken.firstUnseenBefore)
|
|
235
287
|
}
|
|
236
288
|
};
|
|
237
289
|
}
|
|
@@ -241,7 +293,19 @@ async function handleHook(args) {
|
|
|
241
293
|
* `lastSeq` 를 빼면 보내는 쪽은 이미 들어간 것까지 다시 보내고, 그것이 매번 중복으로 걸러지면서
|
|
242
294
|
* 처리량만 먹는다.
|
|
243
295
|
*/
|
|
244
|
-
|
|
296
|
+
/*
|
|
297
|
+
* 구멍을 본 자리에서 **바로 받는다** — 번호가 비었다는 것을 아는 가장 이른 순간이다.
|
|
298
|
+
*
|
|
299
|
+
* 기다리지 않는다. 훅의 응답이 이것 때문에 늦으면 보내는 쪽이 되풀이하고, 그러면 같은 봉투가 두 번
|
|
300
|
+
* 온다. 그리고 같은 단위를 1분 안에 두 번 부르지 않는다(§`FILL_DEBOUNCE_MS`) — 밀어 받는 쪽이
|
|
301
|
+
* 구멍 뒤의 봉투를 계속 보내면 그때마다 구멍 판정이 나기 때문이다.
|
|
302
|
+
*/
|
|
303
|
+
function gapOutcome(scope, gap,
|
|
304
|
+
/*
|
|
305
|
+
* 떨어진 것 — 봉투 id 를 알면 그것을, 모르면 레코드를 싣는다(§`InboundItem.eventId`).
|
|
306
|
+
* 보내는 쪽이 자기 행을 찾을 수 있어야 하고, 못 찾으면 무엇이 떨어졌는지라도 보여야 한다.
|
|
307
|
+
*/
|
|
308
|
+
rejected = [], advanced) {
|
|
245
309
|
return {
|
|
246
310
|
status: ops_contract_1.WEBHOOK_STATUS.sequenceGap,
|
|
247
311
|
body: {
|
|
@@ -267,10 +331,26 @@ const NOT_LIVE_RETRY_MS = 30_000;
|
|
|
267
331
|
* 번호를 매기지 않는 원본은 확인할 것이 없다. 그것을 응답에서 「이상 없다」와 같아 보이게 두면, 어느
|
|
268
332
|
* 연결이 연속성을 지키고 있는지 아무도 알 수 없다.
|
|
269
333
|
*/
|
|
270
|
-
|
|
334
|
+
/*
|
|
335
|
+
* The return type is the contract's, narrowed to the fields this function fills.
|
|
336
|
+
*
|
|
337
|
+
* It was `Record<string, unknown>`, and because the result is **spread** into the reply, an index
|
|
338
|
+
* signature slips past the excess-property check entirely — the four fields below were unverified
|
|
339
|
+
* even after `HookOutcome.body` was typed. Naming them here is what makes the check reach them.
|
|
340
|
+
*/
|
|
341
|
+
function seqBody(scope, advanced, numbered, firstUnseenBefore) {
|
|
271
342
|
if (!numbered)
|
|
272
343
|
return { sequenceChecked: false };
|
|
273
|
-
return {
|
|
344
|
+
return {
|
|
345
|
+
sequenceChecked: true,
|
|
346
|
+
scope,
|
|
347
|
+
lastSeq: advanced?.seq,
|
|
348
|
+
/*
|
|
349
|
+
* 첫 봉투였고 그 앞을 못 봤으면 **그 수를 보내는 쪽에 돌려준다.** 안 돌려주면 그 사실이 아무 데도
|
|
350
|
+
* 안 남는다 — 구멍으로도 안 세어지고 backfill 대상도 아니다(backfill 은 커서부터 묻는다).
|
|
351
|
+
*/
|
|
352
|
+
...(firstUnseenBefore ? { firstUnseenBefore } : {})
|
|
353
|
+
};
|
|
274
354
|
}
|
|
275
355
|
/**
|
|
276
356
|
* 커서에 번호를 적는다 — **반영한 뒤에만.**
|
|
@@ -281,8 +361,8 @@ function seqBody(scope, advanced, numbered) {
|
|
|
281
361
|
* 적지 못하는 것은 사실 유입을 무르는 이유가 아니다 — 이미 들어간 사실은 그대로 두고, 다음 번호가 오면
|
|
282
362
|
* `gap` 으로 드러난다. 그래서 실패를 던지지 않고 남긴다.
|
|
283
363
|
*/
|
|
284
|
-
async function saveSeq(store, ref, scope, seq) {
|
|
285
|
-
const next = (0, hook_contract_js_1.withSeq)(ref.liveCursor, scope, seq);
|
|
364
|
+
async function saveSeq(store, ref, scope, seq, firstSeq) {
|
|
365
|
+
const next = (0, hook_contract_js_1.withSeq)(ref.liveCursor, scope, seq, firstSeq);
|
|
286
366
|
ref.liveCursor = next;
|
|
287
367
|
await store
|
|
288
368
|
.saveCursor(ref.id, next)
|