'''
Demonstrate the "service daemon" pattern: a named,
long-lived actor spawned via `ActorNursery.start_actor()`
which any other task can locate through the registrar using
`tractor.find_actor()` / `tractor.wait_for_actor()` - no
spawn-portal required - and RPC into directly.

Teardown is explicit and graceful via `portal.cancel_actor()`
once the clients are done.

'''
import trio
import tractor

_quotes: dict[str, float] = {
    'btcusdt': 66_000.5,
    'ethusdt': 3_500.25,
}


async def get_quote(sym: str) -> float:
    '''
    Look up the "current" quote for a symbol.

    '''
    name: str = tractor.current_actor().name
    print(f'{name}: serving quote for {sym!r}')
    return _quotes[sym]


async def client_task() -> None:
    '''
    Locate the quote service by name and RPC it; note no
    spawn-nursery/portal reference is ever passed in here!

    '''
    # a lookup miss yields `None` (not an error).
    async with tractor.find_actor('no_such_svc') as portal:
        assert portal is None
        print('client: "no_such_svc" is not registered')
    # block until the service shows up in the registry,
    # then call into it through the delivered portal.
    async with tractor.wait_for_actor('quote_svc') as portal:
        quote: float = await portal.run(
            get_quote,
            sym='btcusdt',
        )
        print(f'client: got btcusdt quote {quote}')


async def main() -> None:
    async with tractor.open_nursery() as an:
        portal = await an.start_actor(
            'quote_svc',
            enable_modules=[__name__],
        )
        # run the client in a separate task which discovers
        # the daemon purely by its registered name.
        async with trio.open_nursery() as tn:
            tn.start_soon(client_task)
        # explicit graceful teardown of the daemon.
        print('root: cancelling quote_svc')
        await portal.cancel_actor()
    print('root: service shut down cleanly')


if __name__ == '__main__':
    trio.run(main)
