Multi-directory aware nick tracking.
Implements the pattern from JoinMarket reference implementation where a nick
is only considered "gone" when ALL directory connections report it as disconnected.
This prevents premature nick leave detection when:
- A peer temporarily disconnects from one directory but remains on others
- Directory connections are flaky or experiencing network issues
- There's a race condition between directory updates
Reference: joinmarket-clientserver/src/jmdaemon/onionmc.py:1078-1103
Attributes
TDirectory = TypeVar('TDirectory')
module-attribute
Classes
NickTracker
Bases: Generic[TDirectory]
Tracks nick availability across multiple directory servers.
A nick is considered "active" if it appears on at least one directory.
A nick is only marked as "gone" when ALL directories report it as disconnected.
This implements the multi-directory awareness pattern from the reference
implementation (onionmc.py lines 1078-1103).
Source code in jmcore/src/jmcore/nick_tracker.py
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213 | class NickTracker(Generic[TDirectory]):
"""
Tracks nick availability across multiple directory servers.
A nick is considered "active" if it appears on at least one directory.
A nick is only marked as "gone" when ALL directories report it as disconnected.
This implements the multi-directory awareness pattern from the reference
implementation (onionmc.py lines 1078-1103).
"""
def __init__(self, on_nick_leave: Callable[[str], None] | None = None):
"""
Initialize the nick tracker.
Args:
on_nick_leave: Optional callback when a nick leaves ALL directories
"""
# active_nicks[nick] = {directory1: True, directory2: True, ...}
# True = nick is present on this directory, False = gone from this directory
self.active_nicks: dict[str, dict[TDirectory, bool]] = {}
self.on_nick_leave = on_nick_leave
def update_nick(self, nick: str, directory: TDirectory, is_present: bool) -> None:
"""
Update a nick's presence status on a specific directory.
Args:
nick: The nick to update
directory: The directory reporting the status
is_present: True if nick is present on this directory, False if gone
"""
if nick not in self.active_nicks:
self.active_nicks[nick] = {}
old_status = self.active_nicks[nick].get(directory)
self.active_nicks[nick][directory] = is_present
# Check if this update causes the nick to be completely gone
if not is_present and old_status is True:
# Nick just disappeared from this directory
# Check if it's still present on any other directory
if not self.is_nick_active(nick):
logger.info(
f"Nick {nick} has left all directories "
f"(directories: {list(self.active_nicks[nick].keys())})"
)
if self.on_nick_leave:
self.on_nick_leave(nick)
# Clean up the entry
del self.active_nicks[nick]
elif is_present and old_status is False:
logger.debug(
f"Nick {nick} returned to directory {directory} (was previously marked gone)"
)
def mark_nick_present(self, nick: str, directory: TDirectory) -> None:
"""
Mark a nick as present on a directory.
Args:
nick: The nick
directory: The directory where the nick is present
"""
self.update_nick(nick, directory, True)
def mark_nick_gone(self, nick: str, directory: TDirectory) -> None:
"""
Mark a nick as gone from a directory.
If this is the last directory where the nick was present,
triggers the on_nick_leave callback.
Args:
nick: The nick
directory: The directory where the nick left
"""
self.update_nick(nick, directory, False)
def is_nick_active(self, nick: str) -> bool:
"""
Check if a nick is active on at least one directory.
Args:
nick: The nick to check
Returns:
True if nick is present on at least one directory
"""
if nick not in self.active_nicks:
return False
return any(status for status in self.active_nicks[nick].values())
def get_active_directories_for_nick(self, nick: str) -> list[TDirectory]:
"""
Get list of directories where a nick is currently present.
Args:
nick: The nick to query
Returns:
List of directories where nick is active
"""
if nick not in self.active_nicks:
return []
return [
directory for directory, is_present in self.active_nicks[nick].items() if is_present
]
def get_all_active_nicks(self) -> set[str]:
"""
Get all nicks that are active on at least one directory.
Returns:
Set of active nicks
"""
return {nick for nick in self.active_nicks if self.is_nick_active(nick)}
def remove_directory(self, directory: TDirectory) -> list[str]:
"""
Remove a directory from tracking (when connection is lost).
Returns list of nicks that became completely gone after removing this directory.
Args:
directory: The directory to remove
Returns:
List of nicks that are no longer active after removing this directory
"""
gone_nicks = []
for nick in list(self.active_nicks.keys()):
if directory in self.active_nicks[nick]:
# Remove this directory from the nick's tracking
del self.active_nicks[nick][directory]
# Check if nick is now gone from all directories
if not self.active_nicks[nick]:
# No directories left for this nick
logger.info(f"Nick {nick} is gone (last directory {directory} was removed)")
gone_nicks.append(nick)
if self.on_nick_leave:
self.on_nick_leave(nick)
del self.active_nicks[nick]
elif not self.is_nick_active(nick):
# Still tracked on some directories but marked as gone on all
logger.info(
f"Nick {nick} is gone from all remaining directories "
f"after removing {directory}"
)
gone_nicks.append(nick)
if self.on_nick_leave:
self.on_nick_leave(nick)
del self.active_nicks[nick]
if gone_nicks:
logger.info(
f"After removing directory {directory}, {len(gone_nicks)} nicks are gone: "
f"{gone_nicks}"
)
return gone_nicks
def sync_with_peerlist(self, directory: TDirectory, active_nicks: set[str]) -> None:
"""
Synchronize nick tracking with a directory's peerlist.
This is called after fetching a peerlist from a directory to update
the nick tracking state. Nicks not in the peerlist are marked as gone
from that directory.
Args:
directory: The directory reporting the peerlist
active_nicks: Set of nicks currently active on this directory
"""
# First, mark all nicks in the peerlist as present
for nick in active_nicks:
self.mark_nick_present(nick, directory)
# Then, mark nicks we're tracking but not in this peerlist as gone from this directory
for nick in list(self.active_nicks.keys()):
if directory in self.active_nicks[nick] and nick not in active_nicks:
self.mark_nick_gone(nick, directory)
def __repr__(self) -> str:
"""String representation showing active nicks and their directories."""
return f"NickTracker(active_nicks={len(self.get_all_active_nicks())})"
|
Attributes
active_nicks: dict[str, dict[TDirectory, bool]] = {}
instance-attribute
on_nick_leave = on_nick_leave
instance-attribute
Functions
__init__(on_nick_leave: Callable[[str], None] | None = None)
Initialize the nick tracker.
Args:
on_nick_leave: Optional callback when a nick leaves ALL directories
Source code in jmcore/src/jmcore/nick_tracker.py
37
38
39
40
41
42
43
44
45
46
47 | def __init__(self, on_nick_leave: Callable[[str], None] | None = None):
"""
Initialize the nick tracker.
Args:
on_nick_leave: Optional callback when a nick leaves ALL directories
"""
# active_nicks[nick] = {directory1: True, directory2: True, ...}
# True = nick is present on this directory, False = gone from this directory
self.active_nicks: dict[str, dict[TDirectory, bool]] = {}
self.on_nick_leave = on_nick_leave
|
__repr__() -> str
String representation showing active nicks and their directories.
Source code in jmcore/src/jmcore/nick_tracker.py
| def __repr__(self) -> str:
"""String representation showing active nicks and their directories."""
return f"NickTracker(active_nicks={len(self.get_all_active_nicks())})"
|
get_active_directories_for_nick(nick: str) -> list[TDirectory]
Get list of directories where a nick is currently present.
Args:
nick: The nick to query
Returns:
List of directories where nick is active
Source code in jmcore/src/jmcore/nick_tracker.py
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133 | def get_active_directories_for_nick(self, nick: str) -> list[TDirectory]:
"""
Get list of directories where a nick is currently present.
Args:
nick: The nick to query
Returns:
List of directories where nick is active
"""
if nick not in self.active_nicks:
return []
return [
directory for directory, is_present in self.active_nicks[nick].items() if is_present
]
|
get_all_active_nicks() -> set[str]
Get all nicks that are active on at least one directory.
Returns:
Set of active nicks
Source code in jmcore/src/jmcore/nick_tracker.py
135
136
137
138
139
140
141
142 | def get_all_active_nicks(self) -> set[str]:
"""
Get all nicks that are active on at least one directory.
Returns:
Set of active nicks
"""
return {nick for nick in self.active_nicks if self.is_nick_active(nick)}
|
is_nick_active(nick: str) -> bool
Check if a nick is active on at least one directory.
Args:
nick: The nick to check
Returns:
True if nick is present on at least one directory
Source code in jmcore/src/jmcore/nick_tracker.py
105
106
107
108
109
110
111
112
113
114
115
116
117 | def is_nick_active(self, nick: str) -> bool:
"""
Check if a nick is active on at least one directory.
Args:
nick: The nick to check
Returns:
True if nick is present on at least one directory
"""
if nick not in self.active_nicks:
return False
return any(status for status in self.active_nicks[nick].values())
|
mark_nick_gone(nick: str, directory: TDirectory) -> None
Mark a nick as gone from a directory.
If this is the last directory where the nick was present,
triggers the on_nick_leave callback.
Args:
nick: The nick
directory: The directory where the nick left
Source code in jmcore/src/jmcore/nick_tracker.py
92
93
94
95
96
97
98
99
100
101
102
103 | def mark_nick_gone(self, nick: str, directory: TDirectory) -> None:
"""
Mark a nick as gone from a directory.
If this is the last directory where the nick was present,
triggers the on_nick_leave callback.
Args:
nick: The nick
directory: The directory where the nick left
"""
self.update_nick(nick, directory, False)
|
mark_nick_present(nick: str, directory: TDirectory) -> None
Mark a nick as present on a directory.
Args:
nick: The nick
directory: The directory where the nick is present
Source code in jmcore/src/jmcore/nick_tracker.py
82
83
84
85
86
87
88
89
90 | def mark_nick_present(self, nick: str, directory: TDirectory) -> None:
"""
Mark a nick as present on a directory.
Args:
nick: The nick
directory: The directory where the nick is present
"""
self.update_nick(nick, directory, True)
|
remove_directory(directory: TDirectory) -> list[str]
Remove a directory from tracking (when connection is lost).
Returns list of nicks that became completely gone after removing this directory.
Args:
directory: The directory to remove
Returns:
List of nicks that are no longer active after removing this directory
Source code in jmcore/src/jmcore/nick_tracker.py
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188 | def remove_directory(self, directory: TDirectory) -> list[str]:
"""
Remove a directory from tracking (when connection is lost).
Returns list of nicks that became completely gone after removing this directory.
Args:
directory: The directory to remove
Returns:
List of nicks that are no longer active after removing this directory
"""
gone_nicks = []
for nick in list(self.active_nicks.keys()):
if directory in self.active_nicks[nick]:
# Remove this directory from the nick's tracking
del self.active_nicks[nick][directory]
# Check if nick is now gone from all directories
if not self.active_nicks[nick]:
# No directories left for this nick
logger.info(f"Nick {nick} is gone (last directory {directory} was removed)")
gone_nicks.append(nick)
if self.on_nick_leave:
self.on_nick_leave(nick)
del self.active_nicks[nick]
elif not self.is_nick_active(nick):
# Still tracked on some directories but marked as gone on all
logger.info(
f"Nick {nick} is gone from all remaining directories "
f"after removing {directory}"
)
gone_nicks.append(nick)
if self.on_nick_leave:
self.on_nick_leave(nick)
del self.active_nicks[nick]
if gone_nicks:
logger.info(
f"After removing directory {directory}, {len(gone_nicks)} nicks are gone: "
f"{gone_nicks}"
)
return gone_nicks
|
sync_with_peerlist(directory: TDirectory, active_nicks: set[str]) -> None
Synchronize nick tracking with a directory's peerlist.
This is called after fetching a peerlist from a directory to update
the nick tracking state. Nicks not in the peerlist are marked as gone
from that directory.
Args:
directory: The directory reporting the peerlist
active_nicks: Set of nicks currently active on this directory
Source code in jmcore/src/jmcore/nick_tracker.py
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209 | def sync_with_peerlist(self, directory: TDirectory, active_nicks: set[str]) -> None:
"""
Synchronize nick tracking with a directory's peerlist.
This is called after fetching a peerlist from a directory to update
the nick tracking state. Nicks not in the peerlist are marked as gone
from that directory.
Args:
directory: The directory reporting the peerlist
active_nicks: Set of nicks currently active on this directory
"""
# First, mark all nicks in the peerlist as present
for nick in active_nicks:
self.mark_nick_present(nick, directory)
# Then, mark nicks we're tracking but not in this peerlist as gone from this directory
for nick in list(self.active_nicks.keys()):
if directory in self.active_nicks[nick] and nick not in active_nicks:
self.mark_nick_gone(nick, directory)
|
update_nick(nick: str, directory: TDirectory, is_present: bool) -> None
Update a nick's presence status on a specific directory.
Args:
nick: The nick to update
directory: The directory reporting the status
is_present: True if nick is present on this directory, False if gone
Source code in jmcore/src/jmcore/nick_tracker.py
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80 | def update_nick(self, nick: str, directory: TDirectory, is_present: bool) -> None:
"""
Update a nick's presence status on a specific directory.
Args:
nick: The nick to update
directory: The directory reporting the status
is_present: True if nick is present on this directory, False if gone
"""
if nick not in self.active_nicks:
self.active_nicks[nick] = {}
old_status = self.active_nicks[nick].get(directory)
self.active_nicks[nick][directory] = is_present
# Check if this update causes the nick to be completely gone
if not is_present and old_status is True:
# Nick just disappeared from this directory
# Check if it's still present on any other directory
if not self.is_nick_active(nick):
logger.info(
f"Nick {nick} has left all directories "
f"(directories: {list(self.active_nicks[nick].keys())})"
)
if self.on_nick_leave:
self.on_nick_leave(nick)
# Clean up the entry
del self.active_nicks[nick]
elif is_present and old_status is False:
logger.debug(
f"Nick {nick} returned to directory {directory} (was previously marked gone)"
)
|