meshsocket 0.1.0__tar.gz → 0.1.2__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshsocket
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: Lightweight WebSocket mesh networking — broadcast, direct routing, and request/response over a simple JSON wire protocol.
5
5
  Author: Carter Beaudoin
6
6
  License-Expression: MIT
@@ -23,6 +23,10 @@ Dynamic: license-file
23
23
 
24
24
  # MeshSocket (Python)
25
25
 
26
+ [![PyPI](https://img.shields.io/pypi/v/meshsocket.svg)](https://pypi.org/project/meshsocket/)
27
+ [![Downloads](https://static.pepy.tech/badge/meshsocket)](https://pepy.tech/project/meshsocket)
28
+ [![Python versions](https://img.shields.io/pypi/pyversions/meshsocket.svg)](https://pypi.org/project/meshsocket/)
29
+
26
30
  A lightweight WebSocket **mesh networking** library. Nodes connect to a central relay and
27
31
  communicate by **broadcast**, **direct routing**, and **request/response** — all over a
28
32
  simple JSON wire protocol.
@@ -1,5 +1,9 @@
1
1
  # MeshSocket (Python)
2
2
 
3
+ [![PyPI](https://img.shields.io/pypi/v/meshsocket.svg)](https://pypi.org/project/meshsocket/)
4
+ [![Downloads](https://static.pepy.tech/badge/meshsocket)](https://pepy.tech/project/meshsocket)
5
+ [![Python versions](https://img.shields.io/pypi/pyversions/meshsocket.svg)](https://pypi.org/project/meshsocket/)
6
+
3
7
  A lightweight WebSocket **mesh networking** library. Nodes connect to a central relay and
4
8
  communicate by **broadcast**, **direct routing**, and **request/response** — all over a
5
9
  simple JSON wire protocol.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshsocket
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: Lightweight WebSocket mesh networking — broadcast, direct routing, and request/response over a simple JSON wire protocol.
5
5
  Author: Carter Beaudoin
6
6
  License-Expression: MIT
@@ -23,6 +23,10 @@ Dynamic: license-file
23
23
 
24
24
  # MeshSocket (Python)
25
25
 
26
+ [![PyPI](https://img.shields.io/pypi/v/meshsocket.svg)](https://pypi.org/project/meshsocket/)
27
+ [![Downloads](https://static.pepy.tech/badge/meshsocket)](https://pepy.tech/project/meshsocket)
28
+ [![Python versions](https://img.shields.io/pypi/pyversions/meshsocket.svg)](https://pypi.org/project/meshsocket/)
29
+
26
30
  A lightweight WebSocket **mesh networking** library. Nodes connect to a central relay and
27
31
  communicate by **broadcast**, **direct routing**, and **request/response** — all over a
28
32
  simple JSON wire protocol.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "meshsocket"
7
- version = "0.1.0"
7
+ version = "0.1.2"
8
8
  description = "Lightweight WebSocket mesh networking — broadcast, direct routing, and request/response over a simple JSON wire protocol."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -110,6 +110,10 @@ class MeshSocket:
110
110
 
111
111
  # State
112
112
  self.is_running = False
113
+ # Set by stop(reason=...) when the client is being torn down because of a
114
+ # FAULT rather than an ordinary shutdown; surfaced when the supervisor exits.
115
+ self.stopped_reason: Optional[str] = None
116
+ self._maintain_task: Optional[asyncio.Task] = None
113
117
 
114
118
  # Lazy-initialised inside the event loop to avoid DeprecationWarning
115
119
  # in Python 3.10+ when constructing outside an async context.
@@ -202,12 +206,25 @@ class MeshSocket:
202
206
  raise ValueError("Cannot use start() without a URL.")
203
207
  self._start_time = time.time()
204
208
  self.is_running = True
205
- asyncio.create_task(self._maintain_connection())
209
+ self.stopped_reason = None
210
+ # Hold the reference: the loop keeps only a weak one, so an unreferenced
211
+ # task can be garbage-collected mid-flight and take the reconnect
212
+ # supervisor down silently.
213
+ self._maintain_task = asyncio.create_task(self._maintain_connection())
206
214
 
207
215
  async def wait_until_ready(self):
208
216
  await self.connected_event.wait()
209
217
 
210
- async def stop(self):
218
+ async def stop(self, reason: str = None):
219
+ """Shut the client down for good (the supervisor will NOT reconnect).
220
+
221
+ `reason` marks this as a FAULT rather than an intentional shutdown: it
222
+ is recorded on `stopped_reason` and logged at ERROR when the supervisor
223
+ exits. Callers giving up on a socket should always pass one — a caller
224
+ that reuses the plain shutdown path to mean "fatal error" produces a
225
+ dead client that never reconnects and never says why.
226
+ """
227
+ self.stopped_reason = reason
211
228
  self.is_running = False
212
229
  if self.connection:
213
230
  await self.connection.close()
@@ -227,6 +244,28 @@ class MeshSocket:
227
244
 
228
245
  # --- INTERNAL CORE ---
229
246
  async def _maintain_connection(self):
247
+ try:
248
+ await self._maintain_connection_inner()
249
+ finally:
250
+ # INVARIANT: this supervisor never dies quietly. While it is alive the
251
+ # client reconnects forever; once it exits the client is dead until
252
+ # something calls start() again, so every exit — intentional shutdown,
253
+ # fault, cancellation, or a bug — must leave a line in the log. A
254
+ # silent exit here once cost ~30 h of a "connected" client that was
255
+ # never coming back.
256
+ if self.stopped_reason:
257
+ logging.error(f"{LogColors.FAIL}{self.name} connection supervisor "
258
+ f"STOPPED: {self.stopped_reason} — this client will "
259
+ f"not reconnect until start() is called again"
260
+ f"{LogColors.ENDC}")
261
+ elif self.is_running:
262
+ logging.error(f"{LogColors.FAIL}{self.name} connection supervisor "
263
+ f"exited unexpectedly while still running — this "
264
+ f"client will NOT reconnect{LogColors.ENDC}")
265
+ else:
266
+ logging.info(f"{self.name} connection supervisor stopped (shutdown)")
267
+
268
+ async def _maintain_connection_inner(self):
230
269
  retry_delay = 2
231
270
  while self.is_running:
232
271
  listen_task = None
File without changes
File without changes
File without changes