@startuml inline_umlgraph_1.svg
title 送信者のスレッド構成

rectangle "アプリケーション" {
  [potrSend()]
}

rectangle "porter ライブラリ (送信者) " {
  queue "送信キュー\n(リングバッファ 1024 要素)" as Q
  [送信スレッド] as ST
  [受信スレッド] as RT
  [ヘルスチェックスレッド] as HT
  [UDP ソケット] as SOCK
}

[potrSend()] --> Q : エレメントを push
Q --> ST : pop → パケット構築
ST --> SOCK : sendto\n(DATA / PING / 再送)
RT <-- SOCK : recvfrom\n(NACK / REJECT / FIN)
RT --> ST : 再送要求・REJECT 通知
HT --> SOCK : sendto (PING)\n一定間隔で送信
@enduml
@startuml inline_umlgraph_2.svg
title unicast_bidir 1:1 のスレッド構成

rectangle "アプリケーション" {
  [potrSend(peer_id=POTR_PEER_NA)]
  [コールバック]
}

rectangle "porter ライブラリ (unicast_bidir 1:1)" {
  queue "送信キュー\n(リングバッファ 1024 要素)" as Q
  [送信スレッド] as ST
  [受信スレッド] as RT
  [ヘルスチェックスレッド] as HT
  [UDP ソケット\n(bind 済み)] as SOCK
}

[potrSend(peer_id=POTR_PEER_NA)] --> Q : エレメントを push
Q --> ST : pop → パケット構築
ST --> SOCK : sendto\n(DATA / 再送)
HT --> SOCK : sendto (PING 要求)\n一定間隔で送信
RT <-- SOCK : recvfrom\n(DATA / NACK / REJECT / PING 要求 / PING 応答)
RT --> ST : 再送要求（NACK）・REJECT 通知
RT --> SOCK : sendto (PING 応答)\nPING 要求受信時に即返答
RT --> [コールバック] : DATA / CONNECTED / DISCONNECTED
@enduml
@startuml inline_umlgraph_3.svg
title unicast_bidir N:1 サーバのスレッド構成

rectangle "アプリケーション" {
  [potrSend(peer_id)]
  [コールバック]
}

rectangle "porter ライブラリ (unicast_bidir N:1 サーバ)" {
  queue "送信キュー\n(共有)" as Q
  database "peer table\n(max_peers)" as PT
  [送信スレッド] as ST
  [受信スレッド] as RT
  [ヘルスチェックスレッド] as HT
  [UDP ソケット\n(dst_addr:dst_port で bind)] as SOCK
}

[potrSend(peer_id)] --> Q : peer_id 付きで push
Q --> ST : pop → peer_id で送信先解決
ST --> PT : peer_id ごとの send_window / 送信先参照
ST --> SOCK : sendto(peer ごとの dest_addr)
RT <-- SOCK : recvfrom(DATA / NACK / REJECT / PING)
RT --> PT : session triplet でピア検索 / 新規作成
RT --> [コールバック] : DATA / CONNECTED / DISCONNECTED\n(peer_id 付き)
HT --> PT : 全ピアを巡回
HT --> SOCK : PING 要求送信 / タイムアウト監視
@enduml
@startuml inline_umlgraph_4.svg
title TCP SENDER のスレッド構成（path 数 = N の例）

rectangle "アプリケーション" {
  [potrSend()]
}

rectangle "porter ライブラリ (TCP SENDER)" {
  [connect スレッド #0] as CT0
  [connect スレッド #N-1] as CTN
  queue "送信キュー\n(リングバッファ)" as Q
  [送信スレッド] as ST
  [recv スレッド #0\n(PING 応答 / DATA)] as RT0
  [recv スレッド #N-1] as RTN
  [health スレッド #0] as HT0
  [health スレッド #N-1] as HTN
  [TCP ソケット #0] as S0
  [TCP ソケット #N-1] as SN
}

[potrSend()] --> Q
CT0 --> S0 : connect()
CTN --> SN : connect()
Q --> ST : pop → パケット構築
ST --> S0 : send (DATA)
ST --> SN : send (DATA)
HT0 --> S0 : send (PING)
HTN --> SN : send (PING)
RT0 <-- S0 : recv (PING 応答)
RTN <-- SN : recv (PING 応答)
RT0 --> HT0 : 応答通知
RTN --> HTN : 応答通知

note over RT0, RTN: DATA 受信は POTR_TYPE_TCP_BIDIR 時のみ
@enduml
@startuml inline_umlgraph_5.svg
title TCP RECEIVER のスレッド構成（path 数 = N の例）

rectangle "アプリケーション" {
  [コールバック]
}

rectangle "porter ライブラリ (TCP RECEIVER)" {
  [accept スレッド #0] as AT0
  [accept スレッド #N-1] as ATN
  [recv スレッド #0] as RT0
  [recv スレッド #N-1] as RTN
  [TCP listen ソケット #0] as LS0
  [TCP listen ソケット #N-1] as LSN
  [TCP 接続ソケット #0] as CS0
  [TCP 接続ソケット #N-1] as CSN
}

AT0 --> LS0 : accept() 待機
ATN --> LSN : accept() 待機
AT0 --> CS0 : 接続確立後に起動
ATN --> CSN : 接続確立後に起動
CS0 --> RT0 : recv (DATA / PING 要求)
CSN --> RTN : recv (DATA / PING 要求)
RT0 --> CS0 : send (PING 応答)
RTN --> CSN : send (PING 応答)
RT0 --> [コールバック] : DATA / CONNECTED / DISCONNECTED
RTN --> [コールバック] : DATA / DISCONNECTED
@enduml
@startuml inline_umlgraph_6.svg
title porter コンポーネント構成

package "api" {
  [potrOpenService]
  [potrOpenServiceFromConfig]
  [potrSend]
  [potrCloseService]
}

package "thread" {
  [potrRecvThread]
  [potrSendThread]
  [potrHealthThread]
  [potrConnectThread]
}

package "protocol" {
  [packet\n(パケット構築・解析)]
  [window\n(スライディングウィンドウ)]
  [seqnum\n(通番管理)]
  [config\n(設定ファイル解析)]
}

package "infra" {
  [potrSendQueue\n(リングバッファ)]
  [potrLog\n(ロギング・設定)]
  package "compress" {
    [compress\n(raw DEFLATE)]
  }
  package "crypto" {
    [crypto\n(AES-256-GCM)]
  }
}

package "util" {
  [potrIpAddr\n(名前解決)]
}

database "PotrContext\n(セッション全状態)" as CTX

[potrOpenService] --> CTX : 生成・初期化
[potrOpenServiceFromConfig] --> [potrOpenService] : 委譲
[potrSend] --> [potrSendQueue] : エレメント push
[potrCloseService] --> CTX : スレッド停止・解放

[potrSendThread] --> [packet]
[potrSendThread] --> [window]
[potrSendThread] --> [compress]
[potrSendThread] --> [crypto]
[potrSendThread] --> [potrSendQueue]

[potrRecvThread] --> [packet]
[potrRecvThread] --> [window]
[potrRecvThread] --> [seqnum]
[potrRecvThread] --> [compress]
[potrRecvThread] --> [crypto]

api -[hidden]--thread
thread -[hidden]-- infra
api -[hidden]----- protocol
protocol -[hidden]--util
@enduml
@startuml inline_umlgraph_7.svg
title unicast のソケット bind / sendto

participant "送信者\nsrc_addr:src_port" as S
participant "受信者\ndst_addr:dst_port" as R

note over S: bind(src_addr, src_port)\n src_port=0 → OS 自動選定
note over R: bind(dst_addr, dst_port)

S -> R: sendto(dst_addr, dst_port)\nDATA / PING / FIN
R -> S: sendto(src_addr, src_port)\nNACK (NACK 送信元は dst_addr:dst_port)
@enduml
@startuml inline_umlgraph_8.svg
title unicast_bidir 1:1 のソケット bind / sendto

participant "Side A\n(POTR_ROLE_SENDER)\nsrc_addr=A, src_port=PA" as A
participant "Side B\n(POTR_ROLE_RECEIVER)\ndst_addr=B, dst_port=PB" as B

note over A: bind(src_addr=A, src_port=PA または 0)
note over B: bind(dst_addr=B, dst_port=PB)

A -> B: sendto(dst_addr=B, dst_port=PB)\nDATA / PING 要求 / PING 応答 / NACK / REJECT
B -> A: sendto(dst_addr=A, learned or configured port)\nDATA / PING 要求 / PING 応答 / NACK / REJECT
@enduml
@startuml inline_umlgraph_9.svg
title unicast_bidir_n1 のソケット bind / sendto

participant "Client A\n(unicast_bidir 1:1)" as CA
participant "Client B\n(unicast_bidir 1:1)" as CB
participant "Server\n(unicast_bidir_n1\nPOTR_ROLE_RECEIVER)" as S

note over S: bind(dst_addr, dst_port)\ndst_addr 省略時は INADDR_ANY
note over CA: unicast_bidir クライアント
note over CB: unicast_bidir クライアント

CA -> S: sendto(server dst_addr, dst_port)\nDATA / PING / NACK / REJECT
CB -> S: sendto(server dst_addr, dst_port)\nDATA / PING / NACK / REJECT
S -> CA: sendto(recvfrom で学習した送信元)\nDATA / PING / NACK / REJECT
S -> CB: sendto(recvfrom で学習した送信元)\nDATA / PING / NACK / REJECT
@enduml
@startuml inline_umlgraph_10.svg
title multicast のソケット bind / sendto

participant "送信者\nsrc_addr" as S
participant "受信者 A\nsrc_addr_A" as RA
participant "受信者 B\nsrc_addr_B" as RB

note over S: bind(INADDR_ANY, src_port)\n IP_MULTICAST_IF = src_addr
note over RA: bind(INADDR_ANY, dst_port)\n グループ参加: src_addr_A
note over RB: bind(INADDR_ANY, dst_port)\n グループ参加: src_addr_B

S -> RA: sendto(multicast_group, dst_port)\nDATA / PING
S -> RB: sendto(multicast_group, dst_port)\nDATA / PING
RA -> S: sendto(src_addr, src_port)\nNACK (全パスからユニキャスト)
@enduml
@startuml inline_umlgraph_11.svg
title tcp のソケット connect / accept

participant "SENDER\n(TCP クライアント)" as S
participant "RECEIVER\n(TCP サーバー)" as R

note over R: bind(dst_addr, dst_port)\nlisten()
note over S: connect(dst_addr, dst_port)
S -> R: TCP 3way handshake
note over R: accept() → 接続ソケット取得
@enduml
@startuml inline_umlgraph_12.svg
caption UDP 1:1 シングルパス - PATH_CONNECTED → CONNECTED

participant "送信者" as S
participant "UDP" as UDP
participant "受信スレッド" as RT
participant "アプリ\n(受信側)" as APP

note over RT: path_alive[0] = 0\nhealth_alive = 0

S -> UDP: DATA[seq=0, path=0]
UDP -> RT: recvfrom (path=0)

RT -> RT: update_path_recv(path_idx=0)\n→ path_alive[0] == 0\n→ path_alive[0] = 1

RT -> APP: callback(service_id, POTR_PEER_NA,\nPOTR_EVENT_PATH_CONNECTED,\npath_states={1,0,0,0}, len=0)

RT -> RT: notify_health_alive()\n→ health_alive == 0\n→ health_alive = 1

RT -> APP: callback(service_id, POTR_PEER_NA,\nPOTR_EVENT_CONNECTED, NULL, 0)
@enduml
@startuml inline_umlgraph_13.svg
caption UDP 1:1 マルチパス - PATH_CONNECTED × 2 → CONNECTED

participant "送信者" as S
participant "UDP" as UDP
participant "受信スレッド" as RT
participant "アプリ\n(受信側)" as APP

note over RT: path_alive = {0, 0, 0, 0}\nhealth_alive = 0

S -> UDP: DATA[seq=0, path=0]
UDP -> RT: recvfrom (path=0)

RT -> RT: update_path_recv(path_idx=0)\n→ path_alive[0] = 1

RT -> APP: callback(..., POTR_EVENT_PATH_CONNECTED,\npath_states={1,0,0,0}, len=0)

RT -> RT: notify_health_alive()\n→ health_alive = 1

RT -> APP: callback(..., POTR_EVENT_CONNECTED, NULL, 0)

S -> UDP: DATA[seq=0, path=1]
UDP -> RT: recvfrom (path=1)

RT -> RT: update_path_recv(path_idx=1)\n→ path_alive[1] = 1

RT -> APP: callback(..., POTR_EVENT_PATH_CONNECTED,\npath_states={1,1,0,0}, len=1)

RT -> RT: notify_health_alive()\n→ health_alive == 1 → 発火しない
@enduml
@startuml inline_umlgraph_14.svg
caption UDP 1:1 マルチパス - PATH_DISCONNECTED (セッションは維持)

participant "タイムアウト\n監視" as TM
participant "受信スレッド" as RT
participant "アプリ\n(受信側)" as APP

note over RT: path_alive = {1, 1, 0, 0}\nhealth_alive = 1\n(path=1 からは受信なし)

TM -> RT: select() タイムアウト\n→ check_health_timeout()

RT -> RT: path=1: elapsed >= health_timeout_ms\n→ path_alive[1] == 1\n→ path_alive[1] = 0

RT -> APP: callback(..., POTR_EVENT_PATH_DISCONNECTED,\npath_states={1,0,0,0}, len=1)

RT -> RT: peer_port[1] = 0\npath_last_recv_sec[1] = 0

RT -> RT: セッション全体のタイムアウト確認\n→ last_recv_tv_sec が新しい\n(path=0 から最近受信あり)\n→ DISCONNECTED 発火しない
@enduml
@startuml inline_umlgraph_15.svg
caption UDP 1:1 全パスタイムアウト - PATH_DISCONNECTED × 2 → DISCONNECTED

participant "タイムアウト\n監視" as TM
participant "受信スレッド" as RT
participant "アプリ\n(受信側)" as APP

note over RT: path_alive = {1, 1, 0, 0}\nhealth_alive = 1\n(両パスとも受信なし)

TM -> RT: check_health_timeout()

RT -> RT: path=0: elapsed >= health_timeout_ms\n→ path_alive[0] = 0
RT -> APP: callback(..., POTR_EVENT_PATH_DISCONNECTED,\npath_states={0,1,0,0}, len=0)

RT -> RT: path=1: elapsed >= health_timeout_ms\n→ path_alive[1] = 0
RT -> APP: callback(..., POTR_EVENT_PATH_DISCONNECTED,\npath_states={0,0,0,0}, len=1)

RT -> RT: セッション全体のタイムアウト確認\n→ elapsed >= health_timeout_ms\n→ health_alive = 0

RT -> APP: callback(..., POTR_EVENT_DISCONNECTED, NULL, 0)
@enduml
@startuml inline_umlgraph_16.svg
caption UDP N:1 - 新規ピア接続と PATH_CONNECTED

participant "クライアント" as CLI
participant "UDP" as UDP
participant "受信スレッド\n(N:1サーバ)" as RT
participant "アプリ\n(N:1サーバ)" as APP

CLI -> UDP: DATA[seq=0, path=0, session=X]
UDP -> RT: recvfrom (path=0)

RT -> RT: peer_find_by_session(X) → NULL\n→ peer_create()\n→ peer.peer_id = 42

RT -> RT: n1_update_path_recv(peer=42, path=0)\n→ dest_addr[0].sin_family == 0 (新規)\n→ dest_addr[0] = sender_addr\n→ n_paths = 1\n→ n1_fire_path_event(path=0, CONNECTED)

RT -> APP: callback(service_id, peer_id=42,\nPOTR_EVENT_PATH_CONNECTED,\npath_states={1,0,0,0}, len=0)

RT -> RT: n1_notify_health_alive(peer=42)\n→ health_alive = 1

RT -> APP: callback(service_id, peer_id=42,\nPOTR_EVENT_CONNECTED, NULL, 0)

CLI -> UDP: DATA[seq=1, path=1, session=X]
UDP -> RT: recvfrom (path=1)

RT -> RT: peer_find_by_session(X) → peer=42\nn1_update_path_recv(peer=42, path=1)\n→ dest_addr[1].sin_family == 0 (新規)\n→ n_paths = 2\n→ n1_fire_path_event(path=1, CONNECTED)

RT -> APP: callback(service_id, peer_id=42,\nPOTR_EVENT_PATH_CONNECTED,\npath_states={1,1,0,0}, len=1)
@enduml
@startuml inline_umlgraph_17.svg
caption TCP マルチパス - PATH_CONNECTED → CONNECTED → PATH_DISCONNECTED → DISCONNECTED

participant "接続管理\nスレッド[0]" as CT0
participant "接続管理\nスレッド[1]" as CT1
participant "受信\nスレッド[0]" as RT0
participant "受信\nスレッド[1]" as RT1
participant "アプリ" as APP

note over CT0, CT1: path_alive = {0, 0, 0, 0}\nhealth_alive = 0

== TCP 接続確立 ==

CT0 -> CT0: connect(dst[0]) 成功\ntcp_conn_fd[0] = sock0\ntcp_active_paths = 1
CT0 -> CT0: start_connected_threads(path=0)\n→ recv スレッド起動成功\n→ path_alive[0] = 1

CT0 -> APP: callback(...,\nPOTR_EVENT_PATH_CONNECTED,\npath_states={1,0,0,0}, len=0)

CT0 -> RT0**: recv スレッド[0] 起動済み

CT1 -> CT1: connect(dst[1]) 成功\ntcp_conn_fd[1] = sock1\ntcp_active_paths = 2
CT1 -> CT1: start_connected_threads(path=1)\n→ path_alive[1] = 1

CT1 -> APP: callback(...,\nPOTR_EVENT_PATH_CONNECTED,\npath_states={1,1,0,0}, len=1)

CT1 -> RT1**: recv スレッド[1] 起動済み

RT0 -> RT0: 最初のパケット受信\n→ notify_connected_tcp()\n→ health_alive = 1

RT0 -> APP: callback(...,\nPOTR_EVENT_CONNECTED, NULL, 0)

== path=1 のみ切断 ==

RT1 -> RT1: recv() エラー (TCP 切断)\n→ 受信ループ終了

CT1 -> CT1: join_recv_thread(path=1)\n→ path_alive[1] == 1\n→ path_alive[1] = 0

CT1 -> APP: callback(...,\nPOTR_EVENT_PATH_DISCONNECTED,\npath_states={1,0,0,0}, len=1)

CT1 -> CT1: stop_connected_threads(path=1)\ntcp_active_paths = 1\n→ 0 ではないので DISCONNECTED 発火しない

== path=0 も切断 ==

RT0 -> RT0: recv() エラー (TCP 切断)\n→ 受信ループ終了

CT0 -> CT0: join_recv_thread(path=0)\n→ path_alive[0] = 0

CT0 -> APP: callback(...,\nPOTR_EVENT_PATH_DISCONNECTED,\npath_states={0,0,0,0}, len=0)

CT0 -> CT0: stop_connected_threads(path=0)\ntcp_active_paths = 0\n→ reset_all_paths_disconnected()\n→ health_alive = 0

CT0 -> APP: callback(...,\nPOTR_EVENT_DISCONNECTED, NULL, 0)
@enduml
@startuml パス状態遷移.svg
caption パス状態遷移 (path_alive[k] / 各パス独立)

[*] -r-> 未接続 : サービス開始\n(path_alive[k] = 0)

未接続 --> 疎通中 : UDP: パスで初受信\nTCP: TCP 接続確立\n→ PATH_CONNECTED 発火\n(path_alive[k] = 1)

疎通中 --> 未接続 : ヘルスチェックタイムアウト\n→ PATH_DISCONNECTED 発火\n(path_alive[k] = 0)

疎通中 --> 未接続 : TCP: 接続切断\n→ PATH_DISCONNECTED 発火\n(path_alive[k] = 0)

未接続 --> [*] : potrCloseService()\n(PATH イベント発火なし)
疎通中 --> [*] : potrCloseService()\n(PATH イベント発火なし)

note right of 疎通中
  path_alive[k] = 1 の期間中
  セッション全体は CONNECTED または
  DISCONNECTED のいずれかの状態をとる
end note

note left of 未接続
  health_timeout_ms = 0 の場合
  タイムアウトによる PATH_DISCONNECTED は発生しない
  (FIN/REJECT による SESSION DISCONNECTED は発生する)
end note
@enduml
@startuml inline_umlgraph_18.svg
caption サービス開始 (送信者)

participant "アプリ" as APP
participant "potrOpenService" as OPEN
participant "受信スレッド" as RT
participant "送信スレッド" as ST
participant "ヘルスチェックスレッド" as HT

APP -> OPEN: potrOpenService(config, service_id,\nPOTR_ROLE_SENDER, NULL, &handle)

activate OPEN
OPEN -> OPEN: 設定ファイル解析
OPEN -> OPEN: セッション識別子生成\n(session_id + 現在時刻)
OPEN -> OPEN: UDP ソケット作成・bind\n(src_addr, src_port)
OPEN -> OPEN: 送信キュー初期化
OPEN -> OPEN: 送信ウィンドウ初期化

OPEN -> RT**: 受信スレッド起動
OPEN -> ST**: 送信スレッド起動
OPEN -> HT**: ヘルスチェックスレッド起動\n(health_interval_ms > 0 のみ)

OPEN --> APP: POTR_SUCCESS, *handle
deactivate OPEN

activate RT
activate ST
activate HT

note over RT: NACK/REJECT/FIN を\n待機するポーリングループ
note over ST: 送信キューを\n待機するポーリングループ
note over HT: 次回 PING 送信時刻を\n計算してスリープ
@enduml
@startuml inline_umlgraph_19.svg
caption サービス開始 (受信者)

participant "アプリ" as APP
participant "potrOpenService" as OPEN
participant "受信スレッド" as RT

APP -> OPEN: potrOpenService(config, service_id,\nPOTR_ROLE_RECEIVER, callback, &handle)

activate OPEN
OPEN -> OPEN: 設定ファイル解析
OPEN -> OPEN: UDP ソケット作成・bind\n(dst_addr, dst_port)
OPEN -> OPEN: マルチキャスト時:\nグループ参加
OPEN -> OPEN: 受信ウィンドウ初期化

OPEN -> RT**: 受信スレッド起動

OPEN --> APP: POTR_SUCCESS, *handle
deactivate OPEN

activate RT
note over RT: DATA/PING/FIN を\n待機するポーリングループ\nヘルスチェックタイムアウト監視
@enduml
@startuml inline_umlgraph_20.svg
caption 正常送受信 (ノンブロッキング)

participant "アプリ\n(送信側)" as SAPP
participant "送信キュー" as Q
participant "送信スレッド" as ST
participant "UDP" as UDP
participant "受信スレッド\n(受信者)" as RRT
participant "アプリ\n(受信側)" as RAPP

SAPP -> Q: potrSend(handle, POTR_PEER_NA, data, len, 0)\n→ エレメントを push して即座に返る
SAPP <-- Q: POTR_SUCCESS

note over Q, ST: 非同期に処理

Q -> ST: pop (エレメント取り出し)
ST -> ST: seq_num 付与\n送信ウィンドウへ登録\nパケット構築
ST -> UDP: sendto (全パス)

UDP -> RRT: recvfrom → DATA パケット受信
RRT -> RRT: service_id 照合\nセッション識別\n受信ウィンドウへ格納

alt 初回受信 (health_alive = 0)
  RRT -> RAPP: callback(service_id, POTR_PEER_NA, POTR_EVENT_CONNECTED, NULL, 0)
end

RRT -> RAPP: callback(service_id, POTR_PEER_NA, POTR_EVENT_DATA, data, len)
@enduml
@startuml inline_umlgraph_21.svg
caption 正常送受信 (ブロッキング)

participant "アプリ\n(送信側)" as SAPP
participant "送信キュー" as Q
participant "送信スレッド" as ST
participant "UDP" as UDP

SAPP -> Q: potrSend(handle, POTR_PEER_NA, data, len, POTR_SEND_BLOCKING)
activate SAPP

Q -> Q: (1) 既存キューが drained になるまで待機\n count == 0 && inflight == 0
note right of Q: 前の送信が完了するまで待つ

Q -> Q: (2) エレメントを push\n inflight をインクリメント

Q -> ST: pop → パケット構築 → sendto
ST -> UDP: sendto (全パス)
ST -> Q: complete() (inflight デクリメント)

Q -> Q: (3) drained 待機\n count == 0 && inflight == 0

Q --> SAPP: POTR_SUCCESS
deactivate SAPP

note over SAPP: 返却時点で UDP 送信済み
@enduml
@startuml inline_umlgraph_22.svg
caption フラグメント化と結合 (3 フラグメントの例)

participant "アプリ\n(送信側)" as SAPP
participant "送信キュー" as Q
participant "送信スレッド" as ST
participant "UDP" as UDP
participant "受信スレッド\n(受信者)" as RRT
participant "アプリ\n(受信側)" as RAPP

SAPP -> Q: potrSend(handle, POTR_PEER_NA, data, len=max_payload×3, 0)\nlen が max_payload を超えるためフラグメント化

Q -> Q: フラグ MORE_FRAG のエレメント push (1/3)
Q -> Q: フラグ MORE_FRAG のエレメント push (2/3)
Q -> Q: フラグなし (最終) のエレメント push (3/3)

ST -> UDP: DATA[seq=10, MORE_FRAG] (フラグメント 1)
ST -> UDP: DATA[seq=11, MORE_FRAG] (フラグメント 2)
ST -> UDP: DATA[seq=12]           (フラグメント 3、最終)

UDP -> RRT: DATA[seq=10, MORE_FRAG] 受信
RRT -> RRT: frag_buf に蓄積\n(MORE_FRAG: 続きを待つ)

UDP -> RRT: DATA[seq=11, MORE_FRAG] 受信
RRT -> RRT: frag_buf に追加

UDP -> RRT: DATA[seq=12] 受信
RRT -> RRT: frag_buf に追加\nフラグメント結合完了

RRT -> RAPP: callback(service_id, POTR_PEER_NA, POTR_EVENT_DATA, 結合済みデータ, 全体 len)
@enduml
@startuml inline_umlgraph_23.svg
caption NACK による再送

participant "送信スレッド" as ST
participant "UDP\n(送信側)" as SUDP
participant "UDP\n(受信側)" as RUDP
participant "受信スレッド" as RRT

ST -> SUDP: DATA[seq=10] 送信
ST -> SUDP: DATA[seq=11] 送信
ST -> SUDP: DATA[seq=12] 送信

SUDP -> RUDP: DATA[seq=10] 到着
SUDP -[#red]> RUDP: DATA[seq=11] ロスト
SUDP -> RUDP: DATA[seq=12] 到着

RUDP -> RRT: DATA[seq=10] 受信 → 処理 OK
RUDP -> RRT: DATA[seq=12] 受信

RRT -> RRT: seq=11 の欠番を検出

alt reorder_timeout_ms = 0 (即時・デフォルト)
  RRT -> SUDP: NACK[ack_num=11] 送信\n(全パスから送信者へユニキャスト)
else reorder_timeout_ms > 0 (リオーダー待機)
  RRT -> RRT: タイマー開始\n(deadline = now + reorder_timeout_ms)\n→ 待機中に seq=11 が届けば NACK 不要
  note over RRT, SUDP: タイムアウト後に check_reorder_timeout が NACK を送出\n(以下は NACK 送出後と同じフロー)
  RRT -> SUDP: NACK[ack_num=11] 送信 (タイムアウト後)
end

note over ST: NACK 受信スレッドが受け取る

SUDP -> ST: NACK[ack_num=11] 受信
ST -> ST: 送信ウィンドウから seq=11 を検索

ST -> SUDP: DATA[seq=11] 再送 (全パス)

SUDP -> RUDP: DATA[seq=11] 到着

RUDP -> RRT: DATA[seq=11] 受信\n受信ウィンドウから seq=11 取り出し
RRT -> RRT: seq=11, 12 の順で整列
@enduml
@startuml inline_umlgraph_24.svg
caption リオーダー - 通常モード: 待機中に届いた場合 (NACK なし)

participant "送信スレッド" as ST
participant "UDP\n(送信側)" as SUDP
participant "UDP\n(受信側)" as RUDP
participant "受信スレッド" as RRT
participant "アプリ\n(受信側)" as RAPP

ST -> SUDP: DATA[seq=10] 送信
ST -> SUDP: DATA[seq=11] 送信 (遅延)
ST -> SUDP: DATA[seq=12] 送信

SUDP -> RUDP: DATA[seq=10] 到着
RUDP -> RRT: DATA[seq=10] 受信 → 処理 OK
RRT -> RAPP: callback(service_id, POTR_PEER_NA, POTR_EVENT_DATA, data[10], len)

SUDP -> RUDP: DATA[seq=12] 到着 (seq=11 より先に到達)

RUDP -> RRT: DATA[seq=12] 受信
RRT -> RRT: seq=11 の欠番を検出\nタイマー開始 (reorder_timeout_ms)
note over RRT: deadline 内は NACK を保留

SUDP -> RUDP: DATA[seq=11] 到着 (追い越し解消)
RUDP -> RRT: DATA[seq=11] 受信\n欠番が埋まった → reorder_pending クリア
RRT -> RRT: seq=11, 12 の順で取り出し
RRT -> RAPP: callback(service_id, POTR_PEER_NA, POTR_EVENT_DATA, data[11], len)
RRT -> RAPP: callback(service_id, POTR_PEER_NA, POTR_EVENT_DATA, data[12], len)

note over RRT: NACK は送出されなかった
@enduml
@startuml inline_umlgraph_25.svg
caption リオーダー - RAW モード: 待機中に届いた場合 (DISCONNECTED なし)

participant "送信スレッド" as ST
participant "UDP\n(送信側)" as SUDP
participant "UDP\n(受信側)" as RUDP
participant "受信スレッド" as RRT
participant "アプリ\n(受信側)" as RAPP

ST -> SUDP: DATA[seq=10] 送信
ST -> SUDP: DATA[seq=11] 送信 (遅延)
ST -> SUDP: DATA[seq=12] 送信

SUDP -> RUDP: DATA[seq=10] 到着
RUDP -> RRT: DATA[seq=10] 受信 → 処理 OK
RRT -> RAPP: callback(service_id, POTR_PEER_NA, POTR_EVENT_DATA, data[10], len)

SUDP -> RUDP: DATA[seq=12] 到着 (seq=11 より先に到達)

RUDP -> RRT: DATA[seq=12] 受信
RRT -> RRT: seq=11 の欠番を検出 (RAW)\nタイマー開始 (reorder_timeout_ms)
note over RRT: deadline 内は DISCONNECTED を保留

SUDP -> RUDP: DATA[seq=11] 到着 (追い越し解消)
RUDP -> RRT: DATA[seq=11] 受信\n欠番が埋まった → reorder_pending クリア
RRT -> RRT: seq=11, 12 の順で取り出し
RRT -> RAPP: callback(service_id, POTR_PEER_NA, POTR_EVENT_DATA, data[11], len)
RRT -> RAPP: callback(service_id, POTR_PEER_NA, POTR_EVENT_DATA, data[12], len)

note over RRT: DISCONNECTED は発火しなかった
@enduml
@startuml inline_umlgraph_26.svg
caption REJECT による切断と復帰

participant "送信スレッド" as ST
participant "UDP" as UDP
participant "受信スレッド" as RRT
participant "アプリ\n(受信側)" as RAPP

ST -> UDP: DATA[seq=0..15] 送信 (ウィンドウサイズ 16)

note over ST: ウィンドウが満杯→ seq=0 を evict

ST -> UDP: DATA[seq=16] 送信 (seq=0 が evict される)

UDP -[#red]> RRT: DATA[seq=0] ロスト

RRT -> RRT: seq=0 の欠番を検出
RRT -> UDP: NACK[ack_num=0] 送信

UDP -> ST: NACK[ack_num=0] 受信
ST -> ST: 送信ウィンドウから seq=0 を検索\n→ evict 済みで見つからない

ST -> UDP: REJECT[ack_num=0] 送信

UDP -> RRT: REJECT[ack_num=0] 受信

RRT -> RAPP: callback(service_id, POTR_PEER_NA, POTR_EVENT_DISCONNECTED, NULL, 0)
RRT -> RRT: 欠落 seq=0 をスキップ\n次の通番 seq=1 から再開

note over RRT: 次のパケット到着で復帰

UDP -> RRT: DATA[seq=17] 受信
RRT -> RAPP: callback(service_id, POTR_PEER_NA, POTR_EVENT_CONNECTED, NULL, 0)
RRT -> RAPP: callback(service_id, POTR_PEER_NA, POTR_EVENT_DATA, ...)
@enduml
@startuml inline_umlgraph_27.svg
caption ヘルスチェック (正常疎通)

participant "ヘルスチェック\nスレッド" as HT
participant "送信スレッド" as ST
participant "UDP" as UDP
participant "受信スレッド\n(受信者)" as RRT

note over HT: health_interval_ms = 3000ms

HT -> HT: last_send_ms から 3000ms 経過を確認
note over HT: send_window.next_seq を読み取る\n(ウィンドウには登録しない)
HT -> UDP: PING[seq=N] 送信 (全パス)
HT -> HT: last_send_ms を更新

UDP -> RRT: PING[seq=N] 受信
RRT -> RRT: last_recv_tv を更新\n(タイムアウトカウンタリセット)
RRT -> RRT: notify_health_alive()\n(health_alive=0 のとき CONNECTED 発火)
RRT -> RRT: next_seq〜N-1 を全スキャン\n欠番を一括 NACK する
note over RRT: 欠番なければ返信なし

HT -> HT: 3000ms 待機

note over ST: データ送信が発生した場合

ST -> UDP: DATA[seq=M] 送信
ST -> HT: last_send_ms 更新

HT -> HT: last_send_ms から 3000ms 未経過を確認\n→ PING タイマーをリセット (送信見送り) \n→ 残り時間を再計算してスリープし直す
@enduml
@startuml ヘルスチェックタイムアウト.svg
caption ヘルスチェックタイムアウト

participant "送信者" as S
participant "UDP" as UDP
participant "受信スレッド\n(受信者)" as RRT
participant "アプリ\n(受信側)" as RAPP

S -> UDP: DATA 送信 (正常稼働中)
UDP -> RRT: DATA 受信 → last_recv_tv 更新
RRT -> RAPP: callback(service_id, POTR_PEER_NA, POTR_EVENT_DATA, ...)

note over S, UDP: ここでネットワーク断が発生

S -[#red]> UDP: PING 送信 (到達せず)
S -[#red]> UDP: PING 送信 (到達せず)

note over RRT: health_timeout_ms = 10000ms

RRT -> RRT: last_recv_tv から 10000ms 経過を検出

RRT -> RAPP: callback(service_id, POTR_PEER_NA, POTR_EVENT_DISCONNECTED, NULL, 0)
RRT -> RRT: health_alive = 0\npeer_session_known = 0\nrecv_window リセット

note over S, UDP: ネットワーク復旧

S -> UDP: DATA または PING 送信
UDP -> RRT: パケット受信
RRT -> RRT: セッション採用\nrecv_window を pkt.seq_num で初期化
RRT -> RRT: health_alive = 1

RRT -> RAPP: callback(service_id, POTR_PEER_NA, POTR_EVENT_CONNECTED, NULL, 0)
@enduml
@startuml inline_umlgraph_28.svg
caption 正常終了 (送信者側)

participant "アプリ\n(送信側)" as SAPP
participant "potrCloseService" as CLOSE
participant "送信スレッド" as ST
participant "ヘルスチェック\nスレッド" as HT
participant "受信スレッド" as RT
participant "UDP" as UDP
participant "受信スレッド\n(受信者)" as RRT
participant "アプリ\n(受信側)" as RAPP

SAPP -> CLOSE: potrCloseService(handle)
activate CLOSE

CLOSE -> ST: 停止シグナル
CLOSE -> HT: 停止シグナル

CLOSE -> UDP: FIN パケット送信\n(全パス)

UDP -> RRT: FIN 受信
RRT -> RAPP: callback(service_id, POTR_PEER_NA, POTR_EVENT_DISCONNECTED, NULL, 0)
note over RAPP: 送信者が明示的に終了\nDISCONNECTED が発火する
RRT -> RRT: peer_session_known = 0\nrecv_window リセット

CLOSE -> RT: 停止シグナル

CLOSE -> CLOSE: 各スレッドの終了を待機
CLOSE -> CLOSE: ソケット・ウィンドウ・\nキュー等のリソース解放

CLOSE --> SAPP: POTR_SUCCESS
deactivate CLOSE

note over SAPP: handle は以後使用不可
@enduml
@startuml inline_umlgraph_29.svg
caption 正常終了 (受信者側)

participant "アプリ\n(受信側)" as RAPP
participant "potrCloseService" as CLOSE
participant "受信スレッド" as RT

RAPP -> CLOSE: potrCloseService(handle)
activate CLOSE

CLOSE -> RT: 停止シグナル
CLOSE -> CLOSE: 受信スレッドの終了を待機

note over RAPP: 受信者側の potrCloseService() は\n送信者への通知なし\nDISCONNECTED も発火しない

CLOSE -> CLOSE: ソケット・ウィンドウ等の\nリソース解放

CLOSE --> RAPP: POTR_SUCCESS
deactivate CLOSE
@enduml
@startuml inline_umlgraph_30.svg
caption RAW モード - DATA ギャップ検出

participant "送信スレッド" as ST
participant "UDP\n(送信側)" as SUDP
participant "UDP\n(受信側)" as RUDP
participant "受信スレッド" as RRT
participant "アプリ\n(受信側)" as RAPP

ST -> SUDP: DATA[seq=10] 送信
ST -> SUDP: DATA[seq=11] 送信 (ロスト)
ST -> SUDP: DATA[seq=12] 送信

SUDP -> RUDP: DATA[seq=10] 到着
SUDP -[#red]> RUDP: DATA[seq=11] ロスト
SUDP -> RUDP: DATA[seq=12] 到着

RUDP -> RRT: DATA[seq=10] 受信 → 処理 OK
RUDP -> RRT: DATA[seq=12] 受信

RRT -> RRT: seq=11 の欠番を検出\n(RAW: NACK は送らない)

alt reorder_timeout_ms = 0 (即時・デフォルト)
  RRT -> RAPP: callback(service_id, POTR_PEER_NA, POTR_EVENT_DISCONNECTED, NULL, 0)
  RRT -> RRT: recv_window を seq=12 でリセット
  RRT -> RRT: DATA[seq=12] をウィンドウから取り出し
  RRT -> RAPP: callback(service_id, POTR_PEER_NA, POTR_EVENT_CONNECTED, NULL, 0)
  RRT -> RAPP: callback(service_id, POTR_PEER_NA, POTR_EVENT_DATA, data[seq=12], len)
else reorder_timeout_ms > 0 (リオーダー待機)
  RRT -> RRT: タイマー開始\n(deadline = now + reorder_timeout_ms)\n→ 待機中に seq=11 が届けば DISCONNECTED 不要
  note over RRT: タイムアウト後に check_reorder_timeout で\nDISCONNECTED 発火・ウィンドウリセット
end

note over RRT: seq=11 は配信されない\n(欠落として確定)
@enduml
@startuml inline_umlgraph_31.svg
caption RAW モード - PING ギャップ検出

participant "ヘルスチェック\nスレッド (送信者)" as HT
participant "UDP" as UDP
participant "受信スレッド" as RRT
participant "アプリ\n(受信側)" as RAPP

note over RRT: recv_window.next_seq = 10

HT -> UDP: DATA[seq=10..12] 送信 (ロスト)
UDP -[#red]> RRT: DATA[seq=10..12] ロスト

HT -> UDP: PING[seq=13]\n(next_seq=13 を通知)
UDP -> RRT: PING[seq=13] 受信

RRT -> RRT: pkt.seq_num(13) != next_seq(10)\n→ ギャップあり (window内)

alt reorder_timeout_ms = 0 (即時・デフォルト)
  RRT -> RAPP: callback(service_id, POTR_PEER_NA, POTR_EVENT_DISCONNECTED, NULL, 0)
  RRT -> RRT: recv_window を seq=13 でリセット
  RRT -> RAPP: callback(service_id, POTR_PEER_NA, POTR_EVENT_CONNECTED, NULL, 0)
else reorder_timeout_ms > 0 (リオーダー待機)
  RRT -> RRT: タイマー開始 (next_seq=10 の欠番に対して)\n→ 待機中に seq=10〜12 が届けば DISCONNECTED 不要
  note over RRT: タイムアウト後に check_reorder_timeout で\nDISCONNECTED 発火・ウィンドウリセット
end

note over RRT: seq=10〜12 は配信されない\nnext_seq = 13 に更新
@enduml
@startuml inline_umlgraph_32.svg
caption 接続状態遷移 (受信者側 health_alive フラグ)

[*] -r-> 未接続 : サービス開始直後\n(health_alive = 0)

未接続 ---> 疎通中 : 有効なパケットを受信\n→ POTR_EVENT_CONNECTED 発火\n(health_alive = 1)

疎通中 ---> 未接続 : タイムアウト検知\n (health_timeout_ms 経過) \n→ POTR_EVENT_DISCONNECTED 発火

疎通中 --> 未接続 : FIN 受信\n→ POTR_EVENT_DISCONNECTED 発火

疎通中 --> 未接続 : REJECT 受信 (通常モード)\n→ POTR_EVENT_DISCONNECTED 発火

疎通中 --> 未接続 : ギャップ検出 (RAW モード)\n→ POTR_EVENT_DISCONNECTED 発火

未接続 --> [*] : potrCloseService()\n (DISCONNECTED 発火なし)
疎通中 --> [*] : potrCloseService()\n (DISCONNECTED 発火なし)

note right of 疎通中
  health_timeout_ms = 0 の場合
  タイムアウトは発生しない
end note

note left of 疎通中
  reorder_timeout_ms > 0 の場合:
  ギャップ検出直後は NACK (通常モード) または
  DISCONNECTED (RAW モード) を保留。
  タイムアウト後または欠番充足で解消。
end note
@enduml
@startuml inline_umlgraph_33.svg
title unicast_bidir 1:1 双方向通信シーケンス

participant "Side A\n(POTR_ROLE_SENDER)" as A
participant "Side B\n(POTR_ROLE_RECEIVER)" as B

== サービス開始 ==

note over A: potrOpenService()\nbind(src_addr=A, src_port=PA)
note over B: potrOpenService()\nbind(src_addr=B, src_port=PB)

== A → B データ送信 ==

A -> B: DATA (session=S_A, seq=0)
note over B: peer_session_known = false\n→ セッション S_A 採用\nPOTR_EVENT_CONNECTED 発火\nPOTR_EVENT_DATA

A -> B: DATA (session=S_A, seq=1)
note over B: POTR_EVENT_DATA

== B → A データ送信 ==

B -> A: DATA (session=S_B, seq=0)
note over A: peer_session_known = false\n→ セッション S_B 採用\nPOTR_EVENT_CONNECTED 発火\nPOTR_EVENT_DATA

== 双方向データ ==

A -> B: DATA (session=S_A, seq=2)
B -> A: DATA (session=S_B, seq=1)
note over A,B: POTR_EVENT_DATA（各端で独立して発火）

== パケット欠落と再送 ==

A -> B: DATA (session=S_A, seq=3)
A -[#red]x B: DATA (session=S_A, seq=4)  [lost]
A -> B: DATA (session=S_A, seq=5)
note over B: seq=4 の欠落検出
B -> A: NACK (ack_num=4)
A -> B: DATA (session=S_A, seq=4)  [retransmit]
note over B: POTR_EVENT_DATA (seq=4, 5 の順)

== ヘルスチェック（対称） ==

A -> B: PING (session=S_A, ack_num=0, seq_num=N)
B -> A: PING (session=S_A, ack_num=N, seq_num=M)
note over A: PING 応答受信 → last_recv 更新

B -> A: PING (session=S_B, ack_num=0, seq_num=P)
A -> B: PING (session=S_B, ack_num=P, seq_num=Q)
note over B: PING 応答受信 → last_recv 更新
@enduml
@startuml inline_umlgraph_34.svg
title unicast_bidir N:1 サーバでの接続と送受信

participant "Client A" as CA
participant "Server\n(RECEIVER / N:1)" as S
participant "アプリ" as APP

note over S: potrOpenService()\nsrc_addr 省略 → N:1 モード\nbind(dst_addr, dst_port)

CA -> S: DATA (client session=C_A, seq=0)
S -> S: session triplet で未知ピア判定\npeer table に新規登録\npeer_id=1 を払い出し
S -> APP: callback(service_id, 1, POTR_EVENT_CONNECTED, NULL, 0)
S -> APP: callback(service_id, 1, POTR_EVENT_DATA, data, len)

APP -> S: potrSend(handle, 1, reply, len, 0)
S -> CA: DATA (server session=S_1, seq=0)

APP -> S: potrSend(handle, POTR_PEER_ALL, notice, len, 0)
S -> CA: DATA (peer_id=1 向け送信)
@enduml
@startuml inline_umlgraph_35.svg
title unicast_bidir N:1 サーバでの切断

participant "Client A" as CA
participant "Server" as S
participant "アプリ" as APP

CA -> S: FIN
S -> APP: callback(service_id, 1, POTR_EVENT_DISCONNECTED, NULL, 0)
S -> S: peer table から peer_id=1 を削除

APP -> S: potrDisconnectPeer(handle, 2)
S -> CA: FIN
S -> APP: callback(service_id, 2, POTR_EVENT_DISCONNECTED, NULL, 0)
S -> S: peer table から peer_id=2 を削除
@enduml
@startuml inline_umlgraph_36.svg
title unicast_bidir ヘルスタイムアウトによる切断検知

participant "Side A" as A
participant "Side B\n(停止)" as B

note over A,B: 通信中

A -> B: PING (ack_num=0, seq_num=N)
note over B: アプリケーションが停止\n（パケットが一切届かなくなる）
note over A: health_timeout_ms 経過\nlast_recv_tv_sec が更新されない\ncheck_health_timeout() → DISCONNECTED
note over A: POTR_EVENT_DISCONNECTED 発火
@enduml
@startuml inline_umlgraph_37.svg
caption TCP サービス開始 (SENDER)

participant "アプリ" as APP
participant "potrOpenService" as OPEN
participant "connect スレッド" as CT
participant "送信スレッド" as ST
participant "受信スレッド" as RT
participant "ヘルスチェックスレッド" as HT

APP -> OPEN: potrOpenService(config, service_id, POTR_ROLE_SENDER, cb, &handle)
activate OPEN
OPEN -> OPEN: 設定ファイル解析・セッション識別子生成
OPEN -> OPEN: 送信キュー / ウィンドウ初期化
OPEN -> OPEN: tcp_state_mutex / tcp_state_cv 初期化

OPEN -> CT**: connect スレッド起動
OPEN --> APP: POTR_SUCCESS, *handle
deactivate OPEN

activate CT
CT -> CT: connect(dst_addr, dst_port)\n（接続確立まで connect_timeout_ms 待機）
CT -> ST**: 送信スレッド起動
CT -> RT**: 受信スレッド起動
CT -> HT**: ヘルスチェックスレッド起動

note over CT: recv スレッドが切断を検知するまで待機
@enduml
@startuml inline_umlgraph_38.svg
caption TCP サービス開始 (RECEIVER)

participant "アプリ" as APP
participant "potrOpenService" as OPEN
participant "accept スレッド" as AT
participant "recv スレッド" as RT

APP -> OPEN: potrOpenService(config, service_id, POTR_ROLE_RECEIVER, callback, &handle)
activate OPEN
OPEN -> OPEN: 設定ファイル解析
OPEN -> OPEN: TCP listen ソケット作成\nbind(dst_addr, dst_port) → listen()

OPEN -> AT**: accept スレッド起動
OPEN --> APP: POTR_SUCCESS, *handle
deactivate OPEN

activate AT
note over AT: accept() 待機中
AT -> RT**: 接続確立後に recv スレッド起動

activate RT
note over RT: DATA / PING を待機するポーリングループ
@enduml
@startuml inline_umlgraph_39.svg
caption TCP 正常シーケンス

participant "SENDER\n(クライアント)" as S
participant "RECEIVER\n(サーバー)" as R

== サービス開始 ==

note over R: potrOpenService()\nbind() → listen()
note over S: potrOpenService()\nconnect() 開始
S -> R: TCP 3way handshake
note over S: 接続確立

== データ送信 ==

S -> R: DATA (seq_num=0, 最初のパケット)
note over R: peer_session_known = false\n→ セッション採用\nPOTR_EVENT_CONNECTED 発火

S -> R: DATA (seq_num=1)
S -> R: DATA (seq_num=2)
note over R: POTR_EVENT_DATA × 3

== ヘルスチェック ==

S -> R: PING (ack_num=0, seq_num=3, payload_len=0)
R -> S: PING (ack_num=3, seq_num=M, payload_len=0)
note over S: PING 応答受信 → alive 確認

== 正常終了 ==

note over S: potrCloseService()
S -> R: TCP FIN (close)
note over R: TCP 切断検知 → POTR_EVENT_DISCONNECTED 発火
@enduml
@startuml inline_umlgraph_40.svg
caption TCP SENDER 再起動・自動再接続

participant "SENDER" as S
participant "RECEIVER" as R

note over S,R: 通信中（セッション A）

== SENDER プロセス再起動 ==

S -[#red]-> R: TCP 接続断
note over R: TCP 切断検知\nPOTR_EVENT_DISCONNECTED 発火\npeer_session_known = false

note over S: potrOpenService()\n新セッション識別子を生成
S -> R: TCP 3way handshake（再接続）
S -> R: DATA (セッション B の最初のパケット)

note over R: セッション B を採用\nPOTR_EVENT_CONNECTED 発火
@enduml
@startuml inline_umlgraph_41.svg
caption TCP PING 応答タイムアウト

participant "SENDER" as S
participant "RECEIVER\n(応答なし)" as R

note over S,R: 通信中

S -> R: PING (ack_num=0, seq_num=N)
note over R: アプリケーション層がハング\n（TCP 接続は生きているが PING 応答が返らない）
note over S: tcp_health_timeout_ms 経過\nPING 応答（ack_num=N）未受信
note over S: POTR_EVENT_DISCONNECTED 発火\nTCP 接続を切断

note over S: reconnect_interval_ms 待機後\nconnect() 再試行
@enduml
@startuml inline_umlgraph_42.svg
caption TCP RECEIVER 側 PING タイムアウト

participant "SENDER\n(PING 送信停止)" as S
participant "RECEIVER" as R

note over S,R: 通信中

S -> R: PING (ack_num=0, seq_num=N)
R -> S: PING (ack_num=N, seq_num=M)
note over S: アプリケーション層がハング\n（TCP 接続は生きているが PING を送信できない）

note over R: tcp_health_timeout_ms 経過\nPING 要求（ack_num=0）未着信
note over R: POTR_EVENT_DISCONNECTED 発火\nTCP 接続を切断

note over R: accept スレッドへ戻り\n次の接続を待機
@enduml
@startuml inline_umlgraph_43.svg
caption TCP マルチパス接続確立（path 数 = 2）

participant "SENDER\n(connect スレッド #0)" as SC0
participant "SENDER\n(connect スレッド #1)" as SC1
participant "RECEIVER\n(accept スレッド #0)" as RA0
participant "RECEIVER\n(accept スレッド #1)" as RA1
participant "アプリ\n(受信側)" as RAPP

note over RA0: bind(dst_addr[0]) → listen()
note over RA1: bind(dst_addr[1]) → listen()

SC0 -> RA0: TCP 3way handshake (path 0)
RA0 -> RA0: tcp_conn_fd[0] = accept()\ntcp_active_paths 0→1
RA0 -> RAPP: callback(POTR_EVENT_CONNECTED)
note over RA0: recv スレッド #0 起動

SC1 -> RA1: TCP 3way handshake (path 1)
RA1 -> RA1: tcp_conn_fd[1] = accept()\ntcp_active_paths 1→2
note over RA1: POTR_EVENT_CONNECTED は発火しない
note over RA1: recv スレッド #1 起動

note over SC0,RA1: 2 path で接続確立。同一 seq_num のパケットが両 path から届く
@enduml
@startuml inline_umlgraph_44.svg
caption TCP 部分切断時の継続（path 0 切断）

participant "SENDER\n(connect スレッド #0)" as SC0
participant "SENDER\n(送信スレッド)" as ST
participant "RECEIVER\n(accept スレッド #0)" as RA0
participant "RECEIVER\n(recv スレッド #1)" as RR1
participant "アプリ\n(受信側)" as RAPP

note over SC0,RR1: path 0 / path 1 で通信中

note over SC0,RA0: path 0 の TCP 接続断（ネットワーク障害など）
RA0 -> RA0: tcp_active_paths 2→1
note over RA0: POTR_EVENT_DISCONNECTED は発火しない\n(残り 1 path が存在するため)
note over RA0: session_id / session_tv_* を保持

note over ST: path 0 をスキップして path 1 のみ送信継続

SC0 -> SC0: reconnect_interval_ms 待機後に\n再 connect()

SC0 -> RA0: TCP 3way handshake (path 0 再接続)
RA0 -> RA0: session triplet 照合 → 既存セッションに合流\ntcp_active_paths 1→2
note over RA0: POTR_EVENT_CONNECTED は再発火しない

note over SC0,RR1: 2 path で通信再開
@enduml
@startuml inline_umlgraph_45.svg
caption TCP 全 path 切断

participant "RECEIVER\n(accept スレッド #0)" as RA0
participant "RECEIVER\n(accept スレッド #1)" as RA1
participant "アプリ\n(受信側)" as RAPP

note over RA0,RA1: path 0 / path 1 で通信中

note over RA0: path 0 の TCP 接続断
RA0 -> RA0: tcp_active_paths 2→1
note over RA0: POTR_EVENT_DISCONNECTED は発火しない

note over RA1: path 1 の TCP 接続断
RA1 -> RA1: tcp_active_paths 1→0
RA1 -> RAPP: callback(POTR_EVENT_DISCONNECTED)
note over RA1: session_tv_* をリセット\n次の接続は新セッションとして扱う

note over RA0,RA1: 各 accept スレッドが次の接続を待機
@enduml
