caprid-relay 0.1.0__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,133 @@
1
+ """Relay Python SDK.
2
+
3
+ Owns the Relay product API and authentication convention, built on ``moq_core``. The default
4
+ transport is native QUIC through aioquic (``caprid_relay.quic``), which reaches a relay natively with no
5
+ browser; ``create_transport`` swaps in another implementation of the same seam.
6
+
7
+ The public names are the rows of ``spec/vocabulary.md``, pinned by ``tests/test_exports.py``. From
8
+ the core this package re-exports exactly what ``@caprid/relay`` re-exports, plus ``Closed``; every
9
+ other protocol type (location filters, fetch, track status, the constants) is imported from
10
+ ``moq_core``, a declared dependency.
11
+ """
12
+
13
+ from moq_core import (
14
+ Closed,
15
+ Goaway,
16
+ KeyValuePair,
17
+ LocationFilter,
18
+ MessageParameters,
19
+ MoqtObject,
20
+ MoqTrackSource,
21
+ MoqtTransport,
22
+ PublishDone,
23
+ RequestError,
24
+ SubscribeOk,
25
+ Subscription,
26
+ SubscriptionHandlers,
27
+ )
28
+
29
+ from ._awaitable import RelayAwaitable
30
+ from ._subscription import RelaySubscription, SubscriptionEnd, SubscriptionEndKind
31
+ from ._supervision import SupervisedStatus, SupervisionBackoff
32
+ from .cancellation import CancellationToken, Cancelled
33
+ from .connect import RelaySubscriber, RelaySubscriptionHandlers, connect_relay_subscriber
34
+ from .connect_session import ConnectingTransport, RelayCredential, RelayTransportFactory
35
+ from .errors import RelayError, RelayErrorCode, RelayErrorPhase
36
+ from .namespace_events import NamespaceEvent, NamespaceEvents
37
+ from .payload import read_json, read_text
38
+ from .publish import RelayGroupWriter, RelayPublisher, RelayTrackWriter, connect_relay_publisher
39
+ from .room_endpoint import room_endpoint
40
+ from .subscription_events import (
41
+ GoawayEvent,
42
+ ObjectEvent,
43
+ PublisherDoneEvent,
44
+ StateNotifyEvent,
45
+ SubgroupEndEvent,
46
+ SubgroupResetEvent,
47
+ SubscribedEvent,
48
+ SubscriptionConnectionClosedError,
49
+ SubscriptionEvent,
50
+ SubscriptionEvents,
51
+ SubscriptionOverflowError,
52
+ SubscriptionRequestError,
53
+ UpdateOkEvent,
54
+ )
55
+ from .supervised_publisher import PublisherGeneration, SupervisedPublisher, connect_supervised_publisher
56
+ from .supervised_subscriber import (
57
+ RestartReason,
58
+ SupervisedSubscriber,
59
+ WantHandlers,
60
+ WantSwitchedInfo,
61
+ connect_supervised_subscriber,
62
+ )
63
+ from .switchable import SwitchableHandlers, SwitchableSubscription, SwitchedInfo
64
+
65
+ DEFAULT_ENDPOINT = "https://relay.mux.global"
66
+ """The default Relay endpoint pinned in ``spec/protocol.json``."""
67
+
68
+ __all__ = [
69
+ "DEFAULT_ENDPOINT",
70
+ "CancellationToken",
71
+ "Cancelled",
72
+ "Closed",
73
+ "ConnectingTransport",
74
+ "Goaway",
75
+ "GoawayEvent",
76
+ "KeyValuePair",
77
+ "LocationFilter",
78
+ "MessageParameters",
79
+ "MoqTrackSource",
80
+ "MoqtObject",
81
+ "MoqtTransport",
82
+ "NamespaceEvent",
83
+ "NamespaceEvents",
84
+ "ObjectEvent",
85
+ "PublishDone",
86
+ "PublisherDoneEvent",
87
+ "PublisherGeneration",
88
+ "RelayAwaitable",
89
+ "RelayCredential",
90
+ "RelayError",
91
+ "RelayErrorCode",
92
+ "RelayErrorPhase",
93
+ "RelayGroupWriter",
94
+ "RelayPublisher",
95
+ "RelaySubscriber",
96
+ "RelaySubscription",
97
+ "RelaySubscriptionHandlers",
98
+ "RelayTrackWriter",
99
+ "RelayTransportFactory",
100
+ "RequestError",
101
+ "RestartReason",
102
+ "StateNotifyEvent",
103
+ "SubgroupEndEvent",
104
+ "SubgroupResetEvent",
105
+ "SubscribeOk",
106
+ "SubscribedEvent",
107
+ "Subscription",
108
+ "SubscriptionConnectionClosedError",
109
+ "SubscriptionEnd",
110
+ "SubscriptionEndKind",
111
+ "SubscriptionEvent",
112
+ "SubscriptionEvents",
113
+ "SubscriptionHandlers",
114
+ "SubscriptionOverflowError",
115
+ "SubscriptionRequestError",
116
+ "SupervisedPublisher",
117
+ "SupervisedStatus",
118
+ "SupervisedSubscriber",
119
+ "SupervisionBackoff",
120
+ "SwitchableHandlers",
121
+ "SwitchableSubscription",
122
+ "SwitchedInfo",
123
+ "UpdateOkEvent",
124
+ "WantHandlers",
125
+ "WantSwitchedInfo",
126
+ "connect_relay_publisher",
127
+ "connect_relay_subscriber",
128
+ "connect_supervised_publisher",
129
+ "connect_supervised_subscriber",
130
+ "read_json",
131
+ "read_text",
132
+ "room_endpoint",
133
+ ]
@@ -0,0 +1,59 @@
1
+ """The type of every fact the SDK settles once and lets any number of tasks await."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import contextvars
7
+ from collections.abc import Callable, Generator
8
+ from typing import Any, Generic, TypeVar
9
+
10
+ _T = TypeVar("_T")
11
+
12
+
13
+ class RelayAwaitable(Generic[_T]):
14
+ """A fact the SDK settles once: a subscription's ``subscribed`` or ``ended``, a publisher's
15
+ ``announced``, or a connection's ``closed``.
16
+
17
+ Await it as often as needed. Each waiter is isolated: a task cancelled while awaiting, or an
18
+ ``asyncio.wait_for`` that times out, never touches the underlying result, where handing out the
19
+ future itself would let one impatient waiter destroy the signal for every later one. ``done()``,
20
+ ``result()``, ``exception()`` and ``add_done_callback()`` read the result directly and allocate
21
+ nothing, so polling is free. It is not an :class:`asyncio.Future`: it cannot be cancelled or
22
+ set. Wrap it with :func:`asyncio.ensure_future` for an API that needs a future.
23
+ """
24
+
25
+ __slots__ = ("_future",)
26
+
27
+ def __init__(self, future: asyncio.Future[_T]) -> None:
28
+ self._future = future
29
+
30
+ def __await__(self) -> Generator[Any, None, _T]:
31
+ future = self._future
32
+ if not future.done():
33
+ future = asyncio.shield(future)
34
+ return future.__await__()
35
+
36
+ def done(self) -> bool:
37
+ """Whether the result is in."""
38
+ return self._future.done()
39
+
40
+ def result(self) -> _T:
41
+ """The result; raises what awaiting would, or ``asyncio.InvalidStateError`` while pending."""
42
+ return self._future.result()
43
+
44
+ def exception(self) -> BaseException | None:
45
+ """The exception awaiting would raise, ``None`` for a plain result."""
46
+ return self._future.exception()
47
+
48
+ def add_done_callback(
49
+ self, callback: Callable[[asyncio.Future[_T]], object], *, context: contextvars.Context | None = None
50
+ ) -> None:
51
+ """Call ``callback`` with the settled future once the result is in."""
52
+ self._future.add_done_callback(callback, context=context)
53
+
54
+ def remove_done_callback(self, callback: Callable[[asyncio.Future[_T]], object]) -> int:
55
+ """Forget a callback; returns how many registrations were removed."""
56
+ return self._future.remove_done_callback(callback)
57
+
58
+
59
+ __all__ = ["RelayAwaitable"]
@@ -0,0 +1,166 @@
1
+ """The bounded events iterator both events forms share: one owned request, a queue bound and a
2
+ cancellation token. ``subscription_events.py`` and ``namespace_events.py`` each supply the request
3
+ and the events; this module owns the queue, the single pending read and the ending.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import asyncio
9
+ from collections import deque
10
+ from collections.abc import Callable
11
+ from types import TracebackType
12
+ from typing import Generic, Protocol, Self, TypeVar
13
+
14
+ from ._subscription import SubscriptionOverflowError
15
+ from .cancellation import CancellationToken
16
+
17
+ E = TypeVar("E")
18
+
19
+
20
+ class OwnedRequest(Protocol):
21
+ """The request an iterator owns: all it needs is the cancel."""
22
+
23
+ def cancel(self, reason: object = None) -> None:
24
+ """Tear the request down; its echo through the sink is ignored."""
25
+
26
+
27
+ class BoundedEventSink(Generic[E]):
28
+ """Where a producer delivers into a bounded iterator."""
29
+
30
+ __slots__ = ("_events",)
31
+
32
+ def __init__(self, events: BoundedEvents[E]) -> None:
33
+ self._events = events
34
+
35
+ def event(self, event: E) -> None:
36
+ """One event; past the capacity the iterator ends with ``SubscriptionOverflowError``."""
37
+ self._events._push(event)
38
+
39
+ def ended(self, error: BaseException | None = None) -> None:
40
+ """The producer ended: ``None`` finishes the iterator cleanly, anything else is raised from reads.
41
+
42
+ Either way the queue is discarded.
43
+ """
44
+ # An ending the iterator started (close, cancellation, overflow) echoes back here.
45
+ self._events._finish(error)
46
+
47
+ def complete(self) -> None:
48
+ """The producer finished cleanly: queued events are still read, then the iterator is done."""
49
+ self._events._finish(None, keep_queue=True)
50
+
51
+
52
+ class BoundedEvents(Generic[E]):
53
+ """Single-consumer iterator over one owned request. Use ``async with`` for deterministic cleanup.
54
+
55
+ At most ``capacity`` events are retained. Overflow discards the queue, cancels only this request
56
+ and raises on reads. No transport backpressure is implied. Explicit close produces EOF; failures
57
+ raise.
58
+ """
59
+
60
+ def __init__(
61
+ self,
62
+ start: Callable[[BoundedEventSink[E]], OwnedRequest],
63
+ *,
64
+ capacity: int = 64,
65
+ cancellation: CancellationToken | None = None,
66
+ ) -> None:
67
+ if isinstance(capacity, bool) or not isinstance(capacity, int | float):
68
+ raise TypeError("capacity must be a number")
69
+ if not isinstance(capacity, int) or capacity <= 0:
70
+ raise ValueError("capacity must be a positive integer")
71
+ self._capacity = capacity
72
+ self._queue: deque[E] = deque()
73
+ # A one-element tuple, so an event that is itself ``None`` stays distinguishable from a wake.
74
+ self._pending: asyncio.Future[tuple[E] | None] | None = None
75
+ self._ended = False
76
+ self._failure: BaseException | None = None
77
+ self._request: OwnedRequest | None = None
78
+ self._cancellation = cancellation
79
+ if cancellation is not None and cancellation.cancelled:
80
+ self._finish(cancellation.reason)
81
+ return
82
+ if cancellation is not None:
83
+ cancellation.add_callback(self._finish)
84
+ try:
85
+ self._request = start(BoundedEventSink(self))
86
+ except BaseException:
87
+ self.close()
88
+ raise
89
+ # Events can overflow the queue before start() returns. The ending recorded then had no
90
+ # request to cancel, so cancel it now; the echo of that cancel is ignored.
91
+ if self._ended:
92
+ self._request.cancel(self._failure)
93
+
94
+ def _push(self, event: E) -> None:
95
+ if self._ended:
96
+ return
97
+ if self._pending is not None and not self._pending.done():
98
+ self._pending.set_result((event,))
99
+ return
100
+ if len(self._queue) == self._capacity:
101
+ self._finish(SubscriptionOverflowError("Subscription event buffer overflow"))
102
+ return
103
+ self._queue.append(event)
104
+
105
+ def _finish(self, error: BaseException | None = None, *, keep_queue: bool = False) -> None:
106
+ if self._ended:
107
+ return
108
+ self._ended = True
109
+ self._failure = error
110
+ # A pending read only exists while the queue is empty, so keeping it still settles that read as done.
111
+ if not keep_queue:
112
+ self._queue.clear()
113
+ if self._cancellation is not None:
114
+ self._cancellation.remove_callback(self._finish)
115
+ if self._request is not None:
116
+ self._request.cancel(error)
117
+ # Wake by value, not exception: abandoned reads cannot leak an unretrieved exception.
118
+ if self._pending is not None and not self._pending.done():
119
+ self._pending.set_result(None)
120
+
121
+ def __aiter__(self) -> Self:
122
+ return self
123
+
124
+ async def __anext__(self) -> E:
125
+ if self._pending is not None:
126
+ raise RuntimeError("Only one pending subscription read is supported")
127
+ try:
128
+ if not self._ended and not self._queue:
129
+ self._pending = asyncio.get_running_loop().create_future()
130
+ handed = await self._pending
131
+ # A handoff already completed before teardown belongs to the consumer, like TS.
132
+ if handed is not None:
133
+ return handed[0]
134
+ # Events left by a clean completion are read before the end.
135
+ if self._queue:
136
+ return self._queue.popleft()
137
+ if self._ended:
138
+ if self._failure is not None:
139
+ raise self._failure
140
+ raise StopAsyncIteration
141
+ raise RuntimeError("unreachable: an empty queue on a live iterator is awaited")
142
+ except asyncio.CancelledError:
143
+ self.close()
144
+ raise
145
+ finally:
146
+ self._pending = None
147
+
148
+ def close(self) -> None:
149
+ """Discard queued events and settle pending reads as EOF. Idempotent."""
150
+ self._finish()
151
+ self._queue.clear()
152
+
153
+ async def aclose(self) -> None:
154
+ """:meth:`close` as a coroutine, for ``contextlib.aclosing`` and other async-iterator cleanup."""
155
+ self.close()
156
+
157
+ async def __aenter__(self) -> Self:
158
+ return self
159
+
160
+ async def __aexit__(
161
+ self, exc_type: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None
162
+ ) -> None:
163
+ self.close()
164
+
165
+
166
+ __all__ = ["BoundedEventSink", "BoundedEvents", "OwnedRequest"]
@@ -0,0 +1,179 @@
1
+ """The frame reader: a subscription's or a want's handlers that read its track as frames in decode order.
2
+
3
+ It drives a sequencer it is given (``moq_media.FrameSequencer`` qualifies), which owns the ordering, the deadlines
4
+ and the LOC format, so this package stays free of the format package. See the "Media subscribing" section of
5
+ ``spec/behavior.md``.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ from collections.abc import Callable, Sequence
12
+ from typing import Generic, Protocol, TypeVar
13
+
14
+ from moq_core import KeyValuePair, MoqtObject, PublishDone, RequestError, SubgroupEnd, SubgroupReset, SubscribeOk
15
+
16
+ from .connect import RelaySubscriptionHandlers
17
+ from .supervised_subscriber import RestartReason, WantHandlers, WantSwitchedInfo
18
+
19
+ F = TypeVar("F")
20
+ F_co = TypeVar("F_co", covariant=True)
21
+
22
+ # Poll at least this often while a wait runs: another track may reset or re-anchor the shared clock meanwhile.
23
+ _CLOCK_RECHECK_US = 50_000
24
+
25
+
26
+ class FrameReaderPoll(Protocol[F_co]):
27
+ """What a sequencer's ``poll()`` returns: the frames ready now, and when to poll again in microseconds."""
28
+
29
+ @property
30
+ def frames(self) -> Sequence[F_co]: ...
31
+
32
+ @property
33
+ def wait_us(self) -> int | None: ...
34
+
35
+
36
+ class FrameReaderSequencer(Protocol[F_co]):
37
+ """What a :class:`FrameReader` drives: ``moq_media.FrameSequencer`` qualifies."""
38
+
39
+ track_properties: Sequence[KeyValuePair]
40
+
41
+ def object(self, obj: MoqtObject) -> None: ...
42
+
43
+ def group_ended(self, group_id: int) -> None: ...
44
+
45
+ def group_reset(self, group_id: int) -> None: ...
46
+
47
+ def restart(self) -> None: ...
48
+
49
+ def poll(self, max_frames: int | None = None) -> FrameReaderPoll[F_co]: ...
50
+
51
+
52
+ class FrameReader(Generic[F]):
53
+ """Read a track as frames: feed the sequencer every event, and poll it as the application reads, or when a wait it
54
+ reports ends (at most 50 ms on, since another track may move a shared clock). Iterate it for the frames, in decode
55
+ order; :meth:`close` ends the iteration.
56
+
57
+ Pass :meth:`subscription_handlers` to ``RelaySubscriber.subscribe`` or :meth:`want_handlers` to
58
+ ``SupervisedSubscriber.want``. Each group is one subgroup, as the frame writer sends it, so a subgroup's end is its
59
+ group's. A want read through a reader keeps ``drain`` at 0, the default: a drained old subscription's groups would
60
+ arrive after the Move, indistinguishable from the new one's. Frames wait in the sequencer until they are read, and
61
+ lateness is judged then, so a slow reader gets fresh frames rather than a backlog.
62
+ """
63
+
64
+ def __init__(self, sequencer: FrameReaderSequencer[F]) -> None:
65
+ self._sequencer = sequencer
66
+ self._changed: asyncio.Event | None = None
67
+ self._closed = False
68
+
69
+ @property
70
+ def closed(self) -> bool:
71
+ """Whether :meth:`close` was called."""
72
+ return self._closed
73
+
74
+ def close(self) -> None:
75
+ """Stop delivering and drop frames not yet read. Later events are ignored."""
76
+ self._closed = True
77
+ self._wake()
78
+
79
+ def __aiter__(self) -> FrameReader[F]:
80
+ return self
81
+
82
+ async def __anext__(self) -> F:
83
+ while not self._closed:
84
+ # One frame per read: whatever waits stays in the sequencer, judged late when it is taken.
85
+ result = self._sequencer.poll(1)
86
+ if result.frames:
87
+ return result.frames[0]
88
+ changed = self._changed = self._changed or asyncio.Event()
89
+ changed.clear()
90
+ timeout = None if result.wait_us is None else min(result.wait_us, _CLOCK_RECHECK_US) / 1_000_000
91
+ try:
92
+ await asyncio.wait_for(changed.wait(), timeout)
93
+ except TimeoutError:
94
+ pass
95
+ raise StopAsyncIteration
96
+
97
+ # --- handlers
98
+
99
+ def subscription_handlers(
100
+ self,
101
+ *,
102
+ on_error: Callable[[RequestError], None] | None = None,
103
+ on_publisher_done: Callable[[PublishDone], None] | None = None,
104
+ ) -> RelaySubscriptionHandlers:
105
+ """Handlers for one subscription, with the application's own error and completion callbacks."""
106
+ return RelaySubscriptionHandlers(
107
+ on_subscribed=self._subscribed,
108
+ on_error=on_error,
109
+ on_object=self._object,
110
+ on_subgroup_end=self._subgroup_end,
111
+ on_subgroup_reset=self._subgroup_reset,
112
+ on_publisher_done=on_publisher_done,
113
+ )
114
+
115
+ def want_handlers(
116
+ self,
117
+ *,
118
+ on_subscribed: Callable[[SubscribeOk, tuple[str, ...]], None] | None = None,
119
+ on_publisher_done: Callable[[PublishDone], None] | None = None,
120
+ on_restarted: Callable[[RestartReason, tuple[str, ...]], None] | None = None,
121
+ on_switched: Callable[[WantSwitchedInfo], None] | None = None,
122
+ on_error: Callable[[BaseException], None] | None = None,
123
+ ) -> WantHandlers:
124
+ """Handlers for a want. A Restart and a Move start the sequencer over; the application's callbacks run after."""
125
+
126
+ def subscribed(info: SubscribeOk, namespace: tuple[str, ...]) -> None:
127
+ self._subscribed(info)
128
+ if on_subscribed is not None:
129
+ on_subscribed(info, namespace)
130
+
131
+ def restarted(reason: RestartReason, namespace: tuple[str, ...]) -> None:
132
+ self._event(self._sequencer.restart)
133
+ if on_restarted is not None:
134
+ on_restarted(reason, namespace)
135
+
136
+ def switched(info: WantSwitchedInfo) -> None:
137
+ def move() -> None:
138
+ self._sequencer.restart()
139
+ self._sequencer.track_properties = info.subscribed.track_properties
140
+
141
+ self._event(move)
142
+ if on_switched is not None:
143
+ on_switched(info)
144
+
145
+ return WantHandlers(
146
+ on_subscribed=subscribed,
147
+ on_object=self._object,
148
+ on_subgroup_end=self._subgroup_end,
149
+ on_subgroup_reset=self._subgroup_reset,
150
+ on_publisher_done=on_publisher_done,
151
+ on_restarted=restarted,
152
+ on_switched=switched,
153
+ on_error=on_error,
154
+ )
155
+
156
+ def _subscribed(self, info: SubscribeOk) -> None:
157
+ if not self._closed:
158
+ self._sequencer.track_properties = info.track_properties
159
+
160
+ def _object(self, obj: MoqtObject) -> None:
161
+ self._event(lambda: self._sequencer.object(obj))
162
+
163
+ def _subgroup_end(self, end: SubgroupEnd) -> None:
164
+ self._event(lambda: self._sequencer.group_ended(end.group_id))
165
+
166
+ def _subgroup_reset(self, reset: SubgroupReset) -> None:
167
+ self._event(lambda: self._sequencer.group_reset(reset.group_id))
168
+
169
+ # --- delivery
170
+
171
+ def _event(self, apply: Callable[[], None]) -> None:
172
+ if self._closed:
173
+ return
174
+ apply()
175
+ self._wake()
176
+
177
+ def _wake(self) -> None:
178
+ if self._changed is not None:
179
+ self._changed.set()
@@ -0,0 +1,136 @@
1
+ """A publisher connection's settling groups (see ``CONTEXT.md``).
2
+
3
+ A settling group is one whose operation completed once its FIN was handed to the transport, but whose
4
+ FIN the relay has not acknowledged yet. aioquic queues without limit, so without a bound a dead uplink
5
+ keeps accepting groups until the congestion window fills (about 190 one-kilobyte groups). Settling
6
+ groups are what can pile up on a connection: open streams are already bounded by one per track. So
7
+ before an operation opens its stream it waits while the connection has :data:`SETTLING_GROUP_LIMIT`
8
+ settling groups, first come first served; it holds nothing while its stream is open, and its group
9
+ joins the set once its FIN is handed over. The count can briefly exceed the limit by the number of
10
+ groups open at that moment. The TypeScript SDK keeps the same set (``ts/src/settling-groups.ts``).
11
+
12
+ A settling group that fails later (STOP_SENDING, a reset, session teardown) leaves the set like one
13
+ that settles cleanly, and nobody hears of it: the cause surfaces as demand, a rebind, or the
14
+ connection's ``closed``. A group with a SUBGROUP_DELIVERY_TIMEOUT (draft-ietf-moq-transport-22 section
15
+ 5.2) carries its timer here: started when the group joins, it resets the stream if the acknowledgement
16
+ has not come by then, which fails the acknowledgement wait, so the group leaves the set the same
17
+ silent way and frees its room. The timer belongs to the group: a subscription change or end and the
18
+ track's close leave it running, and only the acknowledgement or the connection's end settles the
19
+ group first. ``RelayPublisher.close()`` drains the set before it ends the session, because closing
20
+ the connection discards whatever the relay has not acknowledged; delivery timeouts only end some of
21
+ the drained groups sooner.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import asyncio
27
+ from collections import deque
28
+ from collections.abc import Awaitable, Callable
29
+ from typing import Any
30
+
31
+ from moq_core._tasks import observe, spawn
32
+
33
+ SETTLING_GROUP_LIMIT = 64
34
+ """Settling groups per publisher connection before new streams wait. Internal, not an option."""
35
+
36
+
37
+ async def _settled(acknowledged: Awaitable[None]) -> None:
38
+ try:
39
+ await acknowledged
40
+ except Exception: # a late failure is dropped: it surfaces elsewhere (see the module doc)
41
+ pass
42
+
43
+
44
+ class SettlingGroups:
45
+ """The settling groups of one publisher connection, shared by every track on it."""
46
+
47
+ __slots__ = ("_limit", "_settling", "_waiting")
48
+
49
+ def __init__(self, limit: int = SETTLING_GROUP_LIMIT) -> None:
50
+ self._limit = limit
51
+ self._settling: set[asyncio.Task[None]] = set()
52
+ self._waiting: deque[asyncio.Future[None]] = deque()
53
+
54
+ @property
55
+ def size(self) -> int:
56
+ """How many groups are settling now."""
57
+ return len(self._settling)
58
+
59
+ def has_room(self) -> bool:
60
+ """Whether a new stream may open now: the connection is below the limit."""
61
+ return len(self._settling) < self._limit
62
+
63
+ async def wait_for_room(self, interrupted: asyncio.Future[None]) -> None:
64
+ """Wait until the connection is below the limit, in arrival order.
65
+
66
+ Raises the exception ``interrupted`` settles with, if that comes first; the wait then gives up
67
+ its place in the queue.
68
+ """
69
+ if self.has_room():
70
+ return
71
+
72
+ waiter: asyncio.Future[None] = asyncio.get_running_loop().create_future()
73
+ self._waiting.append(waiter)
74
+
75
+ try:
76
+ await asyncio.wait({waiter, interrupted}, return_when=asyncio.FIRST_COMPLETED)
77
+ finally:
78
+ if not waiter.done():
79
+ self._waiting.remove(waiter)
80
+ waiter.cancel()
81
+
82
+ if not waiter.done() or waiter.cancelled():
83
+ interrupted.result()
84
+
85
+ def add(
86
+ self,
87
+ acknowledged: Awaitable[None],
88
+ *,
89
+ timeout: float | None = None,
90
+ on_timeout: Callable[[], None] | None = None,
91
+ ) -> None:
92
+ """A group's FIN was handed over: count it until ``acknowledged`` settles, either way.
93
+
94
+ With a ``timeout`` (seconds, the group's SUBGROUP_DELIVERY_TIMEOUT), ``on_timeout`` runs if
95
+ ``acknowledged`` is still pending by then; it resets the group's stream, which settles
96
+ ``acknowledged`` with the reset.
97
+ """
98
+ tracked = spawn(_settled(acknowledged))
99
+ self._settling.add(tracked)
100
+ tracked.add_done_callback(self._settle)
101
+
102
+ if timeout is not None and on_timeout is not None:
103
+ timer = asyncio.get_running_loop().call_later(timeout, on_timeout)
104
+ tracked.add_done_callback(lambda _: timer.cancel())
105
+
106
+ async def drain(self, seconds: float, stop: asyncio.Future[Any], *also: Awaitable[None]) -> None:
107
+ """Wait for every settling group and each of ``also``, for at most ``seconds`` or until ``stop`` settles.
108
+
109
+ Never raises; whatever is still unsettled afterwards is left to the transport.
110
+ """
111
+ # Futures at once, so a wait cancelled before it starts still counts as awaited.
112
+ extra = [asyncio.ensure_future(awaitable) for awaitable in also]
113
+ waits = [*self._settling, *extra]
114
+
115
+ try:
116
+ if waits and not stop.done():
117
+ everything = asyncio.gather(*waits, return_exceptions=True)
118
+ observe(everything)
119
+ await asyncio.wait({everything, stop}, timeout=seconds, return_when=asyncio.FIRST_COMPLETED)
120
+ finally:
121
+ # Only the extra waits are ours to stop; the settling groups keep counting.
122
+ for wait in extra:
123
+ wait.cancel()
124
+
125
+ def _settle(self, tracked: asyncio.Task[None]) -> None:
126
+ self._settling.discard(tracked)
127
+
128
+ # Every waiter goes on, in arrival order, once the connection is below the limit again.
129
+ while self._waiting and self.has_room():
130
+ waiter = self._waiting.popleft()
131
+
132
+ if not waiter.done():
133
+ waiter.set_result(None)
134
+
135
+
136
+ __all__ = ["SETTLING_GROUP_LIMIT", "SettlingGroups"]