Questions¶
The reasoning behind the shapes the kits take. None of this is needed to use a kit, but it is what stops the obvious alternatives being retried.
Why a topic, and not a mixin?¶
A kit has two jobs: it adds handlers to a connection, and it lets code with no connection push events to clients. The natural first attempt is a mixin parametrised with the kit's events:
That reads well until someone composes two kits, because both parametrise the same base differently:
error: Base classes of AppConsumer are mutually incompatible
Base class "ChanxWebsocketConsumerMixin[NotifyEvent | ChatEvent]" derives from
...which is incompatible with type "ChanxWebsocketConsumerMixin[NotifyEvent]"
Composing kits is the entire point of a registry, so the shape has to survive it. A topic does, because topics are listed on a consumer rather than inherited:
class AppConsumer(AsyncJsonWebsocketConsumer[AppEvents]):
topics = [UserNotificationTopic, RoomChatTopic, PresenceTopic]
Their event types never meet, so any number compose. Each topic keeps its own handler
namespace too, so two kits may both define a cancel action without colliding, and its
own authorize, so one kit cannot widen another's access.
class UserNotificationTopic(Topic[NotificationMessage]):
pattern = "notification:user:{user_id}"
passthrough_events: ClassVar[list[type[BaseMessage]]] = [NotificationMessage]
async def authorize(self, **params: str) -> bool:
return self.current_user_id() == params["user_id"]
What does a topic give me?¶
UserNotificationTopic.broadcast(topic, event)reaches every subscriber, from a signal or worker, with no consumer imported and nothing to bind.AppConsumer.broadcast_event(event)stays typed to the consumer's own union.- Any number of kits compose with no suppressions, and each works alone.
- A topic can also serve a dedicated route with
as_consumer(), where connecting is the subscription and clients send no envelope.
Why must a broadcast come from the class the consumer mounts?¶
A group name is derived from the topic string and the topic's class name. So
MyNotificationTopic and the UserNotificationTopic it subclasses address different
groups, even for the same topic string:
UserNotificationTopic.group_name("notification:user:42")
# UserNotificationTopic.notification-user-42
MyNotificationTopic.group_name("notification:user:42")
# MyNotificationTopic.notification-user-42
The namespace is what keeps two unrelated topic classes from colliding once unsupported characters are replaced. The cost is that as soon as you override a hook, code outside the connection has to publish through your subclass.
Getting it wrong is silent, because a broadcast to a group nobody joined is not an error. If a worker's events never arrive, check this first.
Why is tier metadata rather than a directory?¶
So promotion is a one-line change, and because copit installs kits side by side into
one directory: nesting them under core/ and contrib/ here would make a cross-kit
import need a different depth than it does in a user's project. See
our registry.
Why does the test harness arrive only with --with tests?¶
Because a user who did not ask for tests should not receive a test harness. Kits
declare chanx-testing under their tests optional group rather than in requires,
and copit resolves a group's requirements only when that group is selected. See
our registry.