@things-factory/webhook 10.1.29 → 10.1.33
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/service/index.d.ts +16 -0
- package/dist-server/service/index.js +28 -0
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/observability/clock-offset.d.ts +88 -0
- package/dist-server/service/observability/clock-offset.js +96 -0
- package/dist-server/service/observability/clock-offset.js.map +1 -0
- package/dist-server/service/observability/trace-context.d.ts +66 -0
- package/dist-server/service/observability/trace-context.js +109 -0
- package/dist-server/service/observability/trace-context.js.map +1 -0
- package/dist-server/service/outbox/append.d.ts +85 -0
- package/dist-server/service/outbox/append.js +46 -0
- package/dist-server/service/outbox/append.js.map +1 -0
- package/dist-server/service/outbox/delivery.d.ts +224 -0
- package/dist-server/service/outbox/delivery.js +337 -0
- package/dist-server/service/outbox/delivery.js.map +1 -0
- package/dist-server/service/outbox/dispatcher.d.ts +46 -0
- package/dist-server/service/outbox/dispatcher.js +109 -0
- package/dist-server/service/outbox/dispatcher.js.map +1 -0
- package/dist-server/service/outbox/page.d.ts +18 -0
- package/dist-server/service/outbox/page.js +14 -0
- package/dist-server/service/outbox/page.js.map +1 -0
- package/dist-server/service/outbox/pump-gate.d.ts +74 -0
- package/dist-server/service/outbox/pump-gate.js +90 -0
- package/dist-server/service/outbox/pump-gate.js.map +1 -0
- package/dist-server/service/outbox/reconciliation.d.ts +114 -0
- package/dist-server/service/outbox/reconciliation.js +115 -0
- package/dist-server/service/outbox/reconciliation.js.map +1 -0
- package/dist-server/service/outbox/requeue-note.d.ts +13 -0
- package/dist-server/service/outbox/requeue-note.js +21 -0
- package/dist-server/service/outbox/requeue-note.js.map +1 -0
- package/dist-server/service/outbox/row-effect.d.ts +68 -0
- package/dist-server/service/outbox/row-effect.js +54 -0
- package/dist-server/service/outbox/row-effect.js.map +1 -0
- package/dist-server/service/outbox/seq-report.d.ts +88 -0
- package/dist-server/service/outbox/seq-report.js +112 -0
- package/dist-server/service/outbox/seq-report.js.map +1 -0
- package/dist-server/service/outbox/settlement.d.ts +63 -0
- package/dist-server/service/outbox/settlement.js +68 -0
- package/dist-server/service/outbox/settlement.js.map +1 -0
- package/dist-server/service/outbox/standing.d.ts +63 -0
- package/dist-server/service/outbox/standing.js +65 -0
- package/dist-server/service/outbox/standing.js.map +1 -0
- package/dist-server/service/outbox/store.d.ts +52 -0
- package/dist-server/service/outbox/store.js +3 -0
- package/dist-server/service/outbox/store.js.map +1 -0
- package/dist-server/service/outbox/webhook-store.d.ts +17 -0
- package/dist-server/service/outbox/webhook-store.js +62 -0
- package/dist-server/service/outbox/webhook-store.js.map +1 -0
- package/dist-server/service/outbox/writing-transaction.d.ts +33 -0
- package/dist-server/service/outbox/writing-transaction.js +40 -0
- package/dist-server/service/outbox/writing-transaction.js.map +1 -0
- package/dist-server/service/webhook-destination/webhook-destination.d.ts +3 -0
- package/dist-server/service/webhook-destination/webhook-destination.js +15 -0
- package/dist-server/service/webhook-destination/webhook-destination.js.map +1 -1
- package/dist-server/service/webhook-outbox-event/webhook-outbox-event.d.ts +3 -0
- package/dist-server/service/webhook-outbox-event/webhook-outbox-event.js +22 -0
- package/dist-server/service/webhook-outbox-event/webhook-outbox-event.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -6
- package/tests/outbox-append.test.ts +106 -0
- package/tests/outbox-pump-gate.test.ts +65 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reconciliation.js","sourceRoot":"","sources":["../../../server/service/outbox/reconciliation.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * **보낸 수와 앉은 수를 맞춰 본다** — 순수 판단.\n *\n * ═══════════════════════════════════════════════════════════════════════════\n * ── 오늘 이것이 없어서 무슨 일이 났나 ──────────────────────────────────────\n * 2026-09-05 하루에 세 번, 사실이 안 갔는데 **양쪽 다 초록**이었다.\n *\n * ```\n * 우리 쪽 SENT 84건 보냈다\n * 받는 쪽 저널 17건 앉았다\n * 그 차이 아무 데도 없다\n * ```\n *\n * 세 번 다 사람이 손으로 세서 찾았다. 담김이 보류로 남은 것, 재고 둘이 수량 0 으로 거절된 것, 어휘가\n * 안 나간 것 — **다음엔 아무도 안 센다.**\n *\n * ── `SENT` 는 「보냈다」이지 「앉았다」가 아니다 ────────────────────────────\n * 받는 쪽이 `accepted` 로 답하는 것은 **접수했다**는 뜻이다. 그 뒤에 되풀이로 합쳐지거나, 리듀서가\n * 못 읽거나, 표준 밖 값이라 버려질 수 있다. 그 셋 다 우리에게는 `SENT` 로 보인다.\n *\n * ── **넷으로 가른다. 둘로는 안 닫힌다** ────────────────────────────────────\n * 처음에는 「앉았다 · 거절됐다」 둘로 뺐다. 그러면 이렇게 된다.\n *\n * ```\n * 둘로 157 − 46 − 2 = 109 「사라졌다」 ← 진실은 되풀이 108 + 어휘 1\n * 넷으로 157 − 46 − 2 − 108 − 1 = 0\n * ```\n *\n * **109를 「사라졌다」로 내면 그날부터 아무도 그 수를 안 본다.** 늘 큰 수가 떠 있으면 진짜로 하나\n * 사라진 날을 못 본다 — 경보를 울려 놓고 아무도 안 듣게 만드는 것이 안 울리는 것보다 나쁘다.\n *\n * 인티그레이션이 실제 수로 짚었다(2026-09-05). 108은 대부분 **되풀이**였고, 그것은 우리가 같은\n * 실적을 다시 보낸 것을 받는 쪽이 옳게 합친 결과다 — 사라진 것이 아니다.\n *\n * ```\n * landed 저널에 앉았다\n * rejected 받는 쪽이 안 받는다고 말했다 우리가 안다\n * duplicates 이미 있는 사실이라 합쳤다 우리가 다시 보낸 것이다\n * vocabulary 저널에 안 가는 어휘다 설계대로다\n * ```\n *\n * ── 「모른다」를 「맞다」로 읽지 않는다 ─────────────────────────────────────\n * **이 모듈의 요점이다.** 받는 쪽 수를 못 물어보면 답은 「맞다」가 아니라 **「모른다」**다. 0 으로\n * 채우거나 우리 수를 그대로 두 번 쓰면, 맞춰 보는 자리를 만들어 놓고 **늘 초록인 자리**를 만든 것이다.\n *\n * 오늘 하루 종일 잡은 그 부류를, 그것을 잡으려고 만든 자리에서 다시 만들지 않는다.\n * ═══════════════════════════════════════════════════════════════════════════\n */\n\nexport type ReconcileVerdict =\n /** 두 수가 같다. */\n | 'matched'\n /** 보낸 것보다 앉은 것이 적다 — **사실이 사라졌다.** */\n | 'short'\n /**\n * 앉은 것이 더 많다 — **원인이 둘이고, 지금 이 셈은 둘을 못 가른다.**\n *\n * ```\n * ① 같은 사실이 두 번 앉았다 받는 쪽 dedupe 가 놓친 재전송\n * ② 받는 쪽이 스스로 넣은 행이다 우리 봉투가 없는 행. period fact backfill 이 그렇게 넣는다\n * ```\n *\n * ②는 셈이 정상인데 음수가 나는 경우다 — 그 구간을 맞춰 보면 그만큼 많이 나오는 것이 맞다.\n * 그래서 이 판정을 「정상일 수도 있다」로 읽으면 ①도 ②도 안 찾게 된다.\n *\n * 가르려면 저널 행에 「어떻게 들어왔나」가 있어야 하는데 없다(인티그레이션 실측 2026-09-10 —\n * ingest 함수 열이 record 하나에 event 하나만 내고, 저널을 쓰는 문 둘이 다 `.map()` 이라\n * **봉투 하나가 사실 여럿을 내는 경로는 없다**).\n */\n | 'excess'\n /** 받는 쪽 수를 모른다. **맞다가 아니다.** */\n | 'unknown'\n\nexport interface ReconcileInput {\n /** 우리가 보냈다고 표시한 수. */\n sent: number\n /**\n * 받는 쪽에 앉은 수. **못 물어봤으면 `undefined`** — 0 이 아니다.\n *\n * 0 은 「하나도 안 앉았다」이고 그것은 심각한 사실이다. 모르는 것과 섞으면 진짜 0 이 묻힌다.\n */\n landed?: number\n /** 받는 쪽이 거절한다고 답한 수 — 이건 우리가 안다. 사라진 것이 아니라 **거절된 것**이다. */\n rejected?: number\n /**\n * 이미 있는 사실이라 합쳐진 수. **우리가 다시 보낸 것**이지 사라진 것이 아니다.\n *\n * 이 값을 안 빼면 되풀이가 통째로 「사라졌다」로 세어진다 — 오늘 108건이 그랬다.\n */\n duplicates?: number\n /**\n * 저널에 안 가는 어휘(`MasterDataRecord`)의 수. **설계대로 안 가는 것**이다.\n *\n * 이건 우리가 안다 — 우리가 낸 봉투이므로 세어 둘 수 있다. 받는 쪽에 묻지 않아도 된다.\n */\n vocabulary?: number\n}\n\nexport interface Reconciliation {\n verdict: ReconcileVerdict\n /** 설명되지 않는 차이. 모르면 `undefined`. */\n unexplained?: number\n sent: number\n landed?: number\n rejected: number\n duplicates: number\n vocabulary: number\n}\n\n/**\n * 두 수를 맞춘다.\n *\n * **거절된 것은 사라진 것이 아니다.** 받는 쪽이 「안 받는다」고 말했고 우리는 그것을 안다. 그것까지\n * 「사라졌다」로 세면 진짜 조용히 없어진 것이 그 수에 묻힌다.\n */\nexport function reconcile(input: ReconcileInput): Reconciliation {\n const rejected = Math.max(0, input.rejected ?? 0)\n const duplicates = Math.max(0, input.duplicates ?? 0)\n const vocabulary = Math.max(0, input.vocabulary ?? 0)\n const sent = Math.max(0, input.sent)\n\n if (!Number.isFinite(input.landed as number)) {\n /* 못 물어봤다. **맞다고 하지 않는다.** */\n return { verdict: 'unknown', sent, rejected, duplicates, vocabulary }\n }\n\n const landed = Math.max(0, input.landed as number)\n\n /*\n * 앉아야 할 수 = 보낸 것 − (거절 + 되풀이 + 어휘).\n *\n * 셋 다 **우리가 아는 이유**다. 빼지 않으면 아는 것까지 「사라졌다」로 세어지고, 그러면 그 수를\n * 아무도 안 보게 된다(머리말).\n */\n const expected = Math.max(0, sent - rejected - duplicates - vocabulary)\n const unexplained = expected - landed\n\n if (unexplained === 0) {\n return { verdict: 'matched', unexplained: 0, sent, landed, rejected, duplicates, vocabulary }\n }\n\n return {\n verdict: unexplained > 0 ? 'short' : 'excess',\n unexplained,\n sent,\n landed,\n rejected,\n duplicates,\n vocabulary\n }\n}\n\n/** 사람이 읽는 한 줄 — 표에 그대로 걸 수 있게. */\nexport function describeReconciliation(found: Reconciliation, where: string): string {\n switch (found.verdict) {\n case 'unknown':\n return (\n `${where}: 보낸 ${found.sent}건. 받는 쪽에 몇 건이 앉았는지 **묻지 못했습니다** — ` +\n '맞는지 안 맞는지 알 수 없습니다'\n )\n case 'matched':\n return `${where}: 보낸 ${found.sent}건이 모두 설명됩니다 — 앉음 ${found.landed}${aside(found)}`\n case 'short':\n return (\n `${where}: **${found.unexplained}건이 사라졌습니다** — 보낸 ${found.sent}건 중 앉은 것이 ` +\n `${found.landed}건입니다${aside(found)}. 거절도 되풀이도 어휘도 아닌데 안 앉았습니다`\n )\n case 'excess':\n return (\n `${where}: 앉은 것이 ${-found.unexplained!}건 많습니다 — 보낸 ${found.sent}건, 앉은 ${found.landed}건. ` +\n '같은 사실이 두 번 앉았거나, 받는 쪽이 우리 봉투 없이 스스로 넣은 행입니다'\n )\n }\n}\n\n/** 아는 이유들 — 사라진 수와 섞이지 않게 괄호로 따로 적는다. */\nfunction aside(found: Reconciliation): string {\n const known = [\n found.rejected ? `거절 ${found.rejected}` : '',\n found.duplicates ? `되풀이 ${found.duplicates}` : '',\n found.vocabulary ? `어휘 ${found.vocabulary}` : ''\n ].filter(Boolean)\n\n return known.length ? ` (${known.join(' · ')} 은 뺀 수입니다)` : ''\n}\n"]}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The note a re-driven record carries — **both stories in one column.**
|
|
3
|
+
*
|
|
4
|
+
* Kept in its own file with no imports so a test can read it. Pulling the entity in to reach this
|
|
5
|
+
* one function makes the test runner load a TypeORM enum, and the runner strips types rather than
|
|
6
|
+
* compiling them, so the whole file fails to load (§`feedback_test_import_light_only`).
|
|
7
|
+
*
|
|
8
|
+
* Why both stories: the row already holds what the receiver said, and that is the only record of
|
|
9
|
+
* why each envelope was refused. Overwriting it with the operator's reason would trade one fact
|
|
10
|
+
* for another. Adding a column for it would rewrite the table on SQLite, and this product does not
|
|
11
|
+
* migrate during development.
|
|
12
|
+
*/
|
|
13
|
+
export declare function requeueNote(reason: string, previous?: string | null): string;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.requeueNote = requeueNote;
|
|
4
|
+
/**
|
|
5
|
+
* The note a re-driven record carries — **both stories in one column.**
|
|
6
|
+
*
|
|
7
|
+
* Kept in its own file with no imports so a test can read it. Pulling the entity in to reach this
|
|
8
|
+
* one function makes the test runner load a TypeORM enum, and the runner strips types rather than
|
|
9
|
+
* compiling them, so the whole file fails to load (§`feedback_test_import_light_only`).
|
|
10
|
+
*
|
|
11
|
+
* Why both stories: the row already holds what the receiver said, and that is the only record of
|
|
12
|
+
* why each envelope was refused. Overwriting it with the operator's reason would trade one fact
|
|
13
|
+
* for another. Adding a column for it would rewrite the table on SQLite, and this product does not
|
|
14
|
+
* migrate during development.
|
|
15
|
+
*/
|
|
16
|
+
function requeueNote(reason, previous) {
|
|
17
|
+
const said = previous?.trim();
|
|
18
|
+
const note = `requeued: ${reason.trim()}`;
|
|
19
|
+
return said ? `${note} (was: ${said})`.slice(0, 1000) : note.slice(0, 1000);
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=requeue-note.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"requeue-note.js","sourceRoot":"","sources":["../../../server/service/outbox/requeue-note.ts"],"names":[],"mappings":";;AAYA,kCAKC;AAjBD;;;;;;;;;;;GAWG;AACH,SAAgB,WAAW,CAAC,MAAc,EAAE,QAAwB;IAClE,MAAM,IAAI,GAAG,QAAQ,EAAE,IAAI,EAAE,CAAA;IAC7B,MAAM,IAAI,GAAG,aAAa,MAAM,CAAC,IAAI,EAAE,EAAE,CAAA;IAEzC,OAAO,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,UAAU,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,CAAA;AAC7E,CAAC","sourcesContent":["/**\n * The note a re-driven record carries — **both stories in one column.**\n *\n * Kept in its own file with no imports so a test can read it. Pulling the entity in to reach this\n * one function makes the test runner load a TypeORM enum, and the runner strips types rather than\n * compiling them, so the whole file fails to load (§`feedback_test_import_light_only`).\n *\n * Why both stories: the row already holds what the receiver said, and that is the only record of\n * why each envelope was refused. Overwriting it with the operator's reason would trade one fact\n * for another. Adding a column for it would rewrite the table on SQLite, and this product does not\n * migrate during development.\n */\nexport function requeueNote(reason: string, previous?: string | null): string {\n const said = previous?.trim()\n const note = `requeued: ${reason.trim()}`\n\n return said ? `${note} (was: ${said})`.slice(0, 1000) : note.slice(0, 1000)\n}\n"]}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { DeliveryOutcome } from './delivery.js';
|
|
2
|
+
/**
|
|
3
|
+
* 받는 쪽의 답이 **우리 행에 무슨 일을 하나** — 저장소를 모르는 판단.
|
|
4
|
+
*
|
|
5
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
6
|
+
* ## 왜 떼어 냈나 (2026-09-18)
|
|
7
|
+
*
|
|
8
|
+
* 이 판단이 `dispatchPending` 의 `switch` 안에 살았다. 그래서 **어느 시험도 못 지났다** — 그 갈래에
|
|
9
|
+
* 닿으려면 EntityManager 와 503 을 주는 받는 쪽이 있어야 하고, 이 꾸러미의 시험은 가볍게 import 하는
|
|
10
|
+
* 것을 규율로 삼는다. 배압 갈래의 주석이 그것을 스스로 적어 두었다(「⚠ 이 줄을 도는 시험이 없다 …
|
|
11
|
+
* outcome→동작 판단을 떼어 내는 사람이 그때 못 박아라」). 이 파일이 그 자리다.
|
|
12
|
+
*
|
|
13
|
+
* ## 배압만 시도로 세지 않는다
|
|
14
|
+
*
|
|
15
|
+
* ```
|
|
16
|
+
* accepted · partial · gap 범위를 갈라 표시한다 — 어디까지 닿았나는 부르는 쪽이 안다
|
|
17
|
+
* stop 다시 보내도 같다 → 실패로 두고 흐름에서 뺀다
|
|
18
|
+
* backpressure 「나중에 오라」 → 아무것도 안 바꾼다. 시도로 세지 않는다
|
|
19
|
+
* unauthorized · retry · 그 밖 시도 횟수와 이유만 남기고 상태는 둔다 — 다음에 다시 보낸다
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* 배압을 시도로 세면 **기다려 달라는 받는 쪽에게 재시도 예산을 쓴다.** 그래서 안 센다. 대가는
|
|
23
|
+
* `attempts: 0` 이 증거가 못 되는 것이다 — 백 번 붙들려도 행은 `attempts 0` 이고 `lastError` 가
|
|
24
|
+
* 없다. 그 사실을 아는 유일한 자리가 로그 한 줄이라, **세지 않고 되풀이된다**: 한 줄은 정상이고
|
|
25
|
+
* 벽처럼 쌓이면 영영 못 가는 보내는 쪽이다.
|
|
26
|
+
*
|
|
27
|
+
* ## `stop` 은 상태까지 바꾼다
|
|
28
|
+
*
|
|
29
|
+
* 시도 횟수만 남기고 상태를 안 바꿔 두 가지가 났다(2026-09-06). 매 주기 다시 보내 seq 1~20 이
|
|
30
|
+
* 시도 382회가 됐고, 나중 배치에 딸려 들어가 `SENT` 로 찍혔다 — **받는 쪽에는 없는데 우리 표는 다
|
|
31
|
+
* 보냈다고 말한다.** 그래서 이 갈래는 실패로 두고 흐름에서 뺀다. 봉투는 그대로 남으므로 backfill 은
|
|
32
|
+
* `since: 0` 으로 다시 물어 가면 된다.
|
|
33
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
34
|
+
*/
|
|
35
|
+
export type RowEffect =
|
|
36
|
+
/** 어디까지 닿았는지로 갈라 표시한다. 범위는 `outcome` 이 들고 있고 부르는 쪽이 읽는다. */
|
|
37
|
+
{
|
|
38
|
+
rows: 'settle';
|
|
39
|
+
countsAsAttempt: false;
|
|
40
|
+
}
|
|
41
|
+
/** 상태를 실패로 바꾸고 시도도 남긴다 — 다시 보내지 않는다. */
|
|
42
|
+
| {
|
|
43
|
+
rows: 'fail';
|
|
44
|
+
reason: string;
|
|
45
|
+
countsAsAttempt: true;
|
|
46
|
+
say: SayIt;
|
|
47
|
+
}
|
|
48
|
+
/** 시도 횟수와 이유만 남긴다. 상태는 그대로 — 다음 주기에 다시 보낸다. */
|
|
49
|
+
| {
|
|
50
|
+
rows: 'attempt';
|
|
51
|
+
reason: string;
|
|
52
|
+
countsAsAttempt: true;
|
|
53
|
+
say?: SayIt;
|
|
54
|
+
}
|
|
55
|
+
/** 아무것도 안 바꾼다. **시도로 세지 않는다.** */
|
|
56
|
+
| {
|
|
57
|
+
rows: 'hold';
|
|
58
|
+
retryAfterMs: number;
|
|
59
|
+
countsAsAttempt: false;
|
|
60
|
+
say: SayIt;
|
|
61
|
+
};
|
|
62
|
+
/** 남길 말 — 수준까지 판단에 든다. 부르는 쪽은 고르지 않는다. */
|
|
63
|
+
export interface SayIt {
|
|
64
|
+
level: 'info' | 'warn' | 'error';
|
|
65
|
+
what: string;
|
|
66
|
+
}
|
|
67
|
+
/** 이 답이 우리 행에 하는 일. `count` 는 이 배치의 봉투 수이고 말에만 쓰인다. */
|
|
68
|
+
export declare function rowEffectOf(outcome: DeliveryOutcome, count: number): RowEffect;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.rowEffectOf = rowEffectOf;
|
|
4
|
+
/** 이 답이 우리 행에 하는 일. `count` 는 이 배치의 봉투 수이고 말에만 쓰인다. */
|
|
5
|
+
function rowEffectOf(outcome, count) {
|
|
6
|
+
switch (outcome.kind) {
|
|
7
|
+
case 'accepted':
|
|
8
|
+
case 'partial':
|
|
9
|
+
case 'gap':
|
|
10
|
+
return { rows: 'settle', countsAsAttempt: false };
|
|
11
|
+
case 'stop':
|
|
12
|
+
return {
|
|
13
|
+
rows: 'fail',
|
|
14
|
+
reason: outcome.reason,
|
|
15
|
+
countsAsAttempt: true,
|
|
16
|
+
say: {
|
|
17
|
+
level: 'error',
|
|
18
|
+
what: `[webhook] ${outcome.reason} — 이 ${count}건을 실패로 두고 다시 보내지 않습니다`
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
case 'backpressure':
|
|
22
|
+
return {
|
|
23
|
+
rows: 'hold',
|
|
24
|
+
retryAfterMs: outcome.retryAfterMs,
|
|
25
|
+
countsAsAttempt: false,
|
|
26
|
+
say: {
|
|
27
|
+
level: 'warn',
|
|
28
|
+
what: `[webhook] the receiver asked us to hold ${count} record(s) for ` +
|
|
29
|
+
`${outcome.retryAfterMs}ms — held, not failed, and not counted as an attempt`
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
case 'unauthorized':
|
|
33
|
+
return {
|
|
34
|
+
rows: 'attempt',
|
|
35
|
+
reason: 'signature was not accepted',
|
|
36
|
+
countsAsAttempt: true,
|
|
37
|
+
say: {
|
|
38
|
+
level: 'error',
|
|
39
|
+
what: '[webhook] the receiver did not accept our signature — check the shared secret before retrying'
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
/*
|
|
43
|
+
* `retry` 와 앞으로 늘어날 갈래. **이유를 지어내지 않는다** — 이유 칸이 없는 답이 오면 그 사실을
|
|
44
|
+
* 그대로 적는다. 「알 수 없음」을 로그가 말하는 것과, 없는 이유를 우리가 채워 넣는 것은 다르다.
|
|
45
|
+
*/
|
|
46
|
+
default:
|
|
47
|
+
return {
|
|
48
|
+
rows: 'attempt',
|
|
49
|
+
reason: outcome.reason ?? `the receiver answered ${outcome.kind} without a reason`,
|
|
50
|
+
countsAsAttempt: true
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=row-effect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"row-effect.js","sourceRoot":"","sources":["../../../server/service/outbox/row-effect.ts"],"names":[],"mappings":";;AAqDA,kCAqDC;AAtDD,uDAAuD;AACvD,SAAgB,WAAW,CAAC,OAAwB,EAAE,KAAa;IACjE,QAAQ,OAAO,CAAC,IAAI,EAAE,CAAC;QACrB,KAAK,UAAU,CAAC;QAChB,KAAK,SAAS,CAAC;QACf,KAAK,KAAK;YACR,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,eAAe,EAAE,KAAK,EAAE,CAAA;QAEnD,KAAK,MAAM;YACT,OAAO;gBACL,IAAI,EAAE,MAAM;gBACZ,MAAM,EAAE,OAAO,CAAC,MAAM;gBACtB,eAAe,EAAE,IAAI;gBACrB,GAAG,EAAE;oBACH,KAAK,EAAE,OAAO;oBACd,IAAI,EAAE,aAAa,OAAO,CAAC,MAAM,QAAQ,KAAK,uBAAuB;iBACtE;aACF,CAAA;QAEH,KAAK,cAAc;YACjB,OAAO;gBACL,IAAI,EAAE,MAAM;gBACZ,YAAY,EAAE,OAAO,CAAC,YAAY;gBAClC,eAAe,EAAE,KAAK;gBACtB,GAAG,EAAE;oBACH,KAAK,EAAE,MAAM;oBACb,IAAI,EACF,2CAA2C,KAAK,iBAAiB;wBACjE,GAAG,OAAO,CAAC,YAAY,sDAAsD;iBAChF;aACF,CAAA;QAEH,KAAK,cAAc;YACjB,OAAO;gBACL,IAAI,EAAE,SAAS;gBACf,MAAM,EAAE,4BAA4B;gBACpC,eAAe,EAAE,IAAI;gBACrB,GAAG,EAAE;oBACH,KAAK,EAAE,OAAO;oBACd,IAAI,EAAE,+FAA+F;iBACtG;aACF,CAAA;QAEH;;;WAGG;QACH;YACE,OAAO;gBACL,IAAI,EAAE,SAAS;gBACf,MAAM,EAAG,OAA+B,CAAC,MAAM,IAAI,yBAAyB,OAAO,CAAC,IAAI,mBAAmB;gBAC3G,eAAe,EAAE,IAAI;aACtB,CAAA;IACL,CAAC;AACH,CAAC","sourcesContent":["import type { DeliveryOutcome } from './delivery.js'\n\n/**\n * 받는 쪽의 답이 **우리 행에 무슨 일을 하나** — 저장소를 모르는 판단.\n *\n * ═══════════════════════════════════════════════════════════════════════════\n * ## 왜 떼어 냈나 (2026-09-18)\n *\n * 이 판단이 `dispatchPending` 의 `switch` 안에 살았다. 그래서 **어느 시험도 못 지났다** — 그 갈래에\n * 닿으려면 EntityManager 와 503 을 주는 받는 쪽이 있어야 하고, 이 꾸러미의 시험은 가볍게 import 하는\n * 것을 규율로 삼는다. 배압 갈래의 주석이 그것을 스스로 적어 두었다(「⚠ 이 줄을 도는 시험이 없다 …\n * outcome→동작 판단을 떼어 내는 사람이 그때 못 박아라」). 이 파일이 그 자리다.\n *\n * ## 배압만 시도로 세지 않는다\n *\n * ```\n * accepted · partial · gap 범위를 갈라 표시한다 — 어디까지 닿았나는 부르는 쪽이 안다\n * stop 다시 보내도 같다 → 실패로 두고 흐름에서 뺀다\n * backpressure 「나중에 오라」 → 아무것도 안 바꾼다. 시도로 세지 않는다\n * unauthorized · retry · 그 밖 시도 횟수와 이유만 남기고 상태는 둔다 — 다음에 다시 보낸다\n * ```\n *\n * 배압을 시도로 세면 **기다려 달라는 받는 쪽에게 재시도 예산을 쓴다.** 그래서 안 센다. 대가는\n * `attempts: 0` 이 증거가 못 되는 것이다 — 백 번 붙들려도 행은 `attempts 0` 이고 `lastError` 가\n * 없다. 그 사실을 아는 유일한 자리가 로그 한 줄이라, **세지 않고 되풀이된다**: 한 줄은 정상이고\n * 벽처럼 쌓이면 영영 못 가는 보내는 쪽이다.\n *\n * ## `stop` 은 상태까지 바꾼다\n *\n * 시도 횟수만 남기고 상태를 안 바꿔 두 가지가 났다(2026-09-06). 매 주기 다시 보내 seq 1~20 이\n * 시도 382회가 됐고, 나중 배치에 딸려 들어가 `SENT` 로 찍혔다 — **받는 쪽에는 없는데 우리 표는 다\n * 보냈다고 말한다.** 그래서 이 갈래는 실패로 두고 흐름에서 뺀다. 봉투는 그대로 남으므로 backfill 은\n * `since: 0` 으로 다시 물어 가면 된다.\n * ═══════════════════════════════════════════════════════════════════════════\n */\n\nexport type RowEffect =\n /** 어디까지 닿았는지로 갈라 표시한다. 범위는 `outcome` 이 들고 있고 부르는 쪽이 읽는다. */\n | { rows: 'settle'; countsAsAttempt: false }\n /** 상태를 실패로 바꾸고 시도도 남긴다 — 다시 보내지 않는다. */\n | { rows: 'fail'; reason: string; countsAsAttempt: true; say: SayIt }\n /** 시도 횟수와 이유만 남긴다. 상태는 그대로 — 다음 주기에 다시 보낸다. */\n | { rows: 'attempt'; reason: string; countsAsAttempt: true; say?: SayIt }\n /** 아무것도 안 바꾼다. **시도로 세지 않는다.** */\n | { rows: 'hold'; retryAfterMs: number; countsAsAttempt: false; say: SayIt }\n\n/** 남길 말 — 수준까지 판단에 든다. 부르는 쪽은 고르지 않는다. */\nexport interface SayIt {\n level: 'info' | 'warn' | 'error'\n what: string\n}\n\n/** 이 답이 우리 행에 하는 일. `count` 는 이 배치의 봉투 수이고 말에만 쓰인다. */\nexport function rowEffectOf(outcome: DeliveryOutcome, count: number): RowEffect {\n switch (outcome.kind) {\n case 'accepted':\n case 'partial':\n case 'gap':\n return { rows: 'settle', countsAsAttempt: false }\n\n case 'stop':\n return {\n rows: 'fail',\n reason: outcome.reason,\n countsAsAttempt: true,\n say: {\n level: 'error',\n what: `[webhook] ${outcome.reason} — 이 ${count}건을 실패로 두고 다시 보내지 않습니다`\n }\n }\n\n case 'backpressure':\n return {\n rows: 'hold',\n retryAfterMs: outcome.retryAfterMs,\n countsAsAttempt: false,\n say: {\n level: 'warn',\n what:\n `[webhook] the receiver asked us to hold ${count} record(s) for ` +\n `${outcome.retryAfterMs}ms — held, not failed, and not counted as an attempt`\n }\n }\n\n case 'unauthorized':\n return {\n rows: 'attempt',\n reason: 'signature was not accepted',\n countsAsAttempt: true,\n say: {\n level: 'error',\n what: '[webhook] the receiver did not accept our signature — check the shared secret before retrying'\n }\n }\n\n /*\n * `retry` 와 앞으로 늘어날 갈래. **이유를 지어내지 않는다** — 이유 칸이 없는 답이 오면 그 사실을\n * 그대로 적는다. 「알 수 없음」을 로그가 말하는 것과, 없는 이유를 우리가 채워 넣는 것은 다르다.\n */\n default:\n return {\n rows: 'attempt',\n reason: (outcome as { reason?: string }).reason ?? `the receiver answered ${outcome.kind} without a reason`,\n countsAsAttempt: true\n }\n }\n}\n"]}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **번호 하나하나가 어떤 상태인가** — 있나 · 잃었나 · 아직인가.
|
|
3
|
+
*
|
|
4
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
5
|
+
* ── 왜 이 문이 필요한가 ────────────────────────────────────────────────────
|
|
6
|
+
* 받는 쪽이 구멍을 보면 그 자리에서 멈춘다 — 「418 다음이 420」. 옳은 거동이다. 그런데 **그 419 가
|
|
7
|
+
* 무엇이었는지 물을 자리가 없었다.**
|
|
8
|
+
*
|
|
9
|
+
* 2026-09-06에 실제로 그랬다. 트윈이 419 에서 막혔고, 그것이 무엇이었는지 알아내려고 사람이
|
|
10
|
+
* 데이터베이스를 손으로 뒤졌다. 뒤져 보니 부하 시험이 번호만 가져가고 죽은 자리였다.
|
|
11
|
+
* 이 문이 있었으면 **한 번의 조회**였다.
|
|
12
|
+
*
|
|
13
|
+
* ── 셋을 가르는 근거는 채번이다 ────────────────────────────────────────────
|
|
14
|
+
* 아웃박스는 번호를 따로 들고 있다(`MesOutboxSequence.lastSeq`). 그래서 「행이 없다」를 둘로 가를
|
|
15
|
+
* 수 있다.
|
|
16
|
+
*
|
|
17
|
+
* ```
|
|
18
|
+
* 행이 있다 present 그 봉투를 그대로 낼 수 있다
|
|
19
|
+
* 행이 없고 n <= lastSeq lost 번호는 나갔는데 행이 없다 — 쓰기를 잃었다
|
|
20
|
+
* 행이 없고 n > lastSeq unissued 아직 그 번호까지 안 갔다
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* **`unknown` 은 우리가 쓸 일이 없다.** 채번을 따로 안 드는 원본(그때는 「행이 없다」에서 둘을 못
|
|
24
|
+
* 가른다)을 위해 계약에 있는 값이고, 여기서는 `lastSeq` 가 늘 있다. 없으면 그때는 `unknown` 이다 —
|
|
25
|
+
* 모르는 것을 `lost` 로 단정하면 **사람이 없는 사실을 찾으러 간다.**
|
|
26
|
+
*
|
|
27
|
+
* ── 순수 함수다 ────────────────────────────────────────────────────────────
|
|
28
|
+
* 저장소를 모른다. 부르는 쪽이 있는 번호와 채번을 읽어 넘긴다.
|
|
29
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
30
|
+
*/
|
|
31
|
+
export type SeqState = 'present' | 'lost' | 'unissued' | 'unknown';
|
|
32
|
+
export interface SeqEntry {
|
|
33
|
+
seq: number;
|
|
34
|
+
state: SeqState;
|
|
35
|
+
/** 언제 났나 — `present` 일 때만. */
|
|
36
|
+
at?: string;
|
|
37
|
+
/** 봉투 자체 — `present` 이고 낼 수 있을 때만. */
|
|
38
|
+
record?: unknown;
|
|
39
|
+
}
|
|
40
|
+
export interface SeqReport {
|
|
41
|
+
scope: string;
|
|
42
|
+
entries: SeqEntry[];
|
|
43
|
+
/** 상한에 닿아 다 못 냈다. 조용히 자르면 읽는 쪽이 그 뒤를 「없다」로 읽는다. */
|
|
44
|
+
truncated?: boolean;
|
|
45
|
+
}
|
|
46
|
+
/** 있는 번호 하나 — 부르는 쪽이 행에서 옮겨 온다. */
|
|
47
|
+
export interface KnownSeq {
|
|
48
|
+
seq: number;
|
|
49
|
+
at?: string;
|
|
50
|
+
record?: unknown;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* 한 번에 낼 수 있는 번호 수.
|
|
54
|
+
*
|
|
55
|
+
* 이 문은 **구멍 하나를 들여다보는 자리**다. 수천 개를 한 번에 묻는 것은 이 문의 쓰임이 아니고,
|
|
56
|
+
* 그렇게 쓰면 봉투 원문이 그만큼 나간다.
|
|
57
|
+
*/
|
|
58
|
+
export declare const SEQ_REPORT_MAX = 200;
|
|
59
|
+
export declare function seqReport(input: {
|
|
60
|
+
scope: string;
|
|
61
|
+
from: number;
|
|
62
|
+
to: number;
|
|
63
|
+
/** 이 구간에서 실제로 있는 번호들. 순서는 상관없다. */
|
|
64
|
+
known: readonly KnownSeq[];
|
|
65
|
+
/** 채번이 어디까지 갔나. **모르면 `undefined`** — 그때 없는 번호는 전부 `unknown` 이다. */
|
|
66
|
+
lastSeq?: number;
|
|
67
|
+
}): SeqReport;
|
|
68
|
+
/**
|
|
69
|
+
* How many numbers are missing, counted per run.
|
|
70
|
+
*
|
|
71
|
+
* ── Why the caller passes rows and not a query ───────────────────────────
|
|
72
|
+
* This is the property the receiving side actually needs — the numbers run unbroken — and it is
|
|
73
|
+
* worth testing without a database. Leaving the arithmetic inside the probe meant the only way to
|
|
74
|
+
* prove it catches a gap was to delete a row and put it back.
|
|
75
|
+
*
|
|
76
|
+
* ── Counted per run, summed ──────────────────────────────────────────────
|
|
77
|
+
* Sequence numbers run per domain and channel. A server carrying two plants has two independent
|
|
78
|
+
* runs, and merging them reports a gap where there is none.
|
|
79
|
+
*
|
|
80
|
+
* Duplicates do not create gaps and are not counted here — that is a different fault with a
|
|
81
|
+
* different fix.
|
|
82
|
+
*/
|
|
83
|
+
export declare function countGaps(runs: Iterable<readonly number[]>): number;
|
|
84
|
+
/** Group rows into one run per owner. */
|
|
85
|
+
export declare function runsOf(rows: readonly {
|
|
86
|
+
owner: string;
|
|
87
|
+
seq: number;
|
|
88
|
+
}[]): Map<string, number[]>;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* **번호 하나하나가 어떤 상태인가** — 있나 · 잃었나 · 아직인가.
|
|
4
|
+
*
|
|
5
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
6
|
+
* ── 왜 이 문이 필요한가 ────────────────────────────────────────────────────
|
|
7
|
+
* 받는 쪽이 구멍을 보면 그 자리에서 멈춘다 — 「418 다음이 420」. 옳은 거동이다. 그런데 **그 419 가
|
|
8
|
+
* 무엇이었는지 물을 자리가 없었다.**
|
|
9
|
+
*
|
|
10
|
+
* 2026-09-06에 실제로 그랬다. 트윈이 419 에서 막혔고, 그것이 무엇이었는지 알아내려고 사람이
|
|
11
|
+
* 데이터베이스를 손으로 뒤졌다. 뒤져 보니 부하 시험이 번호만 가져가고 죽은 자리였다.
|
|
12
|
+
* 이 문이 있었으면 **한 번의 조회**였다.
|
|
13
|
+
*
|
|
14
|
+
* ── 셋을 가르는 근거는 채번이다 ────────────────────────────────────────────
|
|
15
|
+
* 아웃박스는 번호를 따로 들고 있다(`MesOutboxSequence.lastSeq`). 그래서 「행이 없다」를 둘로 가를
|
|
16
|
+
* 수 있다.
|
|
17
|
+
*
|
|
18
|
+
* ```
|
|
19
|
+
* 행이 있다 present 그 봉투를 그대로 낼 수 있다
|
|
20
|
+
* 행이 없고 n <= lastSeq lost 번호는 나갔는데 행이 없다 — 쓰기를 잃었다
|
|
21
|
+
* 행이 없고 n > lastSeq unissued 아직 그 번호까지 안 갔다
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* **`unknown` 은 우리가 쓸 일이 없다.** 채번을 따로 안 드는 원본(그때는 「행이 없다」에서 둘을 못
|
|
25
|
+
* 가른다)을 위해 계약에 있는 값이고, 여기서는 `lastSeq` 가 늘 있다. 없으면 그때는 `unknown` 이다 —
|
|
26
|
+
* 모르는 것을 `lost` 로 단정하면 **사람이 없는 사실을 찾으러 간다.**
|
|
27
|
+
*
|
|
28
|
+
* ── 순수 함수다 ────────────────────────────────────────────────────────────
|
|
29
|
+
* 저장소를 모른다. 부르는 쪽이 있는 번호와 채번을 읽어 넘긴다.
|
|
30
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
31
|
+
*/
|
|
32
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
33
|
+
exports.SEQ_REPORT_MAX = void 0;
|
|
34
|
+
exports.seqReport = seqReport;
|
|
35
|
+
exports.countGaps = countGaps;
|
|
36
|
+
exports.runsOf = runsOf;
|
|
37
|
+
/**
|
|
38
|
+
* 한 번에 낼 수 있는 번호 수.
|
|
39
|
+
*
|
|
40
|
+
* 이 문은 **구멍 하나를 들여다보는 자리**다. 수천 개를 한 번에 묻는 것은 이 문의 쓰임이 아니고,
|
|
41
|
+
* 그렇게 쓰면 봉투 원문이 그만큼 나간다.
|
|
42
|
+
*/
|
|
43
|
+
exports.SEQ_REPORT_MAX = 200;
|
|
44
|
+
function seqReport(input) {
|
|
45
|
+
const { scope, known, lastSeq } = input;
|
|
46
|
+
/* 뒤집힌 구간은 빈 것으로 답한다 — 지어내지 않는다. */
|
|
47
|
+
const from = Math.max(1, Math.floor(input.from));
|
|
48
|
+
const to = Math.floor(input.to);
|
|
49
|
+
if (to < from) {
|
|
50
|
+
return { scope, entries: [] };
|
|
51
|
+
}
|
|
52
|
+
const wanted = to - from + 1;
|
|
53
|
+
const take = Math.min(wanted, exports.SEQ_REPORT_MAX);
|
|
54
|
+
const bySeq = new Map(known.map(one => [one.seq, one]));
|
|
55
|
+
const entries = [];
|
|
56
|
+
for (let seq = from; seq < from + take; seq++) {
|
|
57
|
+
const found = bySeq.get(seq);
|
|
58
|
+
if (found) {
|
|
59
|
+
const entry = { seq, state: 'present' };
|
|
60
|
+
if (found.at)
|
|
61
|
+
entry.at = found.at;
|
|
62
|
+
if (found.record !== undefined)
|
|
63
|
+
entry.record = found.record;
|
|
64
|
+
entries.push(entry);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (lastSeq === undefined) {
|
|
68
|
+
/* 채번을 모르면 잃은 것과 아직인 것을 못 가른다. 단정하지 않는다. */
|
|
69
|
+
entries.push({ seq, state: 'unknown' });
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
entries.push({ seq, state: seq <= lastSeq ? 'lost' : 'unissued' });
|
|
73
|
+
}
|
|
74
|
+
return { scope, entries, ...(take < wanted ? { truncated: true } : {}) };
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* How many numbers are missing, counted per run.
|
|
78
|
+
*
|
|
79
|
+
* ── Why the caller passes rows and not a query ───────────────────────────
|
|
80
|
+
* This is the property the receiving side actually needs — the numbers run unbroken — and it is
|
|
81
|
+
* worth testing without a database. Leaving the arithmetic inside the probe meant the only way to
|
|
82
|
+
* prove it catches a gap was to delete a row and put it back.
|
|
83
|
+
*
|
|
84
|
+
* ── Counted per run, summed ──────────────────────────────────────────────
|
|
85
|
+
* Sequence numbers run per domain and channel. A server carrying two plants has two independent
|
|
86
|
+
* runs, and merging them reports a gap where there is none.
|
|
87
|
+
*
|
|
88
|
+
* Duplicates do not create gaps and are not counted here — that is a different fault with a
|
|
89
|
+
* different fix.
|
|
90
|
+
*/
|
|
91
|
+
function countGaps(runs) {
|
|
92
|
+
let gaps = 0;
|
|
93
|
+
for (const run of runs) {
|
|
94
|
+
const sorted = [...new Set(run.filter(Number.isFinite))].sort((a, b) => a - b);
|
|
95
|
+
for (let at = 1; at < sorted.length; at++) {
|
|
96
|
+
gaps += sorted[at] - sorted[at - 1] - 1;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return gaps;
|
|
100
|
+
}
|
|
101
|
+
/** Group rows into one run per owner. */
|
|
102
|
+
function runsOf(rows) {
|
|
103
|
+
const runs = new Map();
|
|
104
|
+
for (const row of rows) {
|
|
105
|
+
const seq = Number(row.seq);
|
|
106
|
+
if (!Number.isFinite(seq))
|
|
107
|
+
continue;
|
|
108
|
+
runs.set(row.owner, [...(runs.get(row.owner) ?? []), seq]);
|
|
109
|
+
}
|
|
110
|
+
return runs;
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=seq-report.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seq-report.js","sourceRoot":"","sources":["../../../server/service/outbox/seq-report.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * **번호 하나하나가 어떤 상태인가** — 있나 · 잃었나 · 아직인가.\n *\n * ═══════════════════════════════════════════════════════════════════════════\n * ── 왜 이 문이 필요한가 ────────────────────────────────────────────────────\n * 받는 쪽이 구멍을 보면 그 자리에서 멈춘다 — 「418 다음이 420」. 옳은 거동이다. 그런데 **그 419 가\n * 무엇이었는지 물을 자리가 없었다.**\n *\n * 2026-09-06에 실제로 그랬다. 트윈이 419 에서 막혔고, 그것이 무엇이었는지 알아내려고 사람이\n * 데이터베이스를 손으로 뒤졌다. 뒤져 보니 부하 시험이 번호만 가져가고 죽은 자리였다.\n * 이 문이 있었으면 **한 번의 조회**였다.\n *\n * ── 셋을 가르는 근거는 채번이다 ────────────────────────────────────────────\n * 아웃박스는 번호를 따로 들고 있다(`MesOutboxSequence.lastSeq`). 그래서 「행이 없다」를 둘로 가를\n * 수 있다.\n *\n * ```\n * 행이 있다 present 그 봉투를 그대로 낼 수 있다\n * 행이 없고 n <= lastSeq lost 번호는 나갔는데 행이 없다 — 쓰기를 잃었다\n * 행이 없고 n > lastSeq unissued 아직 그 번호까지 안 갔다\n * ```\n *\n * **`unknown` 은 우리가 쓸 일이 없다.** 채번을 따로 안 드는 원본(그때는 「행이 없다」에서 둘을 못\n * 가른다)을 위해 계약에 있는 값이고, 여기서는 `lastSeq` 가 늘 있다. 없으면 그때는 `unknown` 이다 —\n * 모르는 것을 `lost` 로 단정하면 **사람이 없는 사실을 찾으러 간다.**\n *\n * ── 순수 함수다 ────────────────────────────────────────────────────────────\n * 저장소를 모른다. 부르는 쪽이 있는 번호와 채번을 읽어 넘긴다.\n * ═══════════════════════════════════════════════════════════════════════════\n */\n\nexport type SeqState = 'present' | 'lost' | 'unissued' | 'unknown'\n\nexport interface SeqEntry {\n seq: number\n state: SeqState\n /** 언제 났나 — `present` 일 때만. */\n at?: string\n /** 봉투 자체 — `present` 이고 낼 수 있을 때만. */\n record?: unknown\n}\n\nexport interface SeqReport {\n scope: string\n entries: SeqEntry[]\n /** 상한에 닿아 다 못 냈다. 조용히 자르면 읽는 쪽이 그 뒤를 「없다」로 읽는다. */\n truncated?: boolean\n}\n\n/** 있는 번호 하나 — 부르는 쪽이 행에서 옮겨 온다. */\nexport interface KnownSeq {\n seq: number\n at?: string\n record?: unknown\n}\n\n/**\n * 한 번에 낼 수 있는 번호 수.\n *\n * 이 문은 **구멍 하나를 들여다보는 자리**다. 수천 개를 한 번에 묻는 것은 이 문의 쓰임이 아니고,\n * 그렇게 쓰면 봉투 원문이 그만큼 나간다.\n */\nexport const SEQ_REPORT_MAX = 200\n\nexport function seqReport(input: {\n scope: string\n from: number\n to: number\n /** 이 구간에서 실제로 있는 번호들. 순서는 상관없다. */\n known: readonly KnownSeq[]\n /** 채번이 어디까지 갔나. **모르면 `undefined`** — 그때 없는 번호는 전부 `unknown` 이다. */\n lastSeq?: number\n}): SeqReport {\n const { scope, known, lastSeq } = input\n\n /* 뒤집힌 구간은 빈 것으로 답한다 — 지어내지 않는다. */\n const from = Math.max(1, Math.floor(input.from))\n const to = Math.floor(input.to)\n\n if (to < from) {\n return { scope, entries: [] }\n }\n\n const wanted = to - from + 1\n const take = Math.min(wanted, SEQ_REPORT_MAX)\n\n const bySeq = new Map(known.map(one => [one.seq, one]))\n const entries: SeqEntry[] = []\n\n for (let seq = from; seq < from + take; seq++) {\n const found = bySeq.get(seq)\n\n if (found) {\n const entry: SeqEntry = { seq, state: 'present' }\n if (found.at) entry.at = found.at\n if (found.record !== undefined) entry.record = found.record\n entries.push(entry)\n continue\n }\n\n if (lastSeq === undefined) {\n /* 채번을 모르면 잃은 것과 아직인 것을 못 가른다. 단정하지 않는다. */\n entries.push({ seq, state: 'unknown' })\n continue\n }\n\n entries.push({ seq, state: seq <= lastSeq ? 'lost' : 'unissued' })\n }\n\n return { scope, entries, ...(take < wanted ? { truncated: true } : {}) }\n}\n\n/**\n * How many numbers are missing, counted per run.\n *\n * ── Why the caller passes rows and not a query ───────────────────────────\n * This is the property the receiving side actually needs — the numbers run unbroken — and it is\n * worth testing without a database. Leaving the arithmetic inside the probe meant the only way to\n * prove it catches a gap was to delete a row and put it back.\n *\n * ── Counted per run, summed ──────────────────────────────────────────────\n * Sequence numbers run per domain and channel. A server carrying two plants has two independent\n * runs, and merging them reports a gap where there is none.\n *\n * Duplicates do not create gaps and are not counted here — that is a different fault with a\n * different fix.\n */\nexport function countGaps(runs: Iterable<readonly number[]>): number {\n let gaps = 0\n\n for (const run of runs) {\n const sorted = [...new Set(run.filter(Number.isFinite))].sort((a, b) => a - b)\n\n for (let at = 1; at < sorted.length; at++) {\n gaps += sorted[at] - sorted[at - 1] - 1\n }\n }\n\n return gaps\n}\n\n/** Group rows into one run per owner. */\nexport function runsOf(rows: readonly { owner: string; seq: number }[]): Map<string, number[]> {\n const runs = new Map<string, number[]>()\n\n for (const row of rows) {\n const seq = Number(row.seq)\n if (!Number.isFinite(seq)) continue\n\n runs.set(row.owner, [...(runs.get(row.owner) ?? []), seq])\n }\n\n return runs\n}\n"]}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **일부만 들어갔을 때 무엇을 어떻게 표시하나** — 순수 판단.
|
|
3
|
+
*
|
|
4
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
5
|
+
* ── 왜 갈라 두나 ───────────────────────────────────────────────────────────
|
|
6
|
+
* 이 판단은 저장소와 붙어 있어서 걸어 볼 수가 없었다. 그런데 **여기가 틀리면 가장 조용하다** —
|
|
7
|
+
* 사실이 두 번 들어가거나 영영 안 들어가는데 오류는 안 난다.
|
|
8
|
+
*
|
|
9
|
+
* 실제로 오늘 다른 자리에서 그 부류를 겪었다: 컴파일도 판단 테스트도 못 잡고 화면에서야 터졌다.
|
|
10
|
+
* 그래서 저장소를 떼고 **무엇을 어떻게 표시할지만** 여기서 정한다.
|
|
11
|
+
*
|
|
12
|
+
* ── 셋으로 갈린다 ──────────────────────────────────────────────────────────
|
|
13
|
+
* ```
|
|
14
|
+
* 들어갔다 lastSeq 이하이고 거절 목록에 없다 → 보냄
|
|
15
|
+
* 거절됐다 lastSeq 이하인데 거절 목록에 있다 → 실패(다시 안 보낸다)
|
|
16
|
+
* 아직이다 lastSeq 보다 뒤다 → 그대로 둔다(다음에 보낸다)
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* **거절된 것을 다시 보내지 않는다.** 다시 보내면 그것은 또 거절되고, **함께 간 나머지가 두 번**
|
|
20
|
+
* 들어간다. 받는 쪽이 봉투 단위로 걸러 주더라도 그 처리량을 매 주기 쓰게 된다.
|
|
21
|
+
*
|
|
22
|
+
* ── `lastSeq` 를 모르면 아무것도 표시하지 않는다 ───────────────────────────
|
|
23
|
+
* 무엇까지 들어갔는지 모르는 채로 「보냄」을 찍으면 **그 사실이 사라진다.** 다시 보내는 쪽이 안전하다 —
|
|
24
|
+
* 받는 쪽이 중복을 거르는 것은 되돌릴 수 있지만, 안 보낸 것은 아무도 모른다.
|
|
25
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
26
|
+
*/
|
|
27
|
+
/** 이 판단이 보는 것만. 엔티티를 끌어오지 않는다. */
|
|
28
|
+
export interface SettleableEvent {
|
|
29
|
+
/** 저장소의 행 식별자 — 표시할 때 쓴다. */
|
|
30
|
+
id: string;
|
|
31
|
+
/** 봉투의 식별자 — 받는 쪽이 거절 목록에 이 값을 담는다. */
|
|
32
|
+
eventId: string;
|
|
33
|
+
/** 도메인 안에서 매긴 번호. 문자열로 올 수 있어 읽는 쪽에서 숫자로 본다. */
|
|
34
|
+
seq: number | string;
|
|
35
|
+
}
|
|
36
|
+
export interface Settlement {
|
|
37
|
+
/** 보냄으로 표시할 행들. */
|
|
38
|
+
sentIds: string[];
|
|
39
|
+
/** 실패로 표시할 행들 — **다시 보내지 않는다.** */
|
|
40
|
+
failedIds: string[];
|
|
41
|
+
/** 그대로 두는 행들 — 다음 주기에 다시 보낸다. */
|
|
42
|
+
pendingIds: string[];
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* 무엇을 어떻게 표시할지 가른다.
|
|
46
|
+
*
|
|
47
|
+
* `lastSeq` 가 없으면 **아무것도 표시하지 않는다**(머리말). 그때는 전부 그대로 남는다.
|
|
48
|
+
*/
|
|
49
|
+
export declare function settlement(pending: readonly SettleableEvent[], lastSeq: number | undefined, rejectedIds?: readonly string[]): Settlement;
|
|
50
|
+
/**
|
|
51
|
+
* **받는 쪽이 구멍을 말했다** — 어디부터 다시 보내야 하나.
|
|
52
|
+
*
|
|
53
|
+
* ── 이미 보냄으로 표시한 것도 되돌린다 ─────────────────────────────────────
|
|
54
|
+
* 받는 쪽이 `expectedSeq` 부터 달라고 하면 그 번호 뒤엣것은 **받는 쪽에 없다.** 우리가 보냄으로
|
|
55
|
+
* 표시해 두었더라도 그렇다 — 표시는 우리 기록이고 받는 쪽의 사실이 아니다.
|
|
56
|
+
*
|
|
57
|
+
* 되돌리지 않으면 그 구간은 **영영 안 간다.** 우리는 보냈다고 알고 받는 쪽은 없는 채로 남는다.
|
|
58
|
+
*
|
|
59
|
+
* ── 구멍 앞은 건드리지 않는다 ──────────────────────────────────────────────
|
|
60
|
+
* `expectedSeq` 보다 앞은 실제로 들어갔다. 그것까지 되돌리면 매 주기 같은 봉투를 다시 보내고, 받는
|
|
61
|
+
* 쪽이 중복으로 거르면서 처리량만 쓴다.
|
|
62
|
+
*/
|
|
63
|
+
export declare function rewindsFrom(expectedSeq: number, events: readonly SettleableEvent[]): string[];
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* **일부만 들어갔을 때 무엇을 어떻게 표시하나** — 순수 판단.
|
|
4
|
+
*
|
|
5
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
6
|
+
* ── 왜 갈라 두나 ───────────────────────────────────────────────────────────
|
|
7
|
+
* 이 판단은 저장소와 붙어 있어서 걸어 볼 수가 없었다. 그런데 **여기가 틀리면 가장 조용하다** —
|
|
8
|
+
* 사실이 두 번 들어가거나 영영 안 들어가는데 오류는 안 난다.
|
|
9
|
+
*
|
|
10
|
+
* 실제로 오늘 다른 자리에서 그 부류를 겪었다: 컴파일도 판단 테스트도 못 잡고 화면에서야 터졌다.
|
|
11
|
+
* 그래서 저장소를 떼고 **무엇을 어떻게 표시할지만** 여기서 정한다.
|
|
12
|
+
*
|
|
13
|
+
* ── 셋으로 갈린다 ──────────────────────────────────────────────────────────
|
|
14
|
+
* ```
|
|
15
|
+
* 들어갔다 lastSeq 이하이고 거절 목록에 없다 → 보냄
|
|
16
|
+
* 거절됐다 lastSeq 이하인데 거절 목록에 있다 → 실패(다시 안 보낸다)
|
|
17
|
+
* 아직이다 lastSeq 보다 뒤다 → 그대로 둔다(다음에 보낸다)
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* **거절된 것을 다시 보내지 않는다.** 다시 보내면 그것은 또 거절되고, **함께 간 나머지가 두 번**
|
|
21
|
+
* 들어간다. 받는 쪽이 봉투 단위로 걸러 주더라도 그 처리량을 매 주기 쓰게 된다.
|
|
22
|
+
*
|
|
23
|
+
* ── `lastSeq` 를 모르면 아무것도 표시하지 않는다 ───────────────────────────
|
|
24
|
+
* 무엇까지 들어갔는지 모르는 채로 「보냄」을 찍으면 **그 사실이 사라진다.** 다시 보내는 쪽이 안전하다 —
|
|
25
|
+
* 받는 쪽이 중복을 거르는 것은 되돌릴 수 있지만, 안 보낸 것은 아무도 모른다.
|
|
26
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
27
|
+
*/
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.settlement = settlement;
|
|
30
|
+
exports.rewindsFrom = rewindsFrom;
|
|
31
|
+
/**
|
|
32
|
+
* 무엇을 어떻게 표시할지 가른다.
|
|
33
|
+
*
|
|
34
|
+
* `lastSeq` 가 없으면 **아무것도 표시하지 않는다**(머리말). 그때는 전부 그대로 남는다.
|
|
35
|
+
*/
|
|
36
|
+
function settlement(pending, lastSeq, rejectedIds = []) {
|
|
37
|
+
if (lastSeq === undefined) {
|
|
38
|
+
return { sentIds: [], failedIds: [], pendingIds: pending.map(one => one.id) };
|
|
39
|
+
}
|
|
40
|
+
const rejected = new Set(rejectedIds);
|
|
41
|
+
const found = { sentIds: [], failedIds: [], pendingIds: [] };
|
|
42
|
+
for (const event of pending) {
|
|
43
|
+
if (Number(event.seq) > lastSeq) {
|
|
44
|
+
found.pendingIds.push(event.id);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
;
|
|
48
|
+
(rejected.has(event.eventId) ? found.failedIds : found.sentIds).push(event.id);
|
|
49
|
+
}
|
|
50
|
+
return found;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* **받는 쪽이 구멍을 말했다** — 어디부터 다시 보내야 하나.
|
|
54
|
+
*
|
|
55
|
+
* ── 이미 보냄으로 표시한 것도 되돌린다 ─────────────────────────────────────
|
|
56
|
+
* 받는 쪽이 `expectedSeq` 부터 달라고 하면 그 번호 뒤엣것은 **받는 쪽에 없다.** 우리가 보냄으로
|
|
57
|
+
* 표시해 두었더라도 그렇다 — 표시는 우리 기록이고 받는 쪽의 사실이 아니다.
|
|
58
|
+
*
|
|
59
|
+
* 되돌리지 않으면 그 구간은 **영영 안 간다.** 우리는 보냈다고 알고 받는 쪽은 없는 채로 남는다.
|
|
60
|
+
*
|
|
61
|
+
* ── 구멍 앞은 건드리지 않는다 ──────────────────────────────────────────────
|
|
62
|
+
* `expectedSeq` 보다 앞은 실제로 들어갔다. 그것까지 되돌리면 매 주기 같은 봉투를 다시 보내고, 받는
|
|
63
|
+
* 쪽이 중복으로 거르면서 처리량만 쓴다.
|
|
64
|
+
*/
|
|
65
|
+
function rewindsFrom(expectedSeq, events) {
|
|
66
|
+
return events.filter(one => Number(one.seq) >= expectedSeq).map(one => one.id);
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=settlement.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settlement.js","sourceRoot":"","sources":["../../../server/service/outbox/settlement.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;;AA0BH,gCAsBC;AAeD,kCAEC;AA5CD;;;;GAIG;AACH,SAAgB,UAAU,CACxB,OAAmC,EACnC,OAA2B,EAC3B,cAAiC,EAAE;IAEnC,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC1B,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,SAAS,EAAE,EAAE,EAAE,UAAU,EAAE,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAA;IAC/E,CAAC;IAED,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,WAAW,CAAC,CAAA;IACrC,MAAM,KAAK,GAAe,EAAE,OAAO,EAAE,EAAE,EAAE,SAAS,EAAE,EAAE,EAAE,UAAU,EAAE,EAAE,EAAE,CAAA;IAExE,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,IAAI,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,OAAO,EAAE,CAAC;YAChC,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAA;YAC/B,SAAQ;QACV,CAAC;QAED,CAAC;QAAA,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAA;IACjF,CAAC;IAED,OAAO,KAAK,CAAA;AACd,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,SAAgB,WAAW,CAAC,WAAmB,EAAE,MAAkC;IACjF,OAAO,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,WAAW,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;AAChF,CAAC","sourcesContent":["/**\n * **일부만 들어갔을 때 무엇을 어떻게 표시하나** — 순수 판단.\n *\n * ═══════════════════════════════════════════════════════════════════════════\n * ── 왜 갈라 두나 ───────────────────────────────────────────────────────────\n * 이 판단은 저장소와 붙어 있어서 걸어 볼 수가 없었다. 그런데 **여기가 틀리면 가장 조용하다** —\n * 사실이 두 번 들어가거나 영영 안 들어가는데 오류는 안 난다.\n *\n * 실제로 오늘 다른 자리에서 그 부류를 겪었다: 컴파일도 판단 테스트도 못 잡고 화면에서야 터졌다.\n * 그래서 저장소를 떼고 **무엇을 어떻게 표시할지만** 여기서 정한다.\n *\n * ── 셋으로 갈린다 ──────────────────────────────────────────────────────────\n * ```\n * 들어갔다 lastSeq 이하이고 거절 목록에 없다 → 보냄\n * 거절됐다 lastSeq 이하인데 거절 목록에 있다 → 실패(다시 안 보낸다)\n * 아직이다 lastSeq 보다 뒤다 → 그대로 둔다(다음에 보낸다)\n * ```\n *\n * **거절된 것을 다시 보내지 않는다.** 다시 보내면 그것은 또 거절되고, **함께 간 나머지가 두 번**\n * 들어간다. 받는 쪽이 봉투 단위로 걸러 주더라도 그 처리량을 매 주기 쓰게 된다.\n *\n * ── `lastSeq` 를 모르면 아무것도 표시하지 않는다 ───────────────────────────\n * 무엇까지 들어갔는지 모르는 채로 「보냄」을 찍으면 **그 사실이 사라진다.** 다시 보내는 쪽이 안전하다 —\n * 받는 쪽이 중복을 거르는 것은 되돌릴 수 있지만, 안 보낸 것은 아무도 모른다.\n * ═══════════════════════════════════════════════════════════════════════════\n */\n\n/** 이 판단이 보는 것만. 엔티티를 끌어오지 않는다. */\nexport interface SettleableEvent {\n /** 저장소의 행 식별자 — 표시할 때 쓴다. */\n id: string\n /** 봉투의 식별자 — 받는 쪽이 거절 목록에 이 값을 담는다. */\n eventId: string\n /** 도메인 안에서 매긴 번호. 문자열로 올 수 있어 읽는 쪽에서 숫자로 본다. */\n seq: number | string\n}\n\nexport interface Settlement {\n /** 보냄으로 표시할 행들. */\n sentIds: string[]\n /** 실패로 표시할 행들 — **다시 보내지 않는다.** */\n failedIds: string[]\n /** 그대로 두는 행들 — 다음 주기에 다시 보낸다. */\n pendingIds: string[]\n}\n\n/**\n * 무엇을 어떻게 표시할지 가른다.\n *\n * `lastSeq` 가 없으면 **아무것도 표시하지 않는다**(머리말). 그때는 전부 그대로 남는다.\n */\nexport function settlement(\n pending: readonly SettleableEvent[],\n lastSeq: number | undefined,\n rejectedIds: readonly string[] = []\n): Settlement {\n if (lastSeq === undefined) {\n return { sentIds: [], failedIds: [], pendingIds: pending.map(one => one.id) }\n }\n\n const rejected = new Set(rejectedIds)\n const found: Settlement = { sentIds: [], failedIds: [], pendingIds: [] }\n\n for (const event of pending) {\n if (Number(event.seq) > lastSeq) {\n found.pendingIds.push(event.id)\n continue\n }\n\n ;(rejected.has(event.eventId) ? found.failedIds : found.sentIds).push(event.id)\n }\n\n return found\n}\n\n/**\n * **받는 쪽이 구멍을 말했다** — 어디부터 다시 보내야 하나.\n *\n * ── 이미 보냄으로 표시한 것도 되돌린다 ─────────────────────────────────────\n * 받는 쪽이 `expectedSeq` 부터 달라고 하면 그 번호 뒤엣것은 **받는 쪽에 없다.** 우리가 보냄으로\n * 표시해 두었더라도 그렇다 — 표시는 우리 기록이고 받는 쪽의 사실이 아니다.\n *\n * 되돌리지 않으면 그 구간은 **영영 안 간다.** 우리는 보냈다고 알고 받는 쪽은 없는 채로 남는다.\n *\n * ── 구멍 앞은 건드리지 않는다 ──────────────────────────────────────────────\n * `expectedSeq` 보다 앞은 실제로 들어갔다. 그것까지 되돌리면 매 주기 같은 봉투를 다시 보내고, 받는\n * 쪽이 중복으로 거르면서 처리량만 쓴다.\n */\nexport function rewindsFrom(expectedSeq: number, events: readonly SettleableEvent[]): string[] {\n return events.filter(one => Number(one.seq) >= expectedSeq).map(one => one.id)\n}\n"]}
|