Thread-safe map of ``task_id`` to ``TaskStateBase`` (or subclass). Substitute for the TS ``AppState.tasks: Record `` pattern. ``update(task_id, mutator)`` is the immutable-update analogue of the TS ``setAppState(prev => ...)`` idiom.
| 70 | |
| 71 | |
| 72 | class RuntimeTaskRegistry: |
| 73 | """Thread-safe map of ``task_id`` to ``TaskStateBase`` (or subclass). |
| 74 | |
| 75 | Substitute for the TS ``AppState.tasks: Record<string, TaskState>`` |
| 76 | pattern. ``update(task_id, mutator)`` is the immutable-update analogue |
| 77 | of the TS ``setAppState(prev => ...)`` idiom. |
| 78 | """ |
| 79 | |
| 80 | def __init__(self) -> None: |
| 81 | self._lock = threading.RLock() |
| 82 | self._tasks: dict[str, TaskStateBase] = {} |
| 83 | |
| 84 | # -- read paths ------------------------------------------------------- |
| 85 | |
| 86 | def get(self, task_id: str) -> TaskStateBase | None: |
| 87 | """Return the task with the given id, or ``None`` if absent.""" |
| 88 | with self._lock: |
| 89 | return self._tasks.get(task_id) |
| 90 | |
| 91 | def all(self) -> list[TaskStateBase]: |
| 92 | """Snapshot of every registered task. Returns a list, not a view — |
| 93 | callers iterating outside the lock should not see in-flight writes.""" |
| 94 | with self._lock: |
| 95 | return list(self._tasks.values()) |
| 96 | |
| 97 | def by_type(self, task_type: TaskType) -> list[TaskStateBase]: |
| 98 | """Snapshot filtered to a single TaskType — used by deprecated |
| 99 | compatibility views (e.g. ``ToolContext.background_bash_tasks``).""" |
| 100 | with self._lock: |
| 101 | return [t for t in self._tasks.values() if t.type == task_type] |
| 102 | |
| 103 | def __contains__(self, task_id: str) -> bool: |
| 104 | with self._lock: |
| 105 | return task_id in self._tasks |
| 106 | |
| 107 | def __len__(self) -> int: |
| 108 | with self._lock: |
| 109 | return len(self._tasks) |
| 110 | |
| 111 | def __iter__(self) -> Iterator[TaskStateBase]: |
| 112 | # Iterate over the snapshot, not the live dict, to avoid |
| 113 | # RuntimeError if writers add entries during iteration. |
| 114 | return iter(self.all()) |
| 115 | |
| 116 | # -- write paths ------------------------------------------------------ |
| 117 | |
| 118 | def upsert(self, task: TaskStateBase) -> None: |
| 119 | """Insert or replace the entry for ``task.id``.""" |
| 120 | with self._lock: |
| 121 | self._tasks[task.id] = task |
| 122 | |
| 123 | def remove(self, task_id: str) -> bool: |
| 124 | """Drop the entry; returns True iff something was removed.""" |
| 125 | with self._lock: |
| 126 | return self._tasks.pop(task_id, None) is not None |
| 127 | |
| 128 | def update( |
| 129 | self, |
no outgoing calls