deft-subspace-kernel 0.1.0a1__py3-none-any.whl

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.
@@ -0,0 +1,815 @@
1
+ """One side of a JSON-RPC 2.0 connection.
2
+
3
+ Every outbound request has exactly one owner, the peer, and ends exactly once,
4
+ in `_settle()`: with the answer, or with a `LocalError` saying why we stopped
5
+ waiting (`aborted`, `timeout`, `closed`). Late answers are reported, never
6
+ delivered.
7
+
8
+ Inbound requests run concurrently, so a handler that waits on a call back to
9
+ the other side never stops other messages from being read. Notification
10
+ handlers start in arrival order (a synchronous handler runs to completion
11
+ before the next message is read).
12
+
13
+ When the other side ends its stream, outbound requests fail with `closed` at
14
+ once, but inbound requests already running may still answer (see
15
+ `drain_timeout_ms`), so `printf '{"id":1,"method":"x"}\n' | agent` is answered.
16
+
17
+ Handlers and middleware may be plain functions or coroutine functions.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import asyncio
23
+ import inspect
24
+ import math
25
+ import sys
26
+ from collections import deque
27
+ from collections.abc import Awaitable, Callable
28
+ from dataclasses import dataclass, field
29
+ from typing import Any, Literal
30
+
31
+ from .abort import AbortController, AbortSignal
32
+ from .channel import Channel, ChannelClosed, EndOfChannel
33
+ from .clock import Clock, system_clock
34
+ from .errors import ErrorCode, LocalError, RpcError
35
+ from .framing import EncodeError
36
+
37
+ JsonRpcId = str | int | float | None
38
+ """JSON-RPC ids are opaque. `0` and `""` are as valid as any other id, and
39
+ JSON-RPC 2.0 also allows `null` (discouraged) for requests we receive.
40
+ Requests we send always get an int."""
41
+
42
+
43
+ @dataclass
44
+ class InboundContext:
45
+ method: str
46
+ #: Fires when the other side cancels this request, or the connection closes.
47
+ signal: AbortSignal
48
+ #: The peer, for calling back to the other side while handling.
49
+ peer: RpcPeer
50
+ #: The request's id. `None` for notifications (see `is_request`); a request's id may also be None.
51
+ id: JsonRpcId = None
52
+ is_request: bool = False
53
+
54
+
55
+ RequestHandler = Callable[[Any, InboundContext], Any]
56
+ NotificationHandler = Callable[[Any, InboundContext], Any]
57
+
58
+
59
+ @dataclass
60
+ class Call:
61
+ """A message as middleware sees it."""
62
+
63
+ kind: Literal["request", "notification"]
64
+ method: str
65
+ params: Any
66
+ id: JsonRpcId = None
67
+
68
+
69
+ InboundMiddleware = Callable[[Call, Callable[[], Awaitable[Any]], InboundContext], Awaitable[Any]]
70
+ """Wraps inbound handling. Await `next()` to run the handler (and inner middleware)."""
71
+ OutboundMiddleware = Callable[[Call, Callable[[Any], Awaitable[Any]]], Awaitable[Any]]
72
+ """Wraps outbound calls. `await next(params)` sends; it returns the result (requests) or None (notifications)."""
73
+
74
+ PeerState = Literal["idle", "open", "draining", "closed"]
75
+ """`idle`: not started. `open`: reading and writing. `draining`: the other side
76
+ ended its stream; running handlers finish and answer, then the peer closes.
77
+ `closed`: nothing more is read or written."""
78
+
79
+ DEFAULT_DRAIN_MS = 1000
80
+
81
+ DiagnosticKind = Literal["invalid-message", "late-response", "handler-failed", "duplicate-request-id", "send-failed"]
82
+
83
+
84
+ @dataclass(frozen=True)
85
+ class PeerDiagnostic:
86
+ kind: DiagnosticKind
87
+ message: str
88
+ detail: Any = None
89
+
90
+
91
+ @dataclass(frozen=True)
92
+ class CloseInfo:
93
+ """`local`: close() was called. `remote-ended`: the other side's stream ended (also when
94
+ close() is called while the peer drains after that: the other side ended it first).
95
+ `error`: the stream failed."""
96
+
97
+ reason: Literal["local", "remote-ended", "error"]
98
+ error: BaseException | None = None
99
+
100
+
101
+ @dataclass(frozen=True)
102
+ class CancelOutbound:
103
+ method: str
104
+ params: Callable[[JsonRpcId], Any]
105
+
106
+
107
+ @dataclass(frozen=True)
108
+ class CancelInbound:
109
+ method: str
110
+ id: Callable[[Any], Any]
111
+ """The id of the request to cancel, or `NO_ID`."""
112
+
113
+
114
+ class _NoId:
115
+ def __repr__(self) -> str:
116
+ return "NO_ID"
117
+
118
+
119
+ NO_ID: Any = _NoId()
120
+
121
+
122
+ def _cancel_request_id(params: Any) -> Any:
123
+ request_id = params.get("requestId", NO_ID) if isinstance(params, dict) else NO_ID
124
+ return request_id if request_id is None or isinstance(request_id, (str, int, float)) and not isinstance(request_id, bool) else NO_ID
125
+
126
+
127
+ cancel_request: dict[str, Any] = {
128
+ "cancel_outbound": CancelOutbound("$/cancel_request", lambda request_id: {"requestId": request_id}),
129
+ "cancel_inbound": CancelInbound("$/cancel_request", _cancel_request_id),
130
+ }
131
+ """Request cancellation as ACP and LSP do it: `$/cancel_request` with `{requestId}`, both ways.
132
+
133
+ RpcPeer(channel, **cancel_request)
134
+ """
135
+
136
+
137
+ def _is_id(value: Any) -> bool:
138
+ if value is None or isinstance(value, str):
139
+ return True
140
+ if isinstance(value, bool):
141
+ return False
142
+ if isinstance(value, int):
143
+ return True
144
+ return isinstance(value, float) and math.isfinite(value)
145
+
146
+
147
+ async def _resolve(value: Any) -> Any:
148
+ return await value if inspect.isawaitable(value) else value
149
+
150
+
151
+ @dataclass
152
+ class _Pending:
153
+ method: str
154
+ future: asyncio.Future[Any]
155
+ cleanups: list[Callable[[], None]] = field(default_factory=list)
156
+
157
+
158
+ #: Python 3.12+ can start a task eagerly: it runs at once, in its own task, until it first
159
+ #: waits, so a write or a handler that does not wait costs no trip through the event loop.
160
+ #:
161
+ #: Not when the loop has a task factory (`loop.set_task_factory`): an eager task would
162
+ #: bypass it, so then tasks are created as usual. And a handler that raises SystemExit or
163
+ #: KeyboardInterrupt before its first `await` raises it inside the read loop, which ends
164
+ #: with it; asyncio lets it out of the event loop then, as it does from a lazy task.
165
+ _EAGER = sys.version_info >= (3, 12)
166
+
167
+
168
+ def _new_task(coroutine: Any, name: str) -> asyncio.Task[Any]:
169
+ loop = asyncio.get_running_loop()
170
+ if _EAGER and loop.get_task_factory() is None:
171
+ return asyncio.Task(coroutine, loop=loop, name=name, eager_start=True) # type: ignore[call-arg]
172
+ return loop.create_task(coroutine, name=name)
173
+
174
+
175
+ def _being_cancelled() -> bool:
176
+ """Whether the running task has been asked to cancel (and has not uncancelled)."""
177
+ task = asyncio.current_task()
178
+ return task is not None and task.cancelling() > 0
179
+
180
+
181
+ def _id_key(value: Any) -> Any:
182
+ # 1 and 1.0 are the same JSON number; "1" is not.
183
+ if isinstance(value, float) and value.is_integer():
184
+ return ("n", int(value))
185
+ if isinstance(value, (int, float)) and not isinstance(value, bool):
186
+ return ("n", value)
187
+ return ("s", value)
188
+
189
+
190
+ class RpcPeer:
191
+ def __init__(
192
+ self,
193
+ channel: Channel[Any],
194
+ *,
195
+ name: str = "peer",
196
+ clock: Clock | None = None,
197
+ timeout_ms: float | None = None,
198
+ cancel_outbound: CancelOutbound | None = None,
199
+ cancel_inbound: CancelInbound | None = None,
200
+ inbound: list[InboundMiddleware] | None = None,
201
+ outbound: list[OutboundMiddleware] | None = None,
202
+ tap: Callable[[Literal["in", "out"], Any], None] | None = None,
203
+ on_diagnostic: Callable[[PeerDiagnostic], None] | None = None,
204
+ drain_timeout_ms: float = DEFAULT_DRAIN_MS,
205
+ ) -> None:
206
+ """Options:
207
+
208
+ - `timeout_ms`: default timeout for outbound requests. Default: none.
209
+ - `cancel_outbound`: how to tell the other side we stopped waiting (abort or timeout),
210
+ e.g. `$/cancel_request` (see `cancel_request`). Default: nothing is sent.
211
+ - `cancel_inbound`: which inbound notification cancels one of our handlers, and how to find its id.
212
+ - `tap`: sees every message in both directions, e.g. for tracing. Must not raise.
213
+ - `drain_timeout_ms`: when the other side ends its stream, how long request handlers
214
+ still running may take to answer before they are aborted. 0 aborts them at once;
215
+ infinity waits for them. Default 1000.
216
+ """
217
+ self.name = name
218
+ self._channel = channel
219
+ self._clock = clock or system_clock
220
+ self._timeout_ms = timeout_ms
221
+ self._cancel_outbound = cancel_outbound
222
+ self._cancel_inbound = cancel_inbound
223
+ self._inbound_chain = list(inbound or [])
224
+ self._outbound_chain = list(outbound or [])
225
+ self._tap_fn = tap
226
+ self._on_diagnostic = on_diagnostic
227
+ self._drain_ms = drain_timeout_ms
228
+ self._request_handlers: dict[str, RequestHandler] = {}
229
+ self._notification_handlers: dict[str, NotificationHandler] = {}
230
+ self._pending: dict[Any, _Pending] = {}
231
+ self._inbound: dict[Any, AbortController] = {}
232
+ self._connection = AbortController()
233
+ self._next_id = 0
234
+ self._state: PeerState = "idle"
235
+ self._running = 0 # inbound request handlers still running
236
+ self._drain_timer: Any = None
237
+ self._close_info: CloseInfo | None = None
238
+ self._closed_future: asyncio.Future[CloseInfo] | None = None
239
+ self._close_listeners: list[Callable[[CloseInfo], None]] = []
240
+ self._read_task: asyncio.Task[None] | None = None
241
+ self._tasks: set[asyncio.Task[Any]] = set()
242
+ # Writes go out in the order they were made, one at a time: a writer task runs while
243
+ # there is something to write (and closes the channel once the peer has closed).
244
+ self._outbox: deque[tuple[dict[str, Any], asyncio.Future[None]]] = deque()
245
+ self._writer_task: asyncio.Task[None] | None = None
246
+ self._writing = False
247
+
248
+ # ---- state ---------------------------------------------------------------
249
+
250
+ @property
251
+ def is_closed(self) -> bool:
252
+ """True once the connection has closed for any reason."""
253
+ return self._state == "closed"
254
+
255
+ @property
256
+ def state(self) -> PeerState:
257
+ return self._state
258
+
259
+ @property
260
+ def close_info(self) -> CloseInfo | None:
261
+ return self._close_info
262
+
263
+ @property
264
+ def pending_count(self) -> int:
265
+ """Outbound requests still waiting for an answer."""
266
+ return len(self._pending)
267
+
268
+ @property
269
+ def signal(self) -> AbortSignal:
270
+ """Aborted when the connection closes."""
271
+ return self._connection.signal
272
+
273
+ @property
274
+ def closed(self) -> asyncio.Future[CloseInfo]:
275
+ """Resolves with the CloseInfo once the connection has closed."""
276
+ if self._closed_future is None:
277
+ self._closed_future = asyncio.get_running_loop().create_future()
278
+ if self._close_info is not None:
279
+ self._closed_future.set_result(self._close_info)
280
+ return self._closed_future
281
+
282
+ def on_close(self, listener: Callable[[CloseInfo], None]) -> None:
283
+ """Call `listener(info)` when the connection closes (at once, if it has)."""
284
+ if self._close_info is not None:
285
+ listener(self._close_info)
286
+ else:
287
+ self._close_listeners.append(listener)
288
+
289
+ # ---- handlers ------------------------------------------------------------
290
+
291
+ def on_request(self, method: str, handler: RequestHandler) -> RpcPeer:
292
+ """Handle a request. `'*'` handles every method without its own handler (see `context.method`)."""
293
+ self._request_handlers[method] = handler
294
+ return self
295
+
296
+ def on_notification(self, method: str, handler: NotificationHandler) -> RpcPeer:
297
+ """Handle a notification. `'*'` handles every method without its own handler."""
298
+ self._notification_handlers[method] = handler
299
+ return self
300
+
301
+ def start(self) -> RpcPeer:
302
+ """Start reading. Register handlers first. Needs a running event loop."""
303
+ if self._state != "idle":
304
+ return self
305
+ self._state = "open"
306
+ self._read_task = asyncio.get_running_loop().create_task(self._read_loop(), name=f"{self.name}:read")
307
+ return self
308
+
309
+ # ---- outbound ------------------------------------------------------------
310
+
311
+ async def request(
312
+ self,
313
+ method: str,
314
+ params: Any = None,
315
+ *,
316
+ signal: AbortSignal | None = None,
317
+ timeout_ms: float | None = None,
318
+ ) -> Any:
319
+ """Send a request and wait for its result.
320
+
321
+ Stops waiting (with a LocalError) when `signal` fires (`aborted`), after `timeout_ms`
322
+ (`timeout`; overrides the peer's default; 0 or infinity means no limit), or when the
323
+ connection closes (`closed`). Cancelling the awaiting task also gives up on the request
324
+ (and tells the other side), then re-raises CancelledError.
325
+ """
326
+ call = Call("request", method, params)
327
+ return await self._run_outbound(call, lambda final: self._send_request(method, final, signal, timeout_ms))
328
+
329
+ async def notify(self, method: str, params: Any = None) -> None:
330
+ call = Call("notification", method, params)
331
+
332
+ async def send(final: Any) -> None:
333
+ message = self._message(method=method)
334
+ if final is not None:
335
+ message["params"] = final
336
+ await self._write(message)
337
+
338
+ await self._run_outbound(call, send)
339
+
340
+ async def send_raw(self, message: dict[str, Any]) -> None:
341
+ """Write a message exactly as given (`jsonrpc: '2.0'` is added if missing), outside request tracking.
342
+
343
+ For proxies and adapters, e.g. answering an id this peer did not receive.
344
+ """
345
+ await self._write(message if "jsonrpc" in message else {"jsonrpc": "2.0", **message})
346
+
347
+ async def close(self) -> CloseInfo:
348
+ """Close the connection. Pending requests fail with `closed`; running handlers are aborted.
349
+
350
+ While draining, this ends the drain early and the reason stays `remote-ended`.
351
+ """
352
+ self._finish(CloseInfo("remote-ended" if self._state == "draining" else "local"))
353
+ task = self._read_task
354
+ if task is not None and not task.done() and task is not asyncio.current_task():
355
+ task.cancel()
356
+ try:
357
+ await task
358
+ except asyncio.CancelledError:
359
+ if _being_cancelled():
360
+ raise # our caller's cancellation, not the reader's
361
+ except BaseException: # noqa: BLE001
362
+ pass
363
+ writer = self._writer_task
364
+ if writer is not None and not writer.done() and writer is not asyncio.current_task():
365
+ try:
366
+ await asyncio.wait_for(asyncio.shield(writer), self._close_wait_s)
367
+ except TimeoutError:
368
+ # The channel is not taking writes (a full pipe nobody reads): drop them.
369
+ writer.cancel()
370
+ try:
371
+ await writer
372
+ except asyncio.CancelledError:
373
+ if _being_cancelled():
374
+ raise # our caller's cancellation, not the writer's
375
+ except BaseException: # noqa: BLE001
376
+ pass
377
+ await self._abort_channel()
378
+ except Exception: # noqa: BLE001
379
+ pass
380
+ return self._close_info # type: ignore[return-value]
381
+
382
+ #: How long close() waits for queued writes before it drops them.
383
+ _close_wait_s = 1.0
384
+
385
+ async def _abort_channel(self) -> None:
386
+ while self._outbox:
387
+ _message, future = self._outbox.popleft()
388
+ if not future.done():
389
+ future.set_exception(LocalError("closed", f"{self.name}: connection closed"))
390
+ abort = getattr(self._channel, "abort", None)
391
+ try:
392
+ await _resolve(abort() if abort is not None else self._channel.close())
393
+ except Exception: # noqa: BLE001
394
+ pass
395
+
396
+ def _run_outbound(self, call: Call, send: Callable[[Any], Awaitable[Any]]) -> Awaitable[Any]:
397
+ chain = self._outbound_chain
398
+ if not chain:
399
+ return send(call.params)
400
+
401
+ def dispatch(index: int, params: Any) -> Awaitable[Any]:
402
+ if index >= len(chain):
403
+ return send(params)
404
+ middleware = chain[index]
405
+ return middleware(Call(call.kind, call.method, params, call.id), lambda nxt: dispatch(index + 1, nxt))
406
+
407
+ return dispatch(0, call.params)
408
+
409
+ async def _send_request(self, method: str, params: Any, signal: AbortSignal | None, timeout_ms: float | None) -> Any:
410
+ # While draining nothing more is read, so no answer could arrive.
411
+ if self._state in ("draining", "closed"):
412
+ raise LocalError("closed", f"{self.name}: connection closed before {method} was sent", method=method)
413
+ if signal is not None and signal.aborted:
414
+ raise LocalError("aborted", f"{self.name}: {method} aborted before it was sent", method=method, cause=signal.reason)
415
+
416
+ request_id = self._next_id
417
+ self._next_id += 1
418
+ future: asyncio.Future[Any] = asyncio.get_running_loop().create_future()
419
+ pending = _Pending(method, future)
420
+ key = _id_key(request_id)
421
+ self._pending[key] = pending
422
+
423
+ limit = self._timeout_ms if timeout_ms is None else timeout_ms
424
+ if limit is not None and limit > 0 and math.isfinite(limit):
425
+
426
+ def on_timeout() -> None:
427
+ self._give_up(request_id, LocalError("timeout", f"{self.name}: {method} timed out after {limit:g} ms", method=method))
428
+
429
+ handle = self._clock.set_timeout(on_timeout, limit)
430
+ pending.cleanups.append(lambda: self._clock.clear_timeout(handle))
431
+
432
+ if signal is not None:
433
+ sig = signal
434
+
435
+ def on_abort(reason: Any) -> None:
436
+ self._give_up(request_id, LocalError("aborted", f"{self.name}: {method} aborted", method=method, cause=reason))
437
+
438
+ pending.cleanups.append(sig.add_listener(on_abort))
439
+
440
+ message = self._message(id=request_id, method=method)
441
+ if params is not None:
442
+ message["params"] = params
443
+
444
+ def on_written(written: asyncio.Future[None]) -> None:
445
+ error = written.exception() if not written.cancelled() else None
446
+ if error is not None:
447
+ self._settle(key, error=LocalError("send-failed", f"{self.name}: could not send {method}", method=method, cause=error))
448
+
449
+ self._write(message).add_done_callback(on_written)
450
+ try:
451
+ return await future
452
+ except asyncio.CancelledError:
453
+ if key in self._pending:
454
+ self._give_up(request_id, LocalError("aborted", f"{self.name}: {method} aborted", method=method))
455
+ raise
456
+
457
+ def _give_up(self, request_id: JsonRpcId, error: LocalError) -> None:
458
+ """We stopped waiting: settle, then tell the other side if configured."""
459
+ if not self._settle(_id_key(request_id), error=error):
460
+ return
461
+ cancel = self._cancel_outbound
462
+ if cancel is not None and self._state == "open":
463
+ self._write_quiet(self._message(method=cancel.method, params=cancel.params(request_id)))
464
+
465
+ _UNSET: Any = object()
466
+
467
+ def _settle(self, key: Any, value: Any = _UNSET, error: BaseException | None = None) -> bool:
468
+ """The only place a pending request ends (`key` is `_id_key(id)`). Returns False if it had already ended."""
469
+ pending = self._pending.pop(key, None)
470
+ if pending is None:
471
+ return False
472
+ for cleanup in pending.cleanups:
473
+ try:
474
+ cleanup()
475
+ except Exception: # noqa: BLE001
476
+ pass
477
+ if not pending.future.done():
478
+ if error is not None:
479
+ pending.future.set_exception(error)
480
+ else:
481
+ pending.future.set_result(value)
482
+ return True
483
+
484
+ # ---- inbound -------------------------------------------------------------
485
+
486
+ async def _read_loop(self) -> None:
487
+ try:
488
+ while True:
489
+ try:
490
+ message = await self._channel.receive()
491
+ except EndOfChannel:
492
+ break
493
+ self._receive(message)
494
+ if self._state == "closed":
495
+ # A handler closed the peer (on 3.12+ it runs eagerly, inside this loop):
496
+ # nothing more is read, even what is already buffered.
497
+ break
498
+ self._drain()
499
+ except asyncio.CancelledError:
500
+ self._finish(self._close_info or CloseInfo("local"))
501
+ raise
502
+ except Exception as error: # noqa: BLE001
503
+ self._finish(self._close_info or CloseInfo("error", error))
504
+
505
+ def _drain(self) -> None:
506
+ """The other side ended its stream: fail what waits on it, let running handlers answer, then close."""
507
+ if self._state != "open":
508
+ return
509
+ self._state = "draining"
510
+ self._fail_pending(CloseInfo("remote-ended"))
511
+ limit = self._drain_ms
512
+ if self._running == 0 or not limit > 0:
513
+ self._finish(CloseInfo("remote-ended"))
514
+ elif math.isfinite(limit):
515
+ self._drain_timer = self._clock.set_timeout(lambda: self._finish(CloseInfo("remote-ended")), limit)
516
+
517
+ def _receive(self, message: Any) -> None:
518
+ if self._state == "closed":
519
+ return # nothing is dispatched on a closed connection
520
+ self._tap("in", message)
521
+ if not isinstance(message, dict):
522
+ self._diagnose("invalid-message", "Message is not a JSON object", message)
523
+ if isinstance(message, list):
524
+ self._reply_error(None, ErrorCode.INVALID_REQUEST, "Batches are not supported")
525
+ return
526
+ method = message.get("method")
527
+ has_id = "id" in message
528
+ message_id = message.get("id")
529
+ if isinstance(method, str):
530
+ if has_id:
531
+ if _is_id(message_id):
532
+ self._handle_request(message_id, method, message.get("params"))
533
+ else:
534
+ self._reply_error(None, ErrorCode.INVALID_REQUEST, "Request id must be a string, number or null")
535
+ return
536
+ self._handle_notification(method, message.get("params"))
537
+ return
538
+ if has_id and ("result" in message or "error" in message):
539
+ self._handle_response(message)
540
+ return
541
+ self._diagnose("invalid-message", "Message is not a request, notification or response", message)
542
+ if has_id and _is_id(message_id):
543
+ self._reply_error(message_id, ErrorCode.INVALID_REQUEST, "Invalid request")
544
+
545
+ def _handle_response(self, message: dict[str, Any]) -> None:
546
+ message_id = message.get("id")
547
+ key = _id_key(message_id) if _is_id(message_id) and message_id is not None else None
548
+ if key is None or key not in self._pending:
549
+ self._diagnose("late-response", f"Response for unknown or finished request {message_id!r}", message)
550
+ return
551
+ error = message.get("error")
552
+ if error is not None:
553
+ if (
554
+ isinstance(error, dict)
555
+ and isinstance(error.get("code"), int)
556
+ and not isinstance(error.get("code"), bool)
557
+ and isinstance(error.get("message"), str)
558
+ ):
559
+ rpc = RpcError(error["code"], error["message"], error["data"]) if "data" in error else RpcError(error["code"], error["message"])
560
+ self._settle(key, error=rpc)
561
+ else:
562
+ method = self._pending[key].method
563
+ self._settle(
564
+ key,
565
+ error=LocalError("invalid-response", f"{self.name}: malformed error for {method}", method=method, cause=error),
566
+ )
567
+ return
568
+ self._settle(key, value=message.get("result"))
569
+
570
+ def _handle_request(self, request_id: JsonRpcId, method: str, params: Any) -> None:
571
+ key = _id_key(request_id)
572
+ if key in self._inbound:
573
+ self._diagnose("duplicate-request-id", f"Request id {request_id!r} reused while in flight")
574
+ controller = AbortController()
575
+ if self._connection.signal.aborted: # a listener added now would never be called
576
+ controller.abort(LocalError("closed", "connection closed"))
577
+ remove_close = self._connection.signal.add_listener(lambda _reason: controller.abort(LocalError("closed", "connection closed")))
578
+ self._inbound[key] = controller
579
+ context = InboundContext(method=method, signal=controller.signal, peer=self, id=request_id, is_request=True)
580
+
581
+ async def run() -> None:
582
+ try:
583
+ handler = self._request_handlers.get(method) or self._request_handlers.get("*")
584
+
585
+ async def handle() -> Any:
586
+ if handler is None:
587
+ raise RpcError.method_not_found(method)
588
+ return await _resolve(handler(params, context))
589
+
590
+ try:
591
+ if self._inbound_chain:
592
+ result = await self._run_inbound(Call("request", method, params, request_id), context, handle)
593
+ elif handler is None:
594
+ raise RpcError.method_not_found(method)
595
+ else:
596
+ result = handler(params, context)
597
+ if inspect.isawaitable(result):
598
+ result = await result
599
+ reply = self._message(id=request_id, result=result)
600
+ except asyncio.CancelledError:
601
+ if self._state != "closed":
602
+ self._reply_error(request_id, ErrorCode.REQUEST_CANCELLED, "Request cancelled")
603
+ raise
604
+ except Exception as error: # noqa: BLE001
605
+ if self._state == "closed":
606
+ return
607
+ if isinstance(error, RpcError):
608
+ reply = self._message(id=request_id, error=error.to_json())
609
+ elif controller.signal.aborted:
610
+ reply = self._message(id=request_id, error={"code": ErrorCode.REQUEST_CANCELLED, "message": "Request cancelled"})
611
+ else:
612
+ self._diagnose("handler-failed", f"Handler for {method} raised", error)
613
+ message = str(error) or type(error).__name__
614
+ reply = self._message(id=request_id, error={"code": ErrorCode.INTERNAL_ERROR, "message": message})
615
+ await self._answer(request_id, method, reply)
616
+ finally:
617
+ if self._inbound.get(key) is controller:
618
+ del self._inbound[key]
619
+ remove_close()
620
+ self._running -= 1
621
+ if self._running == 0 and self._state == "draining":
622
+ self._finish(CloseInfo("remote-ended"))
623
+
624
+ self._running += 1
625
+ # Started eagerly where it can be: a handler that answers without waiting answers now.
626
+ self._spawn(run(), f"{self.name}:{method}", eager=True)
627
+
628
+ def _handle_notification(self, method: str, params: Any) -> None:
629
+ cancel = self._cancel_inbound
630
+ if cancel is not None and method == cancel.method:
631
+ try:
632
+ target = cancel.id(params)
633
+ except Exception as error: # noqa: BLE001
634
+ self._diagnose("invalid-message", f"Could not read the request id from {method}", error)
635
+ target = NO_ID
636
+ if target is not NO_ID:
637
+ controller = self._inbound.get(_id_key(target))
638
+ if controller is not None:
639
+ controller.abort(LocalError("aborted", "cancelled by the other side"))
640
+ handler = self._notification_handlers.get(method) or self._notification_handlers.get("*")
641
+ if handler is None:
642
+ return
643
+ context = InboundContext(method=method, signal=self._connection.signal, peer=self)
644
+
645
+ def fail(error: BaseException) -> None:
646
+ self._diagnose("handler-failed", f"Notification handler for {method} raised", error)
647
+
648
+ if not self._inbound_chain:
649
+ # No middleware: a plain handler runs now, in arrival order.
650
+ try:
651
+ result = handler(params, context)
652
+ except Exception as error: # noqa: BLE001
653
+ fail(error)
654
+ return
655
+ if inspect.isawaitable(result):
656
+ self._spawn(self._guard(result, fail), f"{self.name}:{method}")
657
+ return
658
+
659
+ async def handle() -> Any:
660
+ return await _resolve(handler(params, context))
661
+
662
+ self._spawn(self._guard(self._run_inbound(Call("notification", method, params), context, handle), fail), f"{self.name}:{method}")
663
+
664
+ @staticmethod
665
+ async def _guard(awaitable: Awaitable[Any], fail: Callable[[BaseException], None]) -> None:
666
+ try:
667
+ await awaitable
668
+ except Exception as error: # noqa: BLE001
669
+ fail(error)
670
+
671
+ async def _run_inbound(self, call: Call, context: InboundContext, handle: Callable[[], Awaitable[Any]]) -> Any:
672
+ chain = self._inbound_chain
673
+
674
+ async def dispatch(index: int) -> Any:
675
+ if index >= len(chain):
676
+ return await handle()
677
+ return await chain[index](call, lambda: dispatch(index + 1), context)
678
+
679
+ return await dispatch(0)
680
+
681
+ # ---- plumbing ------------------------------------------------------------
682
+
683
+ def _spawn(self, coroutine: Any, name: str, *, eager: bool = False) -> asyncio.Task[Any]:
684
+ task = _new_task(coroutine, name) if eager else asyncio.get_running_loop().create_task(coroutine, name=name)
685
+ if not task.done():
686
+ self._tasks.add(task)
687
+ task.add_done_callback(self._tasks.discard)
688
+ return task
689
+
690
+ @staticmethod
691
+ def _message(**body: Any) -> dict[str, Any]:
692
+ return {"jsonrpc": "2.0", **body}
693
+
694
+ async def _answer(self, request_id: JsonRpcId, method: str, reply: dict[str, Any]) -> None:
695
+ """Send the answer to a request. One that cannot be encoded becomes an InternalError."""
696
+ try:
697
+ await self._write(reply)
698
+ except Exception as error: # noqa: BLE001
699
+ if isinstance(error, LocalError) and error.kind == "closed":
700
+ return
701
+ self._diagnose("send-failed", f"Could not send the answer for {method}", error)
702
+ if isinstance(error, EncodeError):
703
+ self._reply_error(request_id, ErrorCode.INTERNAL_ERROR, f"The answer for {method} could not be encoded")
704
+
705
+ def _reply_error(self, request_id: JsonRpcId, code: int, message: str) -> None:
706
+ self._write_quiet(self._message(id=request_id, error={"code": code, "message": message}))
707
+
708
+ def _write(self, message: dict[str, Any]) -> asyncio.Future[None]:
709
+ """Queue a message. The future settles once it was handed to the channel (or could not be)."""
710
+ future: asyncio.Future[None] = asyncio.get_running_loop().create_future()
711
+ if self._state == "closed":
712
+ future.set_exception(LocalError("closed", f"{self.name}: connection closed"))
713
+ return future
714
+ self._tap("out", message)
715
+ self._outbox.append((message, future))
716
+ self._ensure_writer()
717
+ return future
718
+
719
+ def _write_quiet(self, message: dict[str, Any]) -> None:
720
+ future = self._write(message)
721
+ future.add_done_callback(lambda f: f.cancelled() or f.exception())
722
+
723
+ def _ensure_writer(self) -> None:
724
+ """Start the writer unless it is running; it writes what is queued (in order), then
725
+ closes the channel if the peer has closed. Started eagerly where it can be, so a write
726
+ to a channel that takes it at once is done before this returns."""
727
+ if self._writing:
728
+ return
729
+ self._writing = True
730
+ try:
731
+ self._writer_task = _new_task(self._write_loop(), f"{self.name}:write")
732
+ except BaseException:
733
+ self._writing = False
734
+ raise
735
+
736
+ async def _write_loop(self) -> None:
737
+ try:
738
+ while self._outbox:
739
+ message, future = self._outbox.popleft()
740
+ try:
741
+ await self._channel.send(message)
742
+ except asyncio.CancelledError:
743
+ if not future.done():
744
+ future.set_exception(LocalError("closed", f"{self.name}: connection closed"))
745
+ raise
746
+ except BaseException as error: # noqa: BLE001
747
+ if not future.done():
748
+ future.set_exception(error if isinstance(error, Exception) else ChannelClosed(str(error)))
749
+ if isinstance(error, ChannelClosed) and self._state == "draining":
750
+ # The other side stopped reading too: no answer can get through.
751
+ self._finish(CloseInfo("remote-ended"))
752
+ else:
753
+ if not future.done():
754
+ future.set_result(None)
755
+ if self._close_info is not None:
756
+ try:
757
+ await self._channel.close()
758
+ except Exception: # noqa: BLE001
759
+ pass
760
+ finally:
761
+ self._writing = False
762
+
763
+ def _tap(self, direction: Literal["in", "out"], message: Any) -> None:
764
+ if self._tap_fn is None:
765
+ return
766
+ try:
767
+ self._tap_fn(direction, message)
768
+ except Exception: # noqa: BLE001 - taps are observers; they never change behaviour
769
+ pass
770
+
771
+ def _diagnose(self, kind: DiagnosticKind, message: str, detail: Any = None) -> None:
772
+ if self._on_diagnostic is None:
773
+ return
774
+ try:
775
+ self._on_diagnostic(PeerDiagnostic(kind, message, detail))
776
+ except Exception: # noqa: BLE001
777
+ pass
778
+
779
+ def _finish(self, info: CloseInfo) -> None:
780
+ if self._state == "closed":
781
+ return
782
+ self._state = "closed"
783
+ self._close_info = info
784
+ if self._drain_timer is not None:
785
+ self._clock.clear_timeout(self._drain_timer)
786
+ self._drain_timer = None
787
+ self._connection.abort(LocalError("closed", f"{self.name}: connection closed"))
788
+ self._fail_pending(info)
789
+ # Flush what was queued, then end our side.
790
+ try:
791
+ self._ensure_writer()
792
+ except RuntimeError:
793
+ pass
794
+ if self._closed_future is not None and not self._closed_future.done():
795
+ self._closed_future.set_result(info)
796
+ listeners, self._close_listeners = self._close_listeners, []
797
+ for listener in listeners:
798
+ try:
799
+ listener(info)
800
+ except Exception: # noqa: BLE001
801
+ pass
802
+
803
+ def _fail_pending(self, info: CloseInfo) -> None:
804
+ if info.reason == "remote-ended":
805
+ text = f"{self.name}: the other side closed the connection"
806
+ elif info.reason == "error":
807
+ text = f"{self.name}: the connection failed"
808
+ else:
809
+ text = f"{self.name}: connection closed"
810
+ for key in list(self._pending):
811
+ pending = self._pending.get(key)
812
+ if pending is None:
813
+ continue
814
+ method = pending.method
815
+ self._settle(key, error=LocalError("closed", f"{text} while waiting for {method}", method=method, cause=info.error))