# SPDX-FileCopyrightText: Christian Amsüss and the aiocoap contributors # # SPDX-License-Identifier: MIT import asyncio from typing import Dict, Generic, TypeVar K = TypeVar("K") V = TypeVar("V") class TimeoutDict(Generic[K, V]): """A dict-ish type whose entries live on a timeout; adding and accessing an item each refreshes the timeout. The timeout is a lower bound; items may live up to twice as long. The container is implemented incompletely, with additions made on demand. This is not thread safe. """ def __init__(self, timeout: float): self.timeout = timeout """Timeout set on any access This can be changed at runtime, but changes only take effect """ self._items: Dict[K, V] = {} """The actual dictionary""" self._recently_accessed = None """Items accessed since the timeout last fired""" self._timeout = None """Canceler for the timeout function""" # Note: Without a __del__ implementation that even cancels, the object # will be kept alive by the main loop for a timeout def __getitem__(self, key): result = self._items[key] self._accessed(key) return result def __setitem__(self, key, value): self._items[key] = value self._accessed(key) def _start_over(self): """Clear _recently_accessed, set the timeout""" self._timeout = asyncio.get_running_loop().call_later(self.timeout, self._tick) self._recently_accessed = set() def _accessed(self, key): """Mark a key as recently accessed""" if self._timeout is None: self._start_over() # No need to add the key, it'll live for this duration anyway else: self._recently_accessed.add(key) def _tick(self): self._items = { k: v for (k, v) in self._items.items() if k in self._recently_accessed } if self._items: self._start_over() else: self._timeout = None self._recently_accessed = None