{"id":47611615,"url":"https://github.com/archivebox/abxbus","last_synced_at":"2026-04-24T05:02:51.485Z","repository":{"id":318735597,"uuid":"1069379944","full_name":"ArchiveBox/abxbus","owner":"ArchiveBox","description":"📢 Fast in-memory Python \u0026 Typescript event bus library with support for advanced concurrency control features, nested event tracking, Pydantic \u0026 Zod type enforcement, and more...","archived":false,"fork":false,"pushed_at":"2026-04-19T04:46:46.000Z","size":3059,"stargazers_count":5,"open_issues_count":4,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-19T06:30:40.346Z","etag":null,"topics":["abx","archivebox","async","celery","event-bus","event-driven","event-sourcing","eventbus","events","kafka","nats","postgres","pydantic","python","redis","sqlite","typescript","zod"],"latest_commit_sha":null,"homepage":"https://abxbus.archivebox.io","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":"browser-use/bubus","license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ArchiveBox.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-10-03T20:57:26.000Z","updated_at":"2026-04-19T04:46:49.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/ArchiveBox/abxbus","commit_stats":null,"previous_names":["pirate/bubus","pirate/bbus","archivebox/abx-bus"],"tags_count":40,"template":false,"template_full_name":null,"purl":"pkg:github/ArchiveBox/abxbus","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ArchiveBox%2Fabxbus","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ArchiveBox%2Fabxbus/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ArchiveBox%2Fabxbus/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ArchiveBox%2Fabxbus/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ArchiveBox","download_url":"https://codeload.github.com/ArchiveBox/abxbus/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ArchiveBox%2Fabxbus/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32209897,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-24T03:15:14.334Z","status":"ssl_error","status_checked_at":"2026-04-24T03:15:11.608Z","response_time":64,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["abx","archivebox","async","celery","event-bus","event-driven","event-sourcing","eventbus","events","kafka","nats","postgres","pydantic","python","redis","sqlite","typescript","zod"],"created_at":"2026-04-01T20:27:10.332Z","updated_at":"2026-04-24T05:02:51.462Z","avatar_url":"https://github.com/ArchiveBox.png","language":"Python","readme":"# `abxbus`: 📢 Production-ready multi-language event bus\n\n\u003cimg width=\"200\" alt=\"image\" src=\"https://github.com/user-attachments/assets/b3525c24-51ba-496c-b327-ccdfe46a7362\" align=\"right\" /\u003e\n\n[![DeepWiki: Python](https://img.shields.io/badge/DeepWiki-abxbus%2FPython-yellow.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAyCAYAAAAnWDnqAAAAAXNSR0IArs4c6QAAA05JREFUaEPtmUtyEzEQhtWTQyQLHNak2AB7ZnyXZMEjXMGeK/AIi+QuHrMnbChYY7MIh8g01fJoopFb0uhhEqqcbWTp06/uv1saEDv4O3n3dV60RfP947Mm9/SQc0ICFQgzfc4CYZoTPAswgSJCCUJUnAAoRHOAUOcATwbmVLWdGoH//PB8mnKqScAhsD0kYP3j/Yt5LPQe2KvcXmGvRHcDnpxfL2zOYJ1mFwrryWTz0advv1Ut4CJgf5uhDuDj5eUcAUoahrdY/56ebRWeraTjMt/00Sh3UDtjgHtQNHwcRGOC98BJEAEymycmYcWwOprTgcB6VZ5JK5TAJ+fXGLBm3FDAmn6oPPjR4rKCAoJCal2eAiQp2x0vxTPB3ALO2CRkwmDy5WohzBDwSEFKRwPbknEggCPB/imwrycgxX2NzoMCHhPkDwqYMr9tRcP5qNrMZHkVnOjRMWwLCcr8ohBVb1OMjxLwGCvjTikrsBOiA6fNyCrm8V1rP93iVPpwaE+gO0SsWmPiXB+jikdf6SizrT5qKasx5j8ABbHpFTx+vFXp9EnYQmLx02h1QTTrl6eDqxLnGjporxl3NL3agEvXdT0WmEost648sQOYAeJS9Q7bfUVoMGnjo4AZdUMQku50McDcMWcBPvr0SzbTAFDfvJqwLzgxwATnCgnp4wDl6Aa+Ax283gghmj+vj7feE2KBBRMW3FzOpLOADl0Isb5587h/U4gGvkt5v60Z1VLG8BhYjbzRwyQZemwAd6cCR5/XFWLYZRIMpX39AR0tjaGGiGzLVyhse5C9RKC6ai42ppWPKiBagOvaYk8lO7DajerabOZP46Lby5wKjw1HCRx7p9sVMOWGzb/vA1hwiWc6jm3MvQDTogQkiqIhJV0nBQBTU+3okKCFDy9WwferkHjtxib7t3xIUQtHxnIwtx4mpg26/HfwVNVDb4oI9RHmx5WGelRVlrtiw43zboCLaxv46AZeB3IlTkwouebTr1y2NjSpHz68WNFjHvupy3q8TFn3Hos2IAk4Ju5dCo8B3wP7VPr/FGaKiG+T+v+TQqIrOqMTL1VdWV1DdmcbO8KXBz6esmYWYKPwDL5b5FA1a0hwapHiom0r/cKaoqr+27/XcrS5UwSMbQAAAABJRU5ErkJggg==)](https://deepwiki.com/ArchiveBox/abxbus) [![PyPI - Version](https://img.shields.io/pypi/v/abxbus)](https://pypi.org/project/abxbus/) [![PyPi Downloads/week](https://static.pepy.tech/badge/bubus/week)](https://pepy.tech/projects/abxbus) ![GitHub last commit](https://img.shields.io/github/last-commit/ArchiveBox/abxbus)\n\n[![DeepWiki: TS](https://img.shields.io/badge/DeepWiki-abxbus%2FTypescript-blue.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAyCAYAAAAnWDnqAAAAAXNSR0IArs4c6QAAA05JREFUaEPtmUtyEzEQhtWTQyQLHNak2AB7ZnyXZMEjXMGeK/AIi+QuHrMnbChYY7MIh8g01fJoopFb0uhhEqqcbWTp06/uv1saEDv4O3n3dV60RfP947Mm9/SQc0ICFQgzfc4CYZoTPAswgSJCCUJUnAAoRHOAUOcATwbmVLWdGoH//PB8mnKqScAhsD0kYP3j/Yt5LPQe2KvcXmGvRHcDnpxfL2zOYJ1mFwrryWTz0advv1Ut4CJgf5uhDuDj5eUcAUoahrdY/56ebRWeraTjMt/00Sh3UDtjgHtQNHwcRGOC98BJEAEymycmYcWwOprTgcB6VZ5JK5TAJ+fXGLBm3FDAmn6oPPjR4rKCAoJCal2eAiQp2x0vxTPB3ALO2CRkwmDy5WohzBDwSEFKRwPbknEggCPB/imwrycgxX2NzoMCHhPkDwqYMr9tRcP5qNrMZHkVnOjRMWwLCcr8ohBVb1OMjxLwGCvjTikrsBOiA6fNyCrm8V1rP93iVPpwaE+gO0SsWmPiXB+jikdf6SizrT5qKasx5j8ABbHpFTx+vFXp9EnYQmLx02h1QTTrl6eDqxLnGjporxl3NL3agEvXdT0WmEost648sQOYAeJS9Q7bfUVoMGnjo4AZdUMQku50McDcMWcBPvr0SzbTAFDfvJqwLzgxwATnCgnp4wDl6Aa+Ax283gghmj+vj7feE2KBBRMW3FzOpLOADl0Isb5587h/U4gGvkt5v60Z1VLG8BhYjbzRwyQZemwAd6cCR5/XFWLYZRIMpX39AR0tjaGGiGzLVyhse5C9RKC6ai42ppWPKiBagOvaYk8lO7DajerabOZP46Lby5wKjw1HCRx7p9sVMOWGzb/vA1hwiWc6jm3MvQDTogQkiqIhJV0nBQBTU+3okKCFDy9WwferkHjtxib7t3xIUQtHxnIwtx4mpg26/HfwVNVDb4oI9RHmx5WGelRVlrtiw43zboCLaxv46AZeB3IlTkwouebTr1y2NjSpHz68WNFjHvupy3q8TFn3Hos2IAk4Ju5dCo8B3wP7VPr/FGaKiG+T+v+TQqIrOqMTL1VdWV1DdmcbO8KXBz6esmYWYKPwDL5b5FA1a0hwapHiom0r/cKaoqr+27/XcrS5UwSMbQAAAABJRU5ErkJggg==)](https://deepwiki.com/ArchiveBox/abxbus/3-typescript-implementation) [![NPM Version](https://img.shields.io/npm/v/abxbus)](https://www.npmjs.com/package/abxbus) [![PyPi Downloads/month](https://static.pepy.tech/badge/bubus/month)](https://pepy.tech/projects/abxbus) [![GitHub License](https://img.shields.io/github/license/ArchiveBox/abxbus)](https://github.com/ArchiveBox/abxbus)\n\nAbxBus is an in-memory event bus library for async Python and TS (node/browser).\n\nIt's designed for quickly building resilient, predictable, complex event-driven apps.\n\nIt \"just works\" with an intuitive, but powerful event JSON format + emit API that's consistent across both languages and scales consistently from one event up to millions (~0.2ms/event):\n\n```python\nclass SomeEvent(BaseEvent):\n    some_data: int\n\ndef handle_some_event(event: SomeEvent):\n    print('hi!')\n\nbus.on(SomeEvent, some_function)\nawait bus.emit(SomeEvent({some_data: 132}))\n# \"hi!\"\"\n```\n\nIt's async native, has proper automatic nested event tracking, and powerful concurrency control options. The API is inspired by `EventEmitter` or [`emittery`](https://github.com/sindresorhus/emittery) in JS, but it takes it a step further:\n\n- nice Pydantic / Zod schemas for events that can be exchanged between both languages\n- automatic UUIDv7s and monotonic nanosecond timestamps for ordering events globally\n- built in locking options to force strict global FIFO processing or fully parallel processing\n\n---\n\n♾️ It's inspired by the simplicity of async and events in `JS` but with baked-in features that allow to eliminate most of the tedious repetitive complexity in event-driven codebases:\n\n- correct timeout enforcement across multiple levels of events, if a parent times out it correctly aborts all child event processing\n- ability to strongly type hint and enforce the return type of event handlers at compile-time\n- ability to queue events on the bus, or inline await them for immediate execution like a normal function call\n- handles thousands of events/sec/core in both languages; see the runtime matrix below for current measured numbers\n\n\u003cbr/\u003e\n\n## 🔢 Quickstart\n\nInstall abxbus and get started with a simple event-driven application:\n\n```bash\npip install abxbus      # see ./abxbus-ts/README.md for JS instructions\n```\n\n```python\nimport asyncio\nfrom abxbus import EventBus, BaseEvent\nfrom your_auth_events import AuthRequestEvent, AuthResponseEvent\n\nclass UserLoginEvent(BaseEvent[str]):\n    username: str\n    is_admin: bool\n\nasync def handle_login(event: UserLoginEvent) -\u003e str:\n    auth_request = await event.emit(AuthRequestEvent(...))  # nested events supported\n    auth_response = await event.event_bus.find(AuthResponseEvent, child_of=auth_request, future=30)\n    return f\"User {event.username} logged in admin={event.is_admin} with API response: {await auth_response.event_result()}\"\n\nbus = EventBus()\nbus.on(UserLoginEvent, handle_login)\nbus.on(AuthRequestEvent, AuthAPI.post)\n\nevent = bus.emit(UserLoginEvent(username=\"alice\", is_admin=True))\nprint(await event.event_result())\n# User alice logged in admin=True with API response: {...}\n```\n\n\u003cbr/\u003e\n\n---\n\n\u003cbr/\u003e\n\n## ✨ Features\n\n\u003cbr/\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003e🔎 Event Pattern Matching\u003c/strong\u003e\u003c/summary\u003e\n\n[Subscribe to events](https://abxbus.archivebox.io/features/event-pattern-matching) using multiple patterns:\n\n```python\n# By event model class (recommended for best type hinting)\nbus.on(UserActionEvent, handler)\n\n# By event type string\nbus.on('UserActionEvent', handler)\n\n# Wildcard - handle all events\nbus.on('*', universal_handler)\n```\n\n\u003cbr/\u003e\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003e🔀 Async and Sync Handler Support\u003c/strong\u003e\u003c/summary\u003e\n\nRegister both [synchronous and asynchronous handlers](https://abxbus.archivebox.io/features/async-sync-handlers) for maximum flexibility:\n\n```python\n# Async handler\nasync def async_handler(event: SomeEvent) -\u003e str:\n    await asyncio.sleep(0.1)  # Simulate async work\n    return \"async result\"\n\n# Sync handler\ndef sync_handler(event: SomeEvent) -\u003e str:\n    return \"sync result\"\n\nbus.on(SomeEvent, async_handler)\nbus.on(SomeEvent, sync_handler)\n```\n\nHandlers can also be defined under classes for easier organization:\n\n```python\nclass SomeService:\n    some_value = 'this works'\n\n    async def handlers_can_be_methods(self, event: SomeEvent) -\u003e str:\n        return self.some_value\n\n    @classmethod\n    async def handler_can_be_classmethods(cls, event: SomeEvent) -\u003e str:\n        return cls.some_value\n\n    @staticmethod\n    async def handlers_can_be_staticmethods(event: SomeEvent) -\u003e str:\n        return 'this works too'\n\n# All usage patterns behave the same:\nbus.on(SomeEvent, SomeService().handlers_can_be_methods)\nbus.on(SomeEvent, SomeService.handler_can_be_classmethods)\nbus.on(SomeEvent, SomeService.handlers_can_be_staticmethods)\n```\n\n\u003cbr/\u003e\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003e🔠 Type-Safe Events with Pydantic\u003c/strong\u003e\u003c/summary\u003e\n\nDefine events as Pydantic models with [full type checking and validation](https://abxbus.archivebox.io/features/typed-events):\n\n```python\nfrom typing import Any\nfrom abxbus import BaseEvent\n\nclass OrderCreatedEvent(BaseEvent):\n    order_id: str\n    customer_id: str\n    total_amount: float\n    items: list[dict[str, Any]]\n\n# Events are automatically validated\nevent = OrderCreatedEvent(\n    order_id=\"ORD-123\",\n    customer_id=\"CUST-456\",\n    total_amount=99.99,\n    items=[{\"sku\": \"ITEM-1\", \"quantity\": 2}]\n)\n```\n\n\u003e [!TIP]\n\u003e You can also enforce the types of [event handler return values](https://abxbus.archivebox.io/features/return-value-handling#typed-return-values).\n\n\u003cbr/\u003e\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003e⏩ Forward `Events` Between `EventBus`s\u003c/strong\u003e\u003c/summary\u003e\n\nYou can define separate `EventBus` instances in different \"microservices\" to separate different areas of concern.\n`EventBus`s can be set up to [forward events between each other](https://abxbus.archivebox.io/features/forwarding-between-buses) (with automatic loop prevention):\n\n```python\n# Create a hierarchy of buses\nmain_bus = EventBus(name='MainBus')\nauth_bus = EventBus(name='AuthBus')\ndata_bus = EventBus(name='DataBus')\n\n# Share all or specific events between buses\nmain_bus.on('*', auth_bus.emit)  # if main bus gets LoginEvent, will forward to AuthBus\nauth_bus.on('*', data_bus.emit)  # auth bus will forward everything to DataBus\ndata_bus.on('*', main_bus.emit)  # don't worry! event will only be processed once by each, no infinite loop occurs\n\n# Events flow through the hierarchy with tracking\nevent = main_bus.emit(LoginEvent())\nawait event\nprint(event.event_path)  # ['MainBus#ab12', 'AuthBus#cd34', 'DataBus#ef56']  # list of bus labels that already processed the event\n```\n\n\u003cbr/\u003e\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003e🔱 Event Handler Return Value Support\u003c/strong\u003e\u003c/summary\u003e\n\n[Collect results](https://abxbus.archivebox.io/features/return-value-handling) from multiple handlers:\n\n```python\nasync def load_user_config(event: GetConfigEvent) -\u003e dict[str, Any]:\n    return {\"debug\": True, \"port\": 8080}\n\nasync def load_system_config(event: GetConfigEvent) -\u003e dict[str, Any]:\n    return {\"debug\": False, \"timeout\": 30}\n\nbus.on(GetConfigEvent, load_user_config)\nbus.on(GetConfigEvent, load_system_config)\n\n# Get all handler result values\nevent = await bus.emit(GetConfigEvent())\nresults = await event.event_results_list()\n\n# Inspect per-handler metadata when needed\nfor handler_id, event_result in event.event_results.items():\n    print(handler_id, event_result.handler_name, event_result.result)\n```\n\n\u003cbr/\u003e\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003e🚦 FIFO / Parallel Event Processing\u003c/strong\u003e\u003c/summary\u003e\n\nBy default, events and their handlers are processed in [strict serial FIFO order](https://abxbus.archivebox.io/concurrency/events-bus-serial), maintaining consistency:\n\n```python\n# Events are processed in the order they were emitted\nfor i in range(10):\n    bus.emit(ProcessTaskEvent(task_id=i))\n\n# Even with async handlers, order is preserved\nawait bus.wait_until_idle(timeout=30.0)\n```\n\nIf a handler emits and awaits any child events during execution, those events will [jump the FIFO queue](https://abxbus.archivebox.io/concurrency/immediate-execution) and be processed immediately:\n\n```python\ndef child_handler(event: SomeOtherEvent) -\u003e str:\n    return 'xzy123'\n\ndef main_handler(event: MainEvent) -\u003e str:\n    # emit an owned child event\n    child_event = event.emit(SomeOtherEvent())\n\n    # can also await child events to process immediately instead of adding to FIFO queue\n    completed_child_event = await child_event\n    return f'result from awaiting child event: {await completed_child_event.event_result()}'  # 'xyz123'\n\nbus.on(SomeOtherEvent, child_handler)\nbus.on(MainEvent, main_handler)\n\nawait bus.emit(MainEvent()).event_result()\n# result from awaiting child event: xyz123\n```\n\nYou can also set [`event_concurrency='parallel'`](https://abxbus.archivebox.io/concurrency/events-parallel) and [`event_handler_concurrency='parallel'`](https://abxbus.archivebox.io/concurrency/handlers-parallel) options per-bus, per-event, or per-handler enable parallel processing when needed.\n\n\u003cbr/\u003e\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003e🪆 Emit Nested Child Events From Handlers\u003c/strong\u003e\u003c/summary\u003e\n\n[Automatically track event relationships](https://abxbus.archivebox.io/features/parent-child-tracking) and causality tree:\n\n```python\nasync def parent_handler(event: BaseEvent):\n    # handlers can emit owned child events\n    child = ChildEvent()\n    child_event_async = event.emit(child)\n    assert child.event_status != 'completed'\n    assert child_event_async.event_parent_id == event.event_id\n    await child_event_async\n\n    # awaiting the child blocks until it finishes processing\n    # this recursively waits for all handlers, including if event is forwarded to other buses\n    # (note: awaiting an event from inside a handler jumps the FIFO queue and will process it immediately, before any other pending events)\n    child_event_sync = await event.emit(ChildEvent())\n    # ChildEvent handlers run immediately\n    assert child_event_sync.event_status == 'completed'\n\n    # in all cases, parent-child relationships are automagically tracked\n    assert child_event_sync.event_parent_id == event.event_id\n\nasync def run_main():\n    bus.on(ChildEvent, child_handler)\n    bus.on(ParentEvent, parent_handler)\n\n    parent_event = bus.emit(ParentEvent())\n    print(parent_event.event_children)           # show all the child events emitted during handling of an event\n    await parent_event\n    print(bus.log_tree())\n    await bus.stop()\n\nif __name__ == '__main__':\n    asyncio.run(run_main())\n```\n\n\u003cimg width=\"100%\" alt=\"show the whole tree of events at any time using the logging helpers\" src=\"https://github.com/user-attachments/assets/f94684a6-7694-4066-b948-46925f47b56c\" /\u003e\u003cbr/\u003e\n\u003cimg width=\"100%\" alt=\"intelligent timeout handling to differentiate handler that timed out from handler that was interrupted\" src=\"https://github.com/user-attachments/assets/8da341fd-6c26-4c68-8fec-aef1ca55c189\" /\u003e\n\n\u003cbr/\u003e\u003cbr/\u003e\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003e🔎 Find Events in History or Wait for Future Events\u003c/strong\u003e\u003c/summary\u003e\n\n[`find()`](https://abxbus.archivebox.io/features/find-events) is the single lookup API: search history, wait for future events, or combine both to check for an existing recent event before emitting a new one.\n\n```python\n# Default: non-blocking history lookup (past=True, future=False)\nexisting = await bus.find(ResponseEvent)\n\n# Wait only for future matches\nfuture = await bus.find(ResponseEvent, past=False, future=5)\n\n# Combine event predicate + event metadata filters\nmatch = await bus.find(\n    ResponseEvent,\n    where=lambda e: e.request_id == my_id,\n    event_status='completed',\n    future=5,\n)\n\n# Wildcard: match any event type, filtered by metadata/predicate\nany_completed = await bus.find(\n    '*',\n    where=lambda e: e.event_type.endswith('ResultEvent'),\n    event_status='completed',\n    future=5,\n)\n```\n\n#### Finding Child Events\n\nWhen you emit an event that triggers child events, use `child_of` to find specific descendants:\n\n```python\n# Emit a parent event that triggers child events\nnav_event = await bus.emit(NavigateToUrlEvent(url=\"https://example.com\"))\n\n# Find a child event (already fired while NavigateToUrlEvent was being handled)\nnew_tab = await bus.find(TabCreatedEvent, child_of=nav_event, past=5)\nif new_tab:\n    print(f\"New tab created: {new_tab.tab_id}\")\n```\n\nThis solves race conditions where child events fire before you start waiting for them.\n\nSee the `EventBus.find(...)` API section below for full parameter details.\n\n\u003e [!IMPORTANT]\n\u003e `find()` resolves when the event is first _emitted_ to the `EventBus`, not when it completes.\n\u003e Use `await event` for immediate-await semantics (queue-jumps when called inside a handler), or `await event.event_completed()` to always wait in normal queue order.\n\u003e If no match is found (or future timeout elapses), `find()` returns `None`.\n\n\u003cbr/\u003e\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003e🔁 Event Debouncing\u003c/strong\u003e\u003c/summary\u003e\n\nAvoid re-running expensive work by reusing recent events. The `find()` method makes [debouncing](https://abxbus.archivebox.io/features/find-events#7-debounce-expensive-work) simple:\n\n```python\n# Simple debouncing: reuse event from last 10 seconds, or emit new\nevent = await (\n    await bus.find(ScreenshotEvent, past=10, future=False)  # Check last 10s of history (instant)\n    or bus.emit(ScreenshotEvent())\n)\n\n# Advanced: check history, wait briefly for new event to appear, fallback to emit new event\nevent = (\n    await bus.find(SyncEvent, past=True, future=False)   # Check all history (instant)\n    or await bus.find(SyncEvent, past=False, future=5)   # Wait up to 5s for in-flight\n    or bus.emit(SyncEvent())                         # Fallback: emit new\n)\nawait event                                              # get completed event\n```\n\n\u003cbr/\u003e\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003e🎯 Event Handler Return Values\u003c/strong\u003e\u003c/summary\u003e\n\nThere are two ways to get [return values](https://abxbus.archivebox.io/features/return-value-handling) from event handlers:\n\n**1. Have handlers return their values directly, which puts them in `event.event_results`:**\n\n```python\nclass DoSomeMathEvent(BaseEvent[int]):  # BaseEvent[int] = handlers are validated as returning int\n    a: int\n    b: int\n\n    # int passed above gets saved to:\n    # event_result_type = int\n\ndef do_some_math(event: DoSomeMathEvent) -\u003e int:\n    return event.a + event.b\n\nevent_bus.on(DoSomeMathEvent, do_some_math)\nprint(await event_bus.emit(DoSomeMathEvent(a=100, b=120)).event_result())\n# 220\n```\n\nYou can use these helpers to interact with the results returned by handlers:\n\n- `BaseEvent.event_result()`\n- `BaseEvent.event_results_list()`\n- Inspect raw per-handler entries via `BaseEvent.event_results`\n\n**2. Have the handler do the work, then emit another event containing the result value, which other code can find:**\n\n```python\ndef do_some_math(event: DoSomeMathEvent[int]) -\u003e int:\n    result = event.a + event.b\n    event.emit(MathCompleteEvent(final_sum=result))\n\nevent_bus.on(DoSomeMathEvent, do_some_math)\nawait event_bus.emit(DoSomeMathEvent(a=100, b=120))\nresult_event = await event_bus.find(MathCompleteEvent, past=False, future=30)\nprint(result_event.final_sum)\n# 220\n```\n\nThese events can also be emitted automatically for you if you enable the [`AutoReturnEventMiddleware`](https://abxbus.archivebox.io/integrations/middleware-auto-return).\n\n#### Annotating Event Handler Return Value Types\n\nAbxBus supports optional [strict typing for Event handler return values](https://abxbus.archivebox.io/features/return-value-handling#typed-return-values) using a generic parameter passed to `BaseEvent[ReturnTypeHere]`.\nFor example if you use `BaseEvent[str]`, abxbus would enforce that all handler functions must return `str | None` at compile-time via IDE/`mypy`/`pyright`/`ty` type hints, and at runtime when each handler finishes.\n\n```python\nclass ScreenshotEvent(BaseEvent[bytes]):  # BaseEvent[bytes] will enforce that handlers can only return bytes\n    width: int\n    height: int\n\nasync def on_ScreenshotEvent(event: ScreenshotEvent) -\u003e bytes:\n    return b'someimagebytes...'  # ✅ IDE type-hints \u0026 runtime both enforce return type matches expected: bytes\n    return 123                   # ❌ will show mypy/pyright issue + raise TypeError if the wrong type is returned\n\nevent_bus.on(ScreenshotEvent, on_ScreenshotEvent)\n\n# Handler return values are automatically validated against the bytes type\nreturned_bytes = await event_bus.emit(ScreenshotEvent(...)).event_result()\nassert isinstance(returned_bytes, bytes)\n```\n\n**Important:** The validation uses Pydantic's `TypeAdapter`, which validates but does not coerce types. Handlers must return the exact type specified or `None`:\n\n```python\nclass StringEvent(BaseEvent[str]):\n    pass\n\n# ✅ This works - returns the expected str type\ndef good_handler(event: StringEvent) -\u003e str:\n    return \"hello\"\n\n# ❌ This fails validation - returns int instead of str\ndef bad_handler(event: StringEvent) -\u003e str:\n    return 42  # ValidationError: expected str, got int\n```\n\nThis also works with complex types and Pydantic models:\n\n```python\nclass EmailMessage(BaseModel):\n    subject: str\n    content_len: int\n    email_from: str\n\nclass FetchInboxEvent(BaseEvent[list[EmailMessage]]):\n    account_id: UUID\n    auth_key: str\n\nasync def fetch_from_gmail(event: FetchInboxEvent) -\u003e list[EmailMessage]:\n    return [EmailMessage(subject=msg.subj, ...) for msg in GmailAPI.get_msgs(event.account_id, ...)]\n\nevent_bus.on(FetchInboxEvent, fetch_from_gmail)\n\n# Return values are automatically validated as list[EmailMessage]\nemail_list = await event_bus.emit(FetchInboxEvent(account_id='124', ...)).event_result()\n```\n\nFor pure Python usage, `event_result_type` can be any Python/Pydantic type you want. For cross-language JSON roundtrips, object-like shapes (e.g. `TypedDict`, `dataclass`, model-like dict schemas) rehydrate on Python as Pydantic models, map keys are constrained to JSON object string keys, and fine-grained string constraints/custom field validator logic is not preserved.\n\n\u003cbr/\u003e\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003e🧵 ContextVar Propagation\u003c/strong\u003e\u003c/summary\u003e\n\nContextVars set before `emit()` are [automatically propagated to event handlers](https://abxbus.archivebox.io/features/context-propagation). This is essential for request-scoped context like request IDs, user sessions, or tracing spans:\n\n```python\nfrom contextvars import ContextVar\n\n# Define your context variables\nrequest_id: ContextVar[str] = ContextVar('request_id', default='\u003cunset\u003e')\nuser_id: ContextVar[str] = ContextVar('user_id', default='\u003cunset\u003e')\n\nasync def handler(event: MyEvent) -\u003e str:\n    # Handler sees the context values that were set before emit()\n    print(f\"Request: {request_id.get()}, User: {user_id.get()}\")\n    return \"done\"\n\nbus.on(MyEvent, handler)\n\n# Set context before emit (e.g., in FastAPI middleware)\nrequest_id.set('req-12345')\nuser_id.set('user-abc')\n\n# Handler will see request_id='req-12345' and user_id='user-abc'\nawait bus.emit(MyEvent())\n```\n\n**Context propagates through nested handlers:**\n\n```python\nasync def parent_handler(event: ParentEvent) -\u003e str:\n    # Context is captured at emit time\n    print(f\"Parent sees: {request_id.get()}\")  # 'req-12345'\n\n    # Child events inherit the same context\n    await event.emit(ChildEvent())\n    return \"parent_done\"\n\nasync def child_handler(event: ChildEvent) -\u003e str:\n    # Child also sees the original emit context\n    print(f\"Child sees: {request_id.get()}\")  # 'req-12345'\n    return \"child_done\"\n```\n\n**Context isolation between emits:**\n\nEach emit captures its own context snapshot. Concurrent emits with different context values are properly isolated:\n\n```python\nrequest_id.set('req-A')\nevent_a = bus.emit(MyEvent())  # Handler A sees 'req-A'\n\nrequest_id.set('req-B')\nevent_b = bus.emit(MyEvent())  # Handler B sees 'req-B'\n\nawait event_a  # Still sees 'req-A'\nawait event_b  # Still sees 'req-B'\n```\n\n\u003e [!NOTE]\n\u003e Context is captured at `emit()` time, not when the handler executes. This ensures handlers see the context from the call site, even if the event is processed later from a queue.\n\n\u003cbr/\u003e\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003e🧹 Memory Management\u003c/strong\u003e\u003c/summary\u003e\n\nEventBus includes [automatic memory management](https://abxbus.archivebox.io/api/eventbus#shared-configuration-semantics) to prevent unbounded growth in long-running applications:\n\n```python\n# Create a bus with memory limits (default: 50 events)\nbus = EventBus(max_history_size=100)  # Keep max 100 events in history\n\n# Or disable memory limits for unlimited history\nbus = EventBus(max_history_size=None)\n\n# Or keep only in-flight events in history (drop each event as soon as it completes)\nbus = EventBus(max_history_size=0)\n\n# Or reject new emits when history is full (instead of dropping old history)\nbus = EventBus(max_history_size=100, max_history_drop=False)\n```\n\n**Automatic Cleanup:**\n\n- When `max_history_size` is set and `max_history_drop=True`, EventBus removes old events when the limit is exceeded\n- If `max_history_size=0`, history keeps only pending/started events and drops each event immediately after completion\n- If `max_history_drop=True`, the bus may drop oldest history entries even if they are uncompleted events\n- Completed events are removed first (oldest first), then started events, then pending events\n- This ensures active events are preserved while cleaning up old completed events\n\n**Manual Memory Management:**\n\n```python\n# For request-scoped buses (e.g. web servers), clear all memory after each request\ntry:\n    event_service = EventService()  # Creates internal EventBus\n    await event_service.process_request()\nfinally:\n    # Clear all event history and remove from global tracking\n    await event_service.eventbus.stop(clear=True)\n```\n\n**Memory Monitoring:**\n\n- EventBus automatically monitors total memory usage across all instances\n- Warnings are logged when total memory exceeds 50MB\n- Use `bus.stop(clear=True)` to completely free memory for unused buses\n- To avoid memory leaks from big events, the default limits are intentionally kept low. events are normally processed as they come in, and there is rarely a need to keep every event in memory longer after its complete. long-term storage should be accomplished using other mechanisms, like the WAL\n\n\u003cbr/\u003e\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003e⛓️ Parallel Handler Execution\u003c/strong\u003e\u003c/summary\u003e\n\n\u003e [!CAUTION]\n\u003e **Not Recommended.** Only for advanced users willing to implement their own concurrency control.\n\nEnable [parallel processing](https://abxbus.archivebox.io/concurrency/handlers-parallel) of handlers for better performance.  \nThe harsh tradeoff is less deterministic ordering as handler execution order will not be guaranteed when run in parallel.\n(It's very hard to write non-flaky/reliable applications when handler execution order is not guaranteed.)\n\n```python\n# Create bus with parallel handler execution\nbus = EventBus(event_handler_concurrency='parallel')\n\n# Multiple handlers run concurrently for each event\nbus.on('DataEvent', slow_handler_1)  # Takes 1 second\nbus.on('DataEvent', slow_handler_2)  # Takes 1 second\n\nstart = time.time()\nawait bus.emit(DataEvent())\n# Total time: ~1 second (not 2)\n```\n\n\u003cbr/\u003e\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003e🧩 Middlewares\u003c/strong\u003e\u003c/summary\u003e\n\n[Middlewares](https://abxbus.archivebox.io/integrations/middlewares) can observe or mutate the `EventResult` at each step, emit additional events, or trigger other side effects (metrics, retries, auth checks, etc.).\n\n```python\nfrom abxbus import EventBus\nfrom abxbus.middlewares import LoggerEventBusMiddleware, WALEventBusMiddleware, SQLiteHistoryMirrorMiddleware, OtelTracingMiddleware\n\nbus = EventBus(\n    name='MyBus',\n    middlewares=[\n        SQLiteHistoryMirrorMiddleware('./events.sqlite3'),\n        WALEventBusMiddleware('./events.jsonl'),\n        LoggerEventBusMiddleware('./events.log'),\n        OtelTracingMiddleware(),\n        # ...\n    ],\n)\n\nawait bus.emit(SecondEventAbc(some_key=\"banana\"))\n# will persist all events to sqlite + events.jsonl + events.log\n```\n\nBuilt-in middlewares you can import from `abxbus.middlewares.*`:\n\n- `AutoErrorEventMiddleware`: on handler error, fire-and-forget emits `OriginalEventTypeErrorEvent` with `{error, error_type}` (skips `*ErrorEvent`/`*ResultEvent` sources). Useful when downstream/remote consumers only see events and need explicit failure notifications.\n- `AutoReturnEventMiddleware`: on non-`None` handler return, fire-and-forget emits `OriginalEventTypeResultEvent` with `{data}` (skips `*ErrorEvent`/`*ResultEvent` sources). Useful for bridges/remote systems since handler return values do not cross bridge boundaries, but events do.\n- `AutoHandlerChangeEventMiddleware`: emits `BusHandlerRegisteredEvent({handler})` / `BusHandlerUnregisteredEvent({handler})` when handlers are added/removed via `.on()` / `.off()`.\n- `OtelTracingMiddleware`: emits OpenTelemetry spans for events and handlers with parent-child linking; can be exported to Sentry via Sentry's OpenTelemetry integration.\n- `WALEventBusMiddleware`: persists completed events to JSONL for replay/debugging.\n- `LoggerEventBusMiddleware`: writes event/handler transitions to stdout and optionally to file.\n- `SQLiteHistoryMirrorMiddleware`: mirrors event and handler snapshots into append-only SQLite `events_log` and `event_results_log` tables for auditing/debugging.\n\n#### Defining a custom middleware\n\nHandler middlewares subclass `EventBusMiddleware` and override whichever lifecycle hooks they need (`on_event_change`, `on_event_result_change`, `on_bus_handlers_change`):\n\n```python\nfrom abxbus.middlewares import EventBusMiddleware\n\nclass AnalyticsMiddleware(EventBusMiddleware):\n    async def on_event_result_change(self, eventbus, event, event_result, status):\n        if status == 'started':\n            await analytics_bus.emit(HandlerStartedAnalyticsEvent(event_id=event_result.event_id))\n        elif status == 'completed':\n            await analytics_bus.emit(\n                HandlerCompletedAnalyticsEvent(\n                    event_id=event_result.event_id,\n                    error=repr(event_result.error) if event_result.error else None,\n                )\n            )\n\n    async def on_bus_handlers_change(self, eventbus, handler, registered):\n        await analytics_bus.emit(\n            HandlerRegistryChangedEvent(handler_id=handler.id, registered=registered, bus=eventbus.name)\n        )\n```\n\n\u003cbr/\u003e\n\n---\n\n---\n\n\u003cbr/\u003e\n\n\u003c/details\u003e\n\n## 📚 API Documentation\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003e\u003ccode\u003eEventBus\u003c/code\u003e\u003c/strong\u003e\u003c/summary\u003e\n\nThe main event bus class that manages event processing and handler execution.\n\n```python\nEventBus(\n    name: str | None = None,\n    event_handler_concurrency: Literal['serial', 'parallel'] = 'serial',\n    event_handler_completion: Literal['all', 'first'] = 'all',\n    event_timeout: float | None = 60.0,\n    event_slow_timeout: float | None = 300.0,\n    event_handler_slow_timeout: float | None = 30.0,\n    event_handler_detect_file_paths: bool = True,\n    max_history_size: int | None = 50,\n    max_history_drop: bool = False,\n    middlewares: Sequence[EventBusMiddleware | type[EventBusMiddleware]] | None = None,\n)\n```\n\n**Parameters:**\n\n- `name`: Optional unique name for the bus (auto-generated if not provided)\n- `event_handler_concurrency`: Default handler execution mode for events on this bus: `'serial'` (default) or `'parallel'` (resolved at processing time when `event.event_handler_concurrency` is unset)\n- `event_handler_completion`: Handler completion mode for each event: `'all'` (default, wait for all handlers) or `'first'` (complete once first successful non-`None` result is available)\n- `event_timeout`: Default per-event timeout in seconds resolved at processing time when `event.event_timeout` is `None`\n- `event_slow_timeout`: Default slow-event warning threshold in seconds\n- `event_handler_slow_timeout`: Default slow-handler warning threshold in seconds\n- `event_handler_detect_file_paths`: Whether to auto-detect handler source file paths at registration time (slightly slower when enabled)\n- `max_history_size`: Maximum number of events to keep in history (default: 50, `None` = unlimited, `0` = keep only in-flight events and drop completed events immediately)\n- `max_history_drop`: If `True`, drop oldest history entries when full (even uncompleted events). If `False` (default), reject new emits once history reaches `max_history_size` (except when `max_history_size=0`, which never rejects on history size)\n- `middlewares`: Optional list of `EventBusMiddleware` subclasses or instances that hook into handler execution for analytics, logging, retries, etc. (see [Middlewares](#middlewares) for more info)\n\nTimeout precedence matches TS:\n\n- Effective handler timeout = `min(resolved_handler_timeout, event_timeout)` where `resolved_handler_timeout` resolves in order: `handler.handler_timeout` -\u003e `event.event_handler_timeout` -\u003e `bus.event_timeout`.\n- Slow handler warning threshold resolves in order: `handler.handler_slow_timeout` -\u003e `event.event_handler_slow_timeout` -\u003e `event.event_slow_timeout` -\u003e `bus.event_handler_slow_timeout` -\u003e `bus.event_slow_timeout`.\n\n#### `EventBus` Properties\n\n- `name`: The bus identifier\n- `id`: Unique UUID7 for this bus instance\n- `event_history`: Dict of all events the bus has seen by event_id (limited by `max_history_size`)\n- `events_pending`: List of events waiting to be processed\n- `events_started`: List of events currently being processed\n- `events_completed`: List of completed events\n- `all_instances`: Class-level WeakSet tracking all active EventBus instances (for memory monitoring)\n\n#### `EventBus` Methods\n\n##### `on(event_type: str | Type[BaseEvent], handler: Callable)`\n\nSubscribe a handler to events matching a specific event type or `'*'` for all events.\n\n```python\nbus.on('UserEvent', handler_func)  # By event type string\nbus.on(UserEvent, handler_func)    # By event class\nbus.on('*', handler_func)          # Wildcard - all events\n```\n\n##### `emit(event: BaseEvent) -\u003e BaseEvent`\n\nEnqueue an event for processing and return the pending `Event` immediately (synchronous).\n\n```python\nevent = bus.emit(MyEvent(data=\"test\"))\nresult = await event  # immediate-await path (queue-jumps when called inside a handler)\nresult_in_queue_order = await event.event_completed()  # always waits in normal queue order\n```\n\n**Note:** Queueing is unbounded. History pressure is controlled by `max_history_size` + `max_history_drop`:\n\n- `max_history_drop=True`: absorb new events and trim old history entries (even uncompleted events).\n- `max_history_drop=False`: raise `RuntimeError` when history is full.\n- `max_history_size=0`: keep pending/in-flight events only; completed events are immediately removed from history.\n\n##### `find(event_type: str | Literal['*'] | Type[BaseEvent], *, where: Callable[[BaseEvent], bool]=None, child_of: BaseEvent | None=None, past: bool | float | timedelta=True, future: bool | float=False, **event_fields) -\u003e BaseEvent | None`\n\nFind an event matching criteria in history and/or future. This is the recommended unified method for event lookup.\n\n**Parameters:**\n\n- `event_type`: The event type string, `'*'` wildcard, or model class to find\n- `where`: Predicate function for filtering (default: matches all)\n- `child_of`: Only match events that are descendants of this parent event\n- `past`: Controls history search behavior (default: `True`)\n  - `True`: search all history\n  - `False`: skip history search\n  - `float`/`timedelta`: search events from last N seconds only\n- `future`: Controls future wait behavior (default: `False`)\n  - `True`: wait forever for matching event\n  - `False`: don't wait for future events\n  - `float`: wait up to N seconds for matching event\n- `**event_fields`: Optional equality filters for any event fields (for example `event_status='completed'`, `user_id='u-1'`)\n\n```python\n# Default call is non-blocking history lookup (past=True, future=False)\nevent = await bus.find(ResponseEvent)\n\n# Find child of a specific parent event\nchild = await bus.find(ChildEvent, child_of=parent_event, future=5)\n\n# Wait only for future events (ignore history)\nevent = await bus.find(ResponseEvent, past=False, future=5)\n\n# Search recent history + optionally wait\nevent = await bus.find(ResponseEvent, past=5, future=5)\n\n# Filter by event metadata\ncompleted = await bus.find(ResponseEvent, event_status='completed')\n\n# Wildcard match across all event types\nany_completed = await bus.find('*', event_status='completed', past=True, future=False)\n```\n\n##### `event_is_child_of(event: BaseEvent, ancestor: BaseEvent) -\u003e bool`\n\nCheck if event is a descendant of ancestor (child, grandchild, etc.).\n\n```python\nif bus.event_is_child_of(child_event, parent_event):\n    print(\"child_event is a descendant of parent_event\")\n```\n\n##### `event_is_parent_of(event: BaseEvent, descendant: BaseEvent) -\u003e bool`\n\nCheck if event is an ancestor of descendant (parent, grandparent, etc.).\n\n```python\nif bus.event_is_parent_of(parent_event, child_event):\n    print(\"parent_event is an ancestor of child_event\")\n```\n\n##### `wait_until_idle(timeout: float | None=None)`\n\nWait until all events are processed and the bus is idle.\n\n```python\nawait bus.wait_until_idle()             # wait indefinitely until EventBus has finished processing all events\n\nawait bus.wait_until_idle(timeout=5.0)  # wait up to 5 seconds\n```\n\n##### `stop(timeout: float | None=None, clear: bool=False)`\n\nStop the event bus, optionally waiting for pending events and clearing memory.\n\n```python\nawait bus.stop(timeout=1.0)  # Graceful stop, wait up to 1sec for pending and active events to finish processing\nawait bus.stop()             # Immediate shutdown, aborts all pending and actively processing events\nawait bus.stop(clear=True)   # Stop and clear all event history and handlers to free memory\n```\n\n---\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003e\u003ccode\u003eBaseEvent\u003c/code\u003e\u003c/strong\u003e\u003c/summary\u003e\n\nBase class for all events. Subclass `BaseEvent` to define your own events.\n\nMake sure none of your own event data fields start with `event_` or `model_` to avoid clashing with `BaseEvent` or `pydantic` builtin attrs.\n\n#### `BaseEvent` Fields\n\n```python\nT_EventResultType = TypeVar('T_EventResultType', bound=Any, default=None)\n\nclass BaseEvent(BaseModel, Generic[T_EventResultType]):\n    # special config fields\n    event_id: str                # Unique UUID7 identifier, auto-generated if not provided\n    event_type: str              # Defaults to class name e.g. 'BaseEvent'\n    event_result_type: Any | None  # Pydantic model/python type to validate handler return values, defaults to T_EventResultType\n    event_version: str           # Defaults to '0.0.1' (override per class/instance for event payload versioning)\n    event_timeout: float | None = None # Event timeout in seconds (bus default resolved at processing time if None)\n    event_handler_timeout: float | None = None # Optional per-event handler timeout cap in seconds\n    event_handler_slow_timeout: float | None = None # Optional per-event slow-handler warning threshold\n    event_handler_concurrency: Literal['serial', 'parallel'] | None = None  # optional per-event handler scheduling override (None -\u003e bus default at processing time)\n    event_handler_completion: Literal['all', 'first'] | None = None  # optional per-event completion override (None -\u003e bus default at processing time)\n\n    # runtime state fields\n    event_status: Literal['pending', 'started', 'completed']  # event processing status (auto-set)\n    event_created_at: str        # Canonical ISO timestamp with 9 fractional digits (auto-set)\n    event_started_at: str | None # Set when first handler starts\n    event_completed_at: str | None # Set when event processing completes\n    event_parent_id: str | None  # Parent event ID that led to this event during handling (auto-set)\n    event_path: list[str]        # List of bus labels traversed, e.g. BusName#ab12 (auto-set)\n    event_results: dict[str, EventResult]   # Handler results {\u003chandler uuid\u003e: EventResult} (auto-set)\n    event_children: list[BaseEvent] # getter property to list any child events emitted during handling\n    event_bus: EventBus          # getter property to get the bus the event was emitted on\n\n    # payload fields\n    # ... subclass BaseEvent to add your own event payload fields here ...\n    # some_key: str\n    # some_other_key: dict[str, int]\n    # ...\n    # (they should not start with event_* to avoid conflict with special built-in fields)\n```\n\n#### `BaseEvent` Methods\n\n##### `await event`\n\nImmediate-await path for the `Event` object.\n\n- Outside a handler: waits for normal completion and returns the completed event.\n- Inside a handler: queue-jumps this child event so it can run immediately, then returns the completed event.\n\n```python\nevent = bus.emit(MyEvent())\ncompleted_event = await event\n\nraw_result_values = [(await event_result) for event_result in completed_event.event_results.values()]\n# equivalent to: completed_event.event_results_list()  (see below)\n```\n\n##### `event_completed() -\u003e Self`\n\nQueue-order await path for an event.\n\n- Never queue-jumps.\n- Waits until the event is completed by normal runloop queue order.\n\n```python\nevent = bus.emit(MyEvent())\ncompleted_event = await event.event_completed()\n```\n\n##### `first(timeout: float | None=None, *, raise_if_any: bool=False, raise_if_none: bool=False) -\u003e Any`\n\nSet `event_handler_completion='first'`, wait for completion, and return the first successful non-`None` handler result.\n\n```python\nevent = bus.emit(MyEvent())\nvalue = await event.first()\n```\n\n##### `reset() -\u003e Self`\n\nReturn a fresh event copy with runtime processing state reset back to pending.\n\n- Intended for re-emitting an already-seen event as a fresh event (for example after crossing a bridge boundary).\n- The original event object is not mutated, it returns a new copy with some fields reset.\n- A new UUIDv7 `event_id` is generated for the returned copy (to allow it to process as a separate event it needs a new unique uuid)\n- Runtime completion state is cleared (`event_results`, completion signal/flags, processed timestamp, emit context).\n\n##### `event_result_update(handler, eventbus: EventBus | None=None, **kwargs) -\u003e EventResult`\n\nCreate or update a single `EventResult` entry for a handler.\n\n- If no entry exists yet for the handler id, a pending result row is created.\n- Useful for deterministic seeding/rehydration before normal processing resumes.\n- Supports `status`, `result`, `error`, and `timeout` updates through `**kwargs`.\n\n```python\nseeded = event.event_result_update(handler=handler_entry, eventbus=bus, status='pending')\nseeded.update(status='completed', result='seeded')\n```\n\n##### `event_result(timeout: float | None=None, include: EventResultFilter=None, raise_if_any: bool=True, raise_if_none: bool=True) -\u003e Any`\n\nUtility method helper to execute all the handlers and return the first handler's raw result value.\n\n**Parameters:**\n\n- `timeout`: Maximum time to wait for handlers to complete (None = use default event timeout)\n- `include`: Filter function to include only specific results (default: only non-None, non-exception results)\n- `raise_if_any`: If `True`, raise exception if any handler raises any `Exception` (`default: True`)\n- `raise_if_none`: If `True`, raise exception if results are empty / all results are `None` or `Exception` (`default: True`)\n\n```python\n# by default it returns the first successful non-None result value\nresult = await event.event_result()\n\n# Get result from first handler that returns a string\nvalid_result = await event.event_result(include=lambda r: isinstance(r.result, str) and len(r.result) \u003e 100)\n\n# Get result but don't raise exceptions or error for 0 results, just return None\nresult_or_none = await event.event_result(raise_if_any=False, raise_if_none=False)\n```\n\n##### `event_results_list(timeout: float | None=None, include: EventResultFilter=None, raise_if_any: bool=True, raise_if_none: bool=True) -\u003e list[Any]`\n\nUtility method helper to get all raw result values in a list.\n\n**Parameters:**\n\n- `timeout`: Maximum time to wait for handlers to complete (None = use default event timeout)\n- `include`: Filter function to include only specific results (default: only non-None, non-exception results)\n- `raise_if_any`: If `True`, raise exception if any handler raises any `Exception` (`default: True`)\n- `raise_if_none`: If `True`, raise exception if results are empty / all results are `None` or `Exception` (`default: True`)\n\n```python\n# by default it returns all successful non-None result values\nresults = await event.event_results_list()\n# [result1, result2]\n\n# Only include results that are strings longer than 10 characters\nfiltered_results = await event.event_results_list(include=lambda r: isinstance(r.result, str) and len(r.result) \u003e 10)\n\n# Get all results without raising on errors\nall_results = await event.event_results_list(raise_if_any=False, raise_if_none=False)\n```\n\n`event_results_list()` is the canonical collection helper for multiple handler return values.\n\n##### `event_bus` (property)\n\nShortcut to get the `EventBus` that is currently processing this event. Can be used to avoid having to pass an `EventBus` instance to your handlers.\n\n```python\nbus = EventBus()\n\nasync def some_handler(event: MyEvent):\n    # Owned child work should use event.emit(...), which blocks parent completion.\n    child_event = await event.emit(ChildEvent())\n\n    # Use bus.emit(...) only for detached background work.\n    detached_child = bus.emit(ChildEvent())\n```\n\n---\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003e\u003ccode\u003eEventResult\u003c/code\u003e\u003c/strong\u003e\u003c/summary\u003e\n\nThe placeholder object that represents the pending result from a single handler executing an event.  \n`Event.event_results` contains a `dict[PythonIdStr, EventResult]` in the shape of `{handler_id: EventResult()}`.\n\nYou generally won't interact with this class directly—the bus instantiates and updates it for you—but its API is documented here for advanced integrations and custom emit loops.\n\n#### `EventResult` Fields\n\n```python\nclass EventResult(BaseModel):\n    id: str                    # Unique identifier\n    handler_id: str           # Handler function ID\n    handler_name: str         # Handler function name\n    eventbus_id: str          # Bus that executed this handler\n    eventbus_name: str        # Bus name\n\n    status: str               # 'pending', 'started', 'completed', 'error'\n    result: Any               # Handler return value\n    error: BaseException | None  # Captured exception if the handler failed\n\n    started_at: str | None      # Canonical ISO timestamp when handler started\n    completed_at: str | None    # Canonical ISO timestamp when handler completed\n    timeout: float | None            # Handler timeout in seconds\n    event_children: list[BaseEvent] # child events emitted during handler execution\n```\n\n#### `EventResult` Methods\n\n##### `await result`\n\nAwait the `EventResult` object directly to get the raw result value.\n\n```python\nhandler_result = event.event_results['handler_id']\nvalue = await handler_result  # Returns result or raises an exception if handler hits an error\n```\n\n- `run_handler(event, handler, *, eventbus, timeout, enter_handler_context, exit_handler_context, format_exception_for_log)`  \n  Low-level helper that runs the handler, updates timing/status fields, captures errors, and notifies its completion signal. `EventBus._run_handler()` (private/internal) delegates to this; you generally should not call either directly unless you are extending internals.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003e\u003ccode\u003eEventHandler\u003c/code\u003e\u003c/strong\u003e\u003c/summary\u003e\n\nSerializable metadata wrapper around a registered handler callable.\n\nYou usually get an `EventHandler` back from `bus.on(...)`, can pass it to `bus.off(...)`, and may see it in middleware hooks like `on_bus_handlers_change(...)`.\n\n#### `EventHandler` Fields\n\n```python\nclass EventHandler(BaseModel):\n    id: str                          # Stable handler identifier\n    handler_name: str                # Callable name\n    handler_file_path: str | None    # Source file path (if known)\n    handler_timeout: float | None    # Optional per-handler timeout override\n    handler_slow_timeout: float | None  # Optional \"slow handler\" threshold\n    handler_registered_at: str       # Registration timestamp (ISO string, 9 fractional digits)\n    event_pattern: str               # Registered event pattern (type name or '*')\n    eventbus_name: str               # Owning EventBus name\n    eventbus_id: str                 # Owning EventBus ID\n```\n\nThe raw callable is stored on `handler`, but is excluded from JSON serialization (`model_dump(mode='json', exclude={'handler'})`).\n\n#### `EventHandler` Properties and Methods\n\n- `label` (property): Short display label like `my_handler#abcd`.\n- `model_dump(mode='json', exclude={'handler'}) -\u003e dict[str, Any]`: JSON-compatible metadata dict (callable excluded).\n- `from_json_dict(data, handler=None) -\u003e EventHandler`: Rebuilds metadata; optional callable reattachment.\n- `from_callable(...) -\u003e EventHandler`: Build a new handler entry from a callable plus bus/pattern metadata.\n\n---\n\n\u003c/details\u003e\n\n## 🏃 Performance (Python)\n\n```bash\nuv run tests/performance_runtime.py   # run the performance test suite in python\n```\n\n| Runtime | 1 bus x 50k events x 1 handler   | 500 buses x 100 events x 1 handler | 1 bus x 1 event x 50k parallel handlers | 1 bus x 50k events x 50k one-off handlers | Worst case (N buses x N events x N handlers) |\n| ------- | -------------------------------- | ---------------------------------- | --------------------------------------- | ----------------------------------------- | -------------------------------------------- |\n| Python  | `0.179ms/event`, `0.235kb/event` | `0.191ms/event`, `0.191kb/event`   | `0.035ms/handler`, `8.164kb/handler`    | `0.255ms/event`, `0.185kb/event`          | `0.351ms/event`, `5.867kb/event`             |\n\n\u003cbr/\u003e\n\n---\n\n---\n\n\u003cbr/\u003e\n\n## 👾 Development\n\nSet up the python development environment using `uv`:\n\n```bash\ngit clone https://github.com/ArchiveBox/abxbus \u0026\u0026 cd abxbus\n\n# Create virtual environment with Python 3.12\nuv venv --python 3.12\n\n# Activate virtual environment (varies by OS)\nsource .venv/bin/activate  # On Unix/macOS\n# or\n.venv\\Scripts\\activate  # On Windows\n\n# Install dependencies\nuv sync --dev --all-extras\n```\n\nRecommended once per clone:\n\n```bash\nprek install           # install pre-commit hooks\nprek run --all-files   # run pre-commit hooks on all files manually\n```\n\n```bash\n# Run linter \u0026 type checker\nuv run ruff check --fix\nuv run ruff format\nuv run pyright\n\n# Run all tests\nuv run pytest -vxs --full-trace tests/\n\n# Run specific test file\nuv run pytest tests/test_eventbus.py\n\n# Run Python perf suite\nuv run tests/performance_runtime.py\n\n# Run the entire lint+test+examples+perf suite for both python and ts\n./test.sh\n```\n\n\u003e For AbxBus-TS development see the `abxbus-ts/README.md` `# Development` section.\n\n## 🔗 Inspiration\n\n- https://www.cosmicpython.com/book/chapter_08_events_and_message_bus.html#message_bus_diagram ⭐️\n- https://developer.mozilla.org/en-US/docs/Web/API/EventTarget ⭐️\n- https://github.com/sindresorhus/emittery ⭐️, https://github.com/EventEmitter2/EventEmitter2, https://github.com/vitaly-t/sub-events\n- https://github.com/pytest-dev/pluggy ⭐️\n- https://github.com/teamhide/fastapi-event ⭐️\n- https://github.com/ethereum/lahja ⭐️\n- https://github.com/enricostara/eventure ⭐️\n- https://github.com/akhundMurad/diator ⭐️\n- https://github.com/n89nanda/pyeventbus\n- https://github.com/iunary/aioemit\n- https://github.com/dboslee/evently\n- https://github.com/faust-streaming/faust\n- https://github.com/ArcletProject/Letoderea\n- https://github.com/seanpar203/event-bus\n- https://github.com/n89nanda/pyeventbus\n- https://github.com/nicolaszein/py-async-bus\n- https://github.com/AngusWG/simple-event-bus\n- https://www.joeltok.com/posts/2021-03-building-an-event-bus-in-python/\n- See more here: https://abxbus.archivebox.io/further-reading/similar-projects\n\n\u003e [!TIP]\n\u003e **Don't like working with event-driven interfaces?**\n\u003e Check out our [`abxbus.events_suck`](https://abxbus.archivebox.io/further-reading/events-suck) wrapper utils that can help wrap events workflows in a simpler imperative API...\n\n---\n\n\u003e [🍃 Main Documentation](https://abxbus.archivebox.io) | [🧠 DeepWiki | Get AI Help](https://deepwiki.com/ArchiveBox/abxbus) | [🐍 PyPI Package](https://pypi.org/project/abxbus) | [📦 NPM Package](https://npmjs.com/package/abxbus) | [\u003c/\u003e Github](https://github.com/ArchiveBox/abxbus)\n\u003e\n\u003e \u003cimg width=\"400\" alt=\"image\" src=\"https://github.com/user-attachments/assets/cedb5a2e-0643-4240-9a3d-5f27cb8b5741\" /\u003e\u003cimg width=\"400\" alt=\"image\" src=\"https://github.com/user-attachments/assets/3ee0ee8c-8322-449f-979b-5c99ba6bd960\" /\u003e\n\n## 🏛️ License\n\nThis project is licensed under the MIT License.\n\nThis repo is a fork that adds many new features and performance enhancements over the [original project named `bubus`](https://github.com/browser-use/bubus), which was built to power the [Browser-Use Agent](https://github.com/browser-use/browser-use/tree/main/browser_use/browser/watchdogs) (but has since gone stale).\n\nTimeline:\n\n- 2025-06 `v1.0.1`: Original library released https://github.com/browser-use/bubus\n- 2025-10 `v1.5.1`: Browser-Use v0.6.0 released, first version powered by `bubus`\n- 2025-11 `v1.7.1`: `bubus` forked to `pirate/bbus` temporarily; `ContextVar` support, `Middlewares`, and `bus.find()` added\n- 2026-01 `v2.3.2`: `bubus-ts` Typescript implementation released, cross-compatible with Python version (now `abxbus-ts`)\n- 2026-03 `v2.4.1`: Fork renamed from `pirate/bbus -\u003e ArchiveBox/abxbus`; added dual `CJS`/`ESM` support, bugfixes and perf improvmeents\n- 2026-03 `v2.4.9`: Added `update()`, `uninstall()`, and support for `uv`, `gem`, `cargo`, `go get`, `docker`, and `nix`. Used in new [`abx-dl`](https://github.com/ArchiveBox/abx-dl) project and [ArchiveBox](https://github.com/ArchiveBox/ArchiveBox).\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Farchivebox%2Fabxbus","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Farchivebox%2Fabxbus","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Farchivebox%2Fabxbus/lists"}