A Python port of the Invisible Internet Project (I2P)
1"""SAM primary session -- multi-session management on single I2P tunnel.
2
3Ported from net.i2p.sam.SAMv3Handler PRIMARY session support.
4"""
5
6from __future__ import annotations
7
8import logging
9from typing import TYPE_CHECKING
10
11if TYPE_CHECKING:
12 from i2p_sam.sessions_db import SessionRecord
13
14logger = logging.getLogger(__name__)
15
16
17class PrimarySession:
18 """Manages multiple subsessions (STREAM, DATAGRAM, RAW) on one I2P session.
19
20 A PRIMARY session allows a single I2P destination to be used for
21 multiple communication styles simultaneously. Subsessions are keyed
22 by their FROM_PORT to allow multiplexing.
23 """
24
25 def __init__(self, nickname: str, destination_b64: str) -> None:
26 self._nickname = nickname
27 self._destination_b64 = destination_b64
28 self._subsessions: dict[str, "SessionRecord"] = {}
29
30 async def add_subsession(self, sub_key: str, style: str,
31 handler: object) -> "SessionRecord":
32 """Add a subsession under this primary session.
33
34 Args:
35 sub_key: Subsession identifier (typically FROM_PORT value).
36 style: Communication style (STREAM, DATAGRAM, RAW).
37 handler: The SAMHandler managing this subsession.
38
39 Returns:
40 The created SessionRecord.
41
42 Raises:
43 ValueError: If sub_key already exists.
44 """
45 from i2p_sam.sessions_db import SessionRecord
46 from i2p_sam.utils import generate_transient_destination
47
48 if sub_key in self._subsessions:
49 raise ValueError(f"Subsession {sub_key} already exists")
50
51 raw, b64 = generate_transient_destination()
52 record = SessionRecord(
53 nickname=f"{self._nickname}:{sub_key}",
54 style=style,
55 destination=raw,
56 destination_b64=b64,
57 handler=handler, # type: ignore[arg-type]
58 )
59 self._subsessions[sub_key] = record
60 logger.info("Added subsession %s (style=%s) to primary %s",
61 sub_key, style, self._nickname)
62 return record
63
64 async def remove_subsession(self, sub_key: str) -> bool:
65 """Remove a subsession.
66
67 Args:
68 sub_key: Subsession identifier to remove.
69
70 Returns:
71 True if removed, False if not found.
72 """
73 if sub_key in self._subsessions:
74 del self._subsessions[sub_key]
75 logger.info("Removed subsession %s from primary %s",
76 sub_key, self._nickname)
77 return True
78 return False
79
80 def get_subsession(self, sub_key: str) -> "SessionRecord | None":
81 """Get a subsession by key.
82
83 Args:
84 sub_key: Subsession identifier.
85
86 Returns:
87 The SessionRecord, or None if not found.
88 """
89 return self._subsessions.get(sub_key)
90
91 @property
92 def subsession_count(self) -> int:
93 """Number of active subsessions."""
94 return len(self._subsessions)