async def subscribe_async( aobv: AsyncObserver[TSource]) -> AsyncDisposable: safe_obv, auto_detach = auto_detach_observer(aobv) infinite: Iterable[int] = seq.infinite async def worker( inbox: MailboxProcessor[Tuple[Notification[TSource], int]] ) -> None: @tailrec_async async def message_loop( current_index: int) -> TailCallResult[NoReturn]: n, index = await inbox.receive() with match(n) as case: log.debug("debounce: %s, %d, %d", n, index, current_index) for x in case(OnNext[TSource]): if index == current_index: await safe_obv.asend(x) current_index = index elif index > current_index: current_index = index for err in case(OnError[TSource]): await safe_obv.athrow(err) while case(OnCompleted): await safe_obv.aclose() return TailCall(current_index) await message_loop(-1) agent = MailboxProcessor.start(worker) indexer = iter(infinite) async def obv(n: Notification[TSource]) -> None: index = next(indexer) agent.post((n, index)) async def worker() -> None: log.debug("debounce, sleeping: %s", seconds) await asyncio.sleep(seconds) agent.post((n, index)) aiotools.start(worker()) obv_: AsyncObserver[TSource] = AsyncNotificationObserver(obv) dispose = await pipe(obv_, source.subscribe_async, auto_detach) async def cancel() -> None: await dispose.dispose_async() agent.post((OnCompleted, 0)) return AsyncDisposable.create(cancel)
async def subscribe_async( aobv: AsyncObserver[TSource]) -> AsyncDisposable: async def worker( inbox: MailboxProcessor[Tuple[Notification[TSource], datetime]] ) -> None: @tailrec_async async def loop() -> TailCallResult[None]: if token.is_cancellation_requested: return ns, due_time = await inbox.receive() diff = due_time - datetime.utcnow() seconds = diff.total_seconds() if seconds > 0: await asyncio.sleep(seconds) async def matcher() -> None: with match(ns) as case: for x in case(OnNext[TSource]): await aobv.asend(x) return for err in case(OnError[TSource]): await aobv.athrow(err) return for x in case(OnCompleted): await aobv.aclose() return await matcher() return TailCall() asyncio.ensure_future(loop()) agent = MailboxProcessor.start(worker, token) async def fn(ns: Notification[TSource]) -> None: due_time = datetime.utcnow() + timedelta(seconds=seconds) agent.post((ns, due_time)) obv: AsyncNotificationObserver[ TSource] = AsyncNotificationObserver(fn) subscription = await source.subscribe_async(obv) async def cancel() -> None: log.debug("delay:cancel()") cts.cancel() await subscription.dispose_async() return AsyncDisposable.create(cancel)
async def test_mailbox(xs: List[int]) -> None: result: List[int] = [] async def process(inbox: MailboxProcessor[int]): """the message processing function.""" async def message_loop() -> None: msg: int = await inbox.receive() result.append(msg) return await message_loop() return await message_loop() # start the loop agent = MailboxProcessor.start(process) for x in xs: agent.post(x) await asyncio.sleep(0) assert result == xs
async def subscribe_async(aobv: AsyncObserver[TSource]) -> AsyncDisposable: safe_obv, auto_detach = auto_detach_observer(aobv) async def worker( inbox: MailboxProcessor[Notification[TSource]]) -> None: @tailrec_async async def message_loop( latest: Notification[TSource]) -> TailCallResult[NoReturn]: n = await inbox.receive() async def get_latest() -> Notification[TSource]: with match(n) as case: for x in case(OnNext[TSource]): if n == latest: break try: await safe_obv.asend(x) except Exception as ex: await safe_obv.athrow(ex) break for err in case(OnError[TSource]): await safe_obv.athrow(err) break while case(OnCompleted): await safe_obv.aclose() break return n latest = await get_latest() return TailCall(latest) await message_loop( OnCompleted ) # Use as sentinel value as it will not match any OnNext value agent = MailboxProcessor.start(worker) async def notification(n: Notification[TSource]) -> None: agent.post(n) obv: AsyncObserver[TSource] = AsyncNotificationObserver(notification) return await pipe(obv, source.subscribe_async, auto_detach)
async def test_mailbox_post_and_async_reply(x: int): async def process(inbox: MailboxProcessor[Tuple[int, AsyncReplyChannel[str]]]): """the message processing function.""" async def message_loop() -> None: msg, rc = await inbox.receive() rc.reply(f"Got {msg}") return await message_loop() # start the loop return await message_loop() agent: MailboxProcessor[Tuple[ int, AsyncReplyChannel[str]]] = MailboxProcessor.start(process) build_message: Callable[[AsyncReplyChannel[str]], Tuple[int, AsyncReplyChannel[str]]] = lambda r: (x, r) reply = await agent.post_and_async_reply(build_message) assert reply == f"Got {x}"
async def subscribe_async(aobv: AsyncObserver[TSource]) -> AsyncDisposable: safe_obv, auto_detach = auto_detach_observer(aobv) def obv(mb: MailboxProcessor[Msg[TSource]], id: int): async def asend(value: TSource) -> None: await safe_obv.asend(value) async def athrow(error: Exception) -> None: await safe_obv.athrow(error) async def aclose() -> None: pipe(Key(id), InnerCompletedMsg, mb.post) return AsyncAnonymousObserver(asend, athrow, aclose) async def worker(inbox: MailboxProcessor[Msg[TSource]]) -> None: @tailrec_async async def message_loop(current: Option[AsyncDisposable], is_stopped: bool, current_id: int) -> TailCallResult[None]: cmd = await inbox.receive() with match(cmd) as case: for xs in case(InnerObservableMsg[TSource]): next_id = current_id + 1 for disp in current.to_list(): await disp.dispose_async() inner = await xs.subscribe_async(obv(inbox, next_id)) current, current_id = Some(inner), next_id break for idx in case(InnerCompletedMsg[Key]): if is_stopped and idx == current_id: await safe_obv.aclose() current, is_stopped = Nothing, True break while case(CompletedMsg): if current.is_none(): await safe_obv.aclose() break while case(DisposeMsg): if current.is_some(): await current.value.dispose_async() current, is_stopped = Nothing, True break return TailCall(current, is_stopped, current_id) await message_loop(Nothing, False, 0) inner_agent = MailboxProcessor.start(worker) async def asend(xs: AsyncObservable[TSource]) -> None: pipe( InnerObservableMsg(xs), inner_agent.post, ) async def athrow(error: Exception) -> None: await safe_obv.athrow(error) async def aclose() -> None: inner_agent.post(CompletedMsg) _obv = AsyncAnonymousObserver(asend, athrow, aclose) dispose = await pipe( _obv, AsyncObserver, source.subscribe_async, auto_detach, ) async def cancel() -> None: await dispose.dispose_async() inner_agent.post(DisposeMsg) return AsyncDisposable.create(cancel)
async def main() -> None: root = Tk() root.title("aioreactive") mousemoves: AsyncSubject[Tuple[int, int]] = AsyncSubject() frame = Frame(root, width=800, height=600, bg="white") async def worker(mb: MailboxProcessor["Event[Misc]"]) -> None: while True: event = await mb.receive() await mousemoves.asend((event.x, event.y)) agent = MailboxProcessor.start(worker) frame.bind("<Motion>", agent.post) text = "TIME FLIES LIKE AN ARROW" labels = [Label(frame, text=c, bg="white") for c in text] def handle_label(label: Label, i: int) -> AsyncObservable[Tuple[Label, int, int]]: label.config(dict(borderwidth=0, padx=0, pady=0)) def mapper(x: int, y: int) -> Tuple[Label, int, int]: """Map mouse-move pos to label and new pos for label.""" return label, x + i * 12 + 15, y return pipe( mousemoves, # stream of mouse-moves rx.delay(i / 10.0), # delay each mouse-move based on index of char rx.starmap( mapper), # place label based on mouse pos and index of char ) stream = pipe( labels, # list of labels rx.from_iterable, # stream of labels rx.flat_mapi( handle_label), # swap stream of labels with stream of labels + pos ) async def asend(value: Tuple[Label, int, int]) -> None: """Perform side effect.""" label, x, y = value label.place(x=x, y=y) async def athrow(ex: Exception): print("Exception: ", ex) obv = AsyncAnonymousObserver(asend, athrow) subscription: AsyncDisposable async def start(): nonlocal subscription print("Subscribing stream") subscription = await stream.subscribe_async(obv) async def stop(): nonlocal subscription print("Disposing stream") await subscription.dispose_async() def handle_focus_in(event: "Event[Misc]"): asyncio.ensure_future(start()) def handle_focus_out(event: "Event[Misc]"): asyncio.ensure_future(stop()) root.bind("<FocusIn>", handle_focus_in) root.bind("<FocusOut>", handle_focus_out) frame.pack() running = True def signal_handler(signal: int, frame: FrameType) -> None: nonlocal running running = False sys.stderr.write("Exiting...\n") root.destroy() root.quit() signal.signal(signal.SIGINT, signal_handler) # A simple combined event loop while running: await asyncio.sleep(0.001) try: root.update() # type: ignore except Exception: pass
def _( source: AsyncObservable[AsyncObservable[TSource]] ) -> AsyncObservable[TSource]: async def subscribe_async( aobv: AsyncObserver[TSource]) -> AsyncDisposable: safe_obv, auto_detach = auto_detach_observer(aobv) initial_model = Model( subscriptions=map.empty, queue=frozenlist.empty, is_stopped=False, key=Key(0), ) async def worker(inbox: MailboxProcessor[Msg[TSource]]) -> None: def obv(key: Key) -> AsyncObserver[TSource]: async def asend(value: TSource) -> None: await safe_obv.asend(value) async def athrow(error: Exception) -> None: await safe_obv.athrow(error) async def aclose() -> None: inbox.post(InnerCompletedMsg(key)) return AsyncAnonymousObserver(asend, athrow, aclose) async def update(msg: Msg[TSource], model: Model[TSource]) -> Model[TSource]: # log.debug("update: %s, model: %s", msg, model) with match(msg) as case: for xs in case(InnerObservableMsg[TSource]): if max_concurrent == 0 or len( model.subscriptions) < max_concurrent: inner = await xs.subscribe_async(obv(model.key) ) return model.replace( subscriptions=model.subscriptions.add( model.key, inner), key=Key(model.key + 1), ) lst = FrozenList.singleton(xs) return model.replace(queue=model.queue.append(lst)) for key in case(InnerCompletedMsg[Key]): subscriptions = model.subscriptions.remove(key) if len(model.queue): xs = model.queue[0] inner = await xs.subscribe_async(obv(model.key) ) return model.replace( subscriptions=subscriptions.add( model.key, inner), key=model.key + 1, queue=model.queue.tail(), ) elif len(subscriptions): return model.replace( subscriptions=subscriptions) else: if model.is_stopped: await safe_obv.aclose() return model.replace(subscriptions=map.empty) while case(CompletedMsg): if not model.subscriptions: log.debug("merge_inner: closing!") await safe_obv.aclose() return model.replace(is_stopped=True) while case.default(): for dispose in model.subscriptions.values(): await dispose.dispose_async() return initial_model.replace(is_stopped=True) async def message_loop(model: Model[TSource]) -> None: while True: msg = await inbox.receive() model = await update(msg, model) if model.is_stopped and not model.subscriptions: break await message_loop(initial_model) agent = MailboxProcessor.start(worker) async def asend(xs: AsyncObservable[TSource]) -> None: log.debug("merge_inner:asend(%s)", xs) agent.post(InnerObservableMsg(inner_observable=xs)) async def athrow(error: Exception) -> None: await safe_obv.athrow(error) agent.post(DisposeMsg) async def aclose() -> None: agent.post(CompletedMsg) obv = AsyncAnonymousObserver(asend, athrow, aclose) dispose = await auto_detach(source.subscribe_async(obv)) async def cancel() -> None: await dispose.dispose_async() agent.post(DisposeMsg) return AsyncDisposable.create(cancel)
def _with_latest_from( source: AsyncObservable[TSource] ) -> AsyncObservable[Tuple[TSource, TOther]]: async def subscribe_async( aobv: AsyncObserver[Tuple[TSource, TOther]]) -> AsyncDisposable: safe_obv, auto_detach = auto_detach_observer(aobv) async def worker(inbox: MailboxProcessor[Msg[TSource]]) -> None: @tailrec_async async def message_loop( latest: Option[TOther]) -> TailCallResult[NoReturn]: cn = await inbox.receive() async def get_value(n: Notification[Any]) -> Option[Any]: with match(n) as case: for value in case(OnNext[TSource]): return Some(value) for err in case(OnError[TSource]): await safe_obv.athrow(err) while case.default(): await safe_obv.aclose() return Nothing source_value = Nothing if isinstance(cn, SourceMsg): cn = cast(SourceMsg[TSource], cn) source_value = await get_value(cn.value) else: cn = cast(OtherMsg[TOther], cn) latest = await get_value(cn.value) def binder(s: TSource) -> Option[Tuple[TSource, TOther]]: def mapper(o: TOther) -> Tuple[TSource, TOther]: return (s, o) return latest.map(mapper) combined = source_value.bind(binder) for x in combined.to_list(): await safe_obv.asend(x) return TailCall(latest) await message_loop(Nothing) agent = MailboxProcessor.start(worker) async def obv_fn1(n: Notification[TSource]) -> None: pipe(SourceMsg(n), agent.post) async def obv_fn2(n: Notification[TOther]) -> None: pipe(OtherMsg(n), agent.post) obv1: AsyncObserver[TSource] = AsyncNotificationObserver(obv_fn1) obv2: AsyncObserver[TOther] = AsyncNotificationObserver(obv_fn2) dispose1 = await pipe(obv1, source.subscribe_async, auto_detach) dispose2 = await pipe(obv2, other.subscribe_async, auto_detach) return AsyncDisposable.composite(dispose1, dispose2)