"""Sequential execution queue — one browser, one tab at a time. Every platform flow drives abang's real browser window. Two flows at once would fight over focus, file inputs and dialogs, so everything funnels through one worker task here. q = PostQueue(inter_platform_delay=(3.0, 6.0)) await q.start() result = await q.submit("threads", coro_factory) `submit` returns that job's result (or raises what the job raised). Jobs run strictly in submission order. """ import asyncio import random import time class PostQueue: def __init__(self, inter_platform_delay=(3.0, 6.0)): lo, hi = inter_platform_delay if lo < 0 or hi < lo: raise ValueError( f"inter_platform_delay must be (lo, hi) with 0 <= lo <= hi, got {lo, hi}" ) self.delay_lo, self.delay_hi = lo, hi self._queue = asyncio.Queue() self._worker = None self._last_finished = None self.history = [] # (iso_time, label, status) async def start(self): if self._worker is None: self._worker = asyncio.create_task(self._run()) return self async def stop(self): if self._worker: self._worker.cancel() try: await self._worker except asyncio.CancelledError: pass self._worker = None @property def depth(self): return self._queue.qsize() async def submit(self, label, factory): """Queue `factory()` (a zero-arg coroutine function) and await its result.""" if self._worker is None: raise RuntimeError("PostQueue.submit() before start()") future = asyncio.get_running_loop().create_future() await self._queue.put((label, factory, future)) return await future async def _run(self): while True: label, factory, future = await self._queue.get() await self._space_out() try: result = await factory() if not future.done(): future.set_result(result) status = result.get("status") if isinstance(result, dict) else "ok" except asyncio.CancelledError: if not future.done(): future.cancel() raise except Exception as exc: # noqa: BLE001 — hand it to the caller intact if not future.done(): future.set_exception(exc) status = f"raised {type(exc).__name__}" finally: self._last_finished = time.time() self._queue.task_done() self.history.append( (time.strftime("%Y-%m-%dT%H:%M:%S"), label, status) ) async def _space_out(self): """Rate-limit gap between consecutive platform jobs.""" if self._last_finished is None: return gap = random.uniform(self.delay_lo, self.delay_hi) elapsed = time.time() - self._last_finished if elapsed < gap: await asyncio.sleep(gap - elapsed)