src/genro_asgi_multiworker_spa/register_row.py¶
Source from this local checkout, regenerated when the reader rebuilds.
Line links use #L<number>; a GitHub line range opens its first line.
1 # Copyright 2025 Softwell S.r.l.2 #3 # Licensed under the Apache License, Version 2.0 (the "License");4 # you may not use this file except in compliance with the License.5 # You may obtain a copy of the License at6 #7 # https://www.apache.org/licenses/LICENSE-2.08 #9 # Unless required by applicable law or agreed to in writing, software10 # distributed under the License is distributed on an "AS IS" BASIS,11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.12 # See the License for the specific language governing permissions and13 # limitations under the License.14 15 """The three register rows as classes: a ``dict`` each, carrying what it knows of itself.16 17 A row is still a dict — ``row["field"]`` reads and writes as it always did,18 the parcel pickles a plain dict built from it, the census serialises it, the19 hosted site receives it as the daemon's item — but the class says what the20 worker used to hard-code about the row (#59, block 3):21 22 - ``default_fields``: what the row is born with beside what the caller passes23 (the row's own ``item_lock`` for every kind);24 - ``fields_left_behind``: what the parcel does NOT carry — the reserved id, the25 edges the folder already says, the lock, and the live objects bound to the26 Bags of the process the row lives in, which the birth on the other side27 makes anew;28 - ``fields_replayed``: what travels in the parcel but cannot be passed to the29 birth, and ``replay_fields`` puts it back on the row once it exists;30 - ``announcement_fields``: what the ``new_page`` worker event carries beside31 the identities.32 33 A consumer subclasses a row and names it on its registry34 (``RegisterRegistry.page_row_class`` and siblings); the worker asks the row and35 knows nothing of the fields. The three rows of the core carry no data of a36 hosted site: what a page queues, subscribes or watches is the consumer's row37 class's to add (genropy-asgi does).38 """39 40 from __future__ import annotations41 42 import asyncio43 import threading44 from typing import Any45 46 __all__ = ["ConnectionRow", "PageRow", "RegisterRow", "UserRow"]47 48 49 class RegisterRow(dict):50 """One register row: a dict born with its defaults, then the caller's fields.51 52 Args:53 fields: what the caller passes; wins over ``default_fields``.54 """55 56 #: What the parcel leaves behind: the reserved id and the row's own lock.57 fields_left_behind: frozenset[str] = frozenset({"register_item_id", "item_lock"})58 #: What travels but is put back after the birth, in order.59 fields_replayed: tuple[str, ...] = ()60 61 def __init__(self, fields: dict[str, Any] | None = None) -> None:62 super().__init__(self.default_fields())63 if fields:64 self.update(fields)65 66 def default_fields(self) -> dict[str, Any]:67 """The fields the row is born with: a fresh lock, exclusive and re-entrant."""68 return {"item_lock": threading.RLock()}69 70 def replay_fields(self, registry: Any, fields: dict[str, Any]) -> None:71 """Put back what travelled and could not be passed to the birth: nothing here."""72 73 def announcement_fields(self) -> dict[str, Any]:74 """What the birth announces beside the identities: nothing here."""75 return {}76 77 78 class UserRow(RegisterRow):79 """The user's row: the top of the chain. Its parcel is the store alone."""80 81 82 class ConnectionRow(RegisterRow):83 """The connection's row: the parcel leaves its two edges behind, the folder says them."""84 85 fields_left_behind = RegisterRow.fields_left_behind | {"user", "pages"}86 87 88 class PageRow(RegisterRow):89 """The page's row: the parcel leaves the edge to the connection behind.90 91 ``wsx`` is how this page uses its channel, and it is the command92 ``openchannel`` that writes it: absent until the page opened its channel,93 then ``True`` for the ordinary page or a dict of parameters for one that94 asked for something. A message for a page that never opened its channel is95 refused, so the field is also the proof that the browser and the row agree96 on who this page is. It TRAVELS in the parcel — it is ``True`` or a dict of97 plain data — because a user parked for being idle and woken by his next98 request never lost his websocket: the browser noticed nothing, and a row99 that came back without its channel would refuse the very next message of a100 page that is still connected.101 """102 103 fields_left_behind = RegisterRow.fields_left_behind | {"connection_id", "call_lock"}104 105 def default_fields(self) -> dict[str, Any]:106 """The row's own lock, its channel, and the queue its calls wait in.107 108 ``call_lock`` is an ``asyncio.Lock``, and it is a different thing from109 ``item_lock``: that one guards the row itself and is taken on whatever110 thread touches it, this one serialises the CALLS of this page, which111 are tasks on the worker's loop. It is taken only when the page asked to112 be served one call at a time, and it never travels — a page that comes113 back from the deposit gets a fresh one.114 """115 return {**super().default_fields(), "wsx": None, "call_lock": asyncio.Lock()}