Skip to content

Add TypeScript implementation and comprehensive test suite#23

Closed
pirate wants to merge 115 commits intobrowser-use:mainfrom
pirate:claude/test-parallel-events-3720c
Closed

Add TypeScript implementation and comprehensive test suite#23
pirate wants to merge 115 commits intobrowser-use:mainfrom
pirate:claude/test-parallel-events-3720c

Conversation

@pirate
Copy link
Contributor

@pirate pirate commented Feb 11, 2026

Summary

This PR adds a complete TypeScript/JavaScript port of the bubus event bus library alongside the existing Python implementation, along with extensive test coverage for both languages. The TypeScript implementation maintains API parity with Python while adapting to JavaScript idioms and async patterns.

Key Changes

TypeScript Implementation

  • Core event bus system (bubus-ts/src/):
    • event_bus.ts: Full EventBus implementation with concurrency control, handler management, and event lifecycle
    • base_event.ts: BaseEvent class with Zod schema validation and event metadata tracking
    • event_result.ts: EventResult tracking for handler execution outcomes
    • event_handler.ts: EventHandler registration and execution management
    • lock_manager.ts: Concurrency primitives (semaphores, locks) supporting global-serial, bus-serial, and parallel modes
    • async_context.ts: AsyncLocalStorage integration for context propagation
    • logging.ts: Event tree visualization utilities
    • types.ts: TypeScript type definitions and utilities

Test Coverage

  • Python tests (tests/):

    • test_find.py: 1500+ lines testing event discovery with past/future/child_of semantics
    • test_context_propagation.py: ContextVar propagation through dispatch and handlers
    • test_event_history_mirroring.py: SQLite middleware for event history persistence
    • test_event_result_standalone.py: EventResult model validation
    • Enhanced test_eventbus.py and test_handler_timeout.py with additional scenarios
  • TypeScript tests (bubus-ts/tests/):

    • eventbus_basics.test.ts: Constructor, defaults, and basic operations
    • find.test.ts: Event discovery with past/future/child_of filtering
    • comprehensive_patterns.test.ts: Forwarding, async/sync dispatch, parent tracking
    • timeout.test.ts: Handler timeouts and error metadata
    • locking.test.ts: Concurrency modes and FIFO correctness
    • context_propagation.test.ts: AsyncLocalStorage context propagation
    • performance.test.ts: 50k event throughput benchmarks
    • Additional tests for error handling, forwarding, parent-child relationships, and typed results

Infrastructure & Documentation

  • Middleware system (bubus/middlewares.py):

    • EventBusMiddleware: Lifecycle hooks for event and result state transitions
    • LoggerEventBusMiddleware: Structured logging integration
    • WALEventBusMiddleware: Write-ahead logging for durability
    • SQLiteHistoryMirrorMiddleware: Persistent event history snapshots
  • Event history (bubus/event_history.py): Bounded history container with configurable capacity

  • TypeScript tooling:

    • package.json: npm configuration with zod, uuid, and test dependencies
    • tsconfig.json: Strict TypeScript configuration
    • eslint.config.js, prettier.config.js: Code quality tools
    • GitHub Actions workflow for npm publishing
  • Documentation:

    • Updated README.md with multi-language positioning
    • bubus-ts/README.md: TypeScript-specific differences and gotchas

Model & Service Updates

  • bubus/models.py:

    • Added EventStatus enum (PENDING, STARTED, COMPLETED)
    • Added _event_dispatch_context for ContextVar capture at dispatch time
    • Added _event_is_complete_flag for completion tracking
    • Enhanced type hints and imports
  • bubus/service.py:

    • Added EventBusMiddleware base class with lifecycle hooks
    • Integrated middleware system for event/result state transitions
    • Enhanced event history management with EventHistory class
    • Improved type annotations throughout

Notable Implementation Details

  • Queue-jump semantics: event.done() in TypeScript (vs await event in Python) triggers immediate processing on all buses where the event is queued
  • Cross-bus forwarding: Events

https://claude.ai/code/session_01FFkpZPxvvoexboUF2Uo4oQ


Summary by cubic

Adds a complete TypeScript port of the bubus event bus with API parity to Python, plus a comprehensive cross-language test suite. Also adds a handler-level retry decorator, first() for racing handlers, middleware hooks, a pluggable event history backend, unified find(), and dispatch-time context propagation in Python.

  • New Features

    • TypeScript/JS implementation: BaseEvent, EventBus, handler/results, global-serial/bus-serial/parallel modes, AsyncLocalStorage context propagation, queue-jump via event.done(), cross-bus forwarding, log tree utilities, event.first() with handler cancellation, and a retry() decorator/HOF with semaphore limits and scope (global/class/instance).
    • Extensive tests for both languages: find(), timeouts, locking/FIFO, parent-child, forwarding, error handling, first(), retry(), type inference, and performance across Node/Bun/Deno/Chromium (50k+ events).
    • Python upgrades: EventBusMiddleware (Logger/WAL/SQLite mirror), simplified middleware API, pluggable EventHistory backend, EventStatus StrEnum, ContextVar propagation at dispatch, unified bus.find(), and consistent event_children naming.
    • Tooling/CI: strict TS config (tsc/eslint/prettier), zod/uuid deps, new GitHub Actions for TS tests and npm publishing, and example/demo code (examples/ and ui/ monitor).
  • Migration

    • TypeScript: use await event.done() instead of await event; event.first() returns the first non-undefined handler result and cancels the rest; retry() on class methods works with bus.on(handler.bind(this)); unbound functions fall back to global semaphore scope.
    • Python: replace expect/get_or_dispatch with bus.find(); EventStatus is a StrEnum; adopt event_children naming; register middleware via bus.use(...); set max_history_size=None for unbounded history.

Written for commit 7c3f883. Summary will update on new commits.

pirate and others added 30 commits October 15, 2025 12:51
Add support for middlewares to hook into event bus handler lifecycle
implement swappable EventHistory storage backend
Updated the description to clarify the library's functionality and similarities to JS event systems.
Revise README description for bubus library
pirate and others added 27 commits February 9, 2026 14:39
Verifies that multiple events can be emitted and raced to completion
via Promise.all([e1.done(), e2.done(), e3.done()]) across three
scenarios: parallel concurrency, bus-serial concurrency, and inside
a handler via queue-jump.

https://claude.ai/code/session_01FFkpZPxvvoexboUF2Uo4oQ
@pirate pirate force-pushed the claude/test-parallel-events-3720c branch from 46ab953 to 7c3f883 Compare February 11, 2026 08:51
Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

8 issues found across 65 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="bubus-ts/tests/error_handling.test.ts">

<violation number="1" location="bubus-ts/tests/error_handling.test.ts:172">
P2: The test excludes forwarding handlers by checking `handler_name !== 'dispatch'`, but bound functions are named `"bound dispatch"`. This can select the forwarding handler instead of the bus A handler, making the test brittle.</violation>
</file>

<file name="bubus-ts/src/lock_manager.ts">

<violation number="1" location="bubus-ts/src/lock_manager.ts:71">
P2: AsyncSemaphore.release() decrements in_use before waking a waiter, allowing a new acquire() to slip in and exceed the semaphore size. This can break mutual exclusion under contention.</violation>
</file>

<file name="bubus-ts/prettier.config.js">

<violation number="1" location="bubus-ts/prettier.config.js:8">
P2: `prettier.config.js` uses ESM `export default` but the repo lacks a `package.json` with `"type": "module"`, so Prettier will treat this file as CommonJS and fail to load it. Use CommonJS export or add `type: module`.</violation>
</file>

<file name="bubus-ts/package.json">

<violation number="1" location="bubus-ts/package.json:28">
P2: Test script uses POSIX-style inline env var assignment, which fails under Windows cmd.exe (npm default), preventing tests from running cross-platform.</violation>
</file>

<file name="README.md">

<violation number="1" location="README.md:16">
P3: README Python quickstart uses `bus.emit` and `SomeEvent({some_data: 132})`, but Python `EventBus` exposes `dispatch()` and events are constructed with keyword args. This snippet is not runnable and will mislead users.</violation>
</file>

<file name="bubus/service.py">

<violation number="1" location="bubus/service.py:628">
P2: Backpressure uses event_history for pending counts, but cleanup_event_history can evict started/pending events when history exceeds max_history_size. That undercounts inflight load and allows dispatch to accept more than the 100‑pending limit, defeating the backpressure mechanism.</violation>

<violation number="2" location="bubus/service.py:654">
P2: PENDING lifecycle hook is fired in a background task while STARTED/COMPLETED are awaited, so middleware can observe STARTED/COMPLETED before PENDING. This introduces out‑of‑order lifecycle events and a race in middleware state transitions.</violation>
</file>

<file name="bubus-ts/src/types.ts">

<violation number="1" location="bubus-ts/src/types.ts:67">
P2: `getStringTypeName` reads `_def.type`, but Zod stores the schema kind in `_def.typeName`. This makes `kind` empty for normal schemas and forces `'unknown'` for all type names, breaking `event_result_type` inference.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.


// bus_a's handler succeeded
const bus_a_result = Array.from(event.event_results.values()).find(
(r) => r.eventbus_name === 'ErrorForwardA' && r.handler_name !== 'dispatch'
Copy link

@cubic-dev-ai cubic-dev-ai bot Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The test excludes forwarding handlers by checking handler_name !== 'dispatch', but bound functions are named "bound dispatch". This can select the forwarding handler instead of the bus A handler, making the test brittle.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At bubus-ts/tests/error_handling.test.ts, line 172:

<comment>The test excludes forwarding handlers by checking `handler_name !== 'dispatch'`, but bound functions are named `"bound dispatch"`. This can select the forwarding handler instead of the bus A handler, making the test brittle.</comment>

<file context>
@@ -0,0 +1,221 @@
+
+  // bus_a's handler succeeded
+  const bus_a_result = Array.from(event.event_results.values()).find(
+    (r) => r.eventbus_name === 'ErrorForwardA' && r.handler_name !== 'dispatch'
+  )
+  assert.ok(bus_a_result)
</file context>
Fix with Cubic

if (this.size === Infinity) {
return
}
this.in_use = Math.max(0, this.in_use - 1)
Copy link

@cubic-dev-ai cubic-dev-ai bot Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: AsyncSemaphore.release() decrements in_use before waking a waiter, allowing a new acquire() to slip in and exceed the semaphore size. This can break mutual exclusion under contention.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At bubus-ts/src/lock_manager.ts, line 71:

<comment>AsyncSemaphore.release() decrements in_use before waking a waiter, allowing a new acquire() to slip in and exceed the semaphore size. This can break mutual exclusion under contention.</comment>

<file context>
@@ -0,0 +1,377 @@
+    if (this.size === Infinity) {
+      return
+    }
+    this.in_use = Math.max(0, this.in_use - 1)
+    const next = this.waiters.shift()
+    if (next) {
</file context>
Fix with Cubic

printWidth: 140,
}

export default config
Copy link

@cubic-dev-ai cubic-dev-ai bot Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: prettier.config.js uses ESM export default but the repo lacks a package.json with "type": "module", so Prettier will treat this file as CommonJS and fail to load it. Use CommonJS export or add type: module.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At bubus-ts/prettier.config.js, line 8:

<comment>`prettier.config.js` uses ESM `export default` but the repo lacks a `package.json` with `"type": "module"`, so Prettier will treat this file as CommonJS and fail to load it. Use CommonJS export or add `type: module`.</comment>

<file context>
@@ -0,0 +1,8 @@
+  printWidth: 140,
+}
+
+export default config
</file context>
Fix with Cubic

"lint": "pnpm run format:check && eslint . && pnpm run typecheck",
"format": "prettier --write .",
"format:check": "prettier --check .",
"test": "NODE_OPTIONS='--expose-gc' node --expose-gc --test --import tsx tests/**/*.test.ts",
Copy link

@cubic-dev-ai cubic-dev-ai bot Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Test script uses POSIX-style inline env var assignment, which fails under Windows cmd.exe (npm default), preventing tests from running cross-platform.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At bubus-ts/package.json, line 28:

<comment>Test script uses POSIX-style inline env var assignment, which fails under Windows cmd.exe (npm default), preventing tests from running cross-platform.</comment>

<file context>
@@ -0,0 +1,63 @@
+    "lint": "pnpm run format:check && eslint . && pnpm run typecheck",
+    "format": "prettier --write .",
+    "format:check": "prettier --check .",
+    "test": "NODE_OPTIONS='--expose-gc' node --expose-gc --test --import tsx tests/**/*.test.ts",
+    "prepack": "pnpm run build",
+    "release:dry-run": "pnpm publish --access public --dry-run --no-git-checks",
</file context>
Fix with Cubic

bubus/service.py Outdated
self._active_event_ids.add(event.event_id)
if self.middlewares:
loop = asyncio.get_running_loop()
loop.create_task(self._on_event_change(event, EventStatus.PENDING))
Copy link

@cubic-dev-ai cubic-dev-ai bot Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: PENDING lifecycle hook is fired in a background task while STARTED/COMPLETED are awaited, so middleware can observe STARTED/COMPLETED before PENDING. This introduces out‑of‑order lifecycle events and a race in middleware state transitions.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At bubus/service.py, line 654:

<comment>PENDING lifecycle hook is fired in a background task while STARTED/COMPLETED are awaited, so middleware can observe STARTED/COMPLETED before PENDING. This introduces out‑of‑order lifecycle events and a race in middleware state transitions.</comment>

<file context>
@@ -565,71 +648,244 @@ def dispatch(self, event: T_ExpectedEvent) -> T_ExpectedEvent:
+                self._active_event_ids.add(event.event_id)
+                if self.middlewares:
+                    loop = asyncio.get_running_loop()
+                    loop.create_task(self._on_event_change(event, EventStatus.PENDING))
+                if logger.isEnabledFor(logging.INFO):
+                    logger.info(
</file context>
Fix with Cubic

bubus/service.py Outdated
queue_size = self.event_queue.qsize() if self.event_queue else 0
pending_in_history = sum(1 for e in self.event_history.values() if e.event_status in ('pending', 'started'))
pending_in_history = 0
for existing_event in self.event_history.values():
Copy link

@cubic-dev-ai cubic-dev-ai bot Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Backpressure uses event_history for pending counts, but cleanup_event_history can evict started/pending events when history exceeds max_history_size. That undercounts inflight load and allows dispatch to accept more than the 100‑pending limit, defeating the backpressure mechanism.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At bubus/service.py, line 628:

<comment>Backpressure uses event_history for pending counts, but cleanup_event_history can evict started/pending events when history exceeds max_history_size. That undercounts inflight load and allows dispatch to accept more than the 100‑pending limit, defeating the backpressure mechanism.</comment>

<file context>
@@ -546,7 +624,12 @@ def dispatch(self, event: T_ExpectedEvent) -> T_ExpectedEvent:
             queue_size = self.event_queue.qsize() if self.event_queue else 0
-            pending_in_history = sum(1 for e in self.event_history.values() if e.event_status in ('pending', 'started'))
+            pending_in_history = 0
+            for existing_event in self.event_history.values():
+                if not self._is_event_complete_fast(existing_event):
+                    pending_in_history += 1
</file context>
Fix with Cubic

visited.add(value)

const def = (value as unknown as { _def?: Record<string, unknown> })._def ?? {}
const kind = typeof def.type === 'string' ? def.type : ''
Copy link

@cubic-dev-ai cubic-dev-ai bot Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: getStringTypeName reads _def.type, but Zod stores the schema kind in _def.typeName. This makes kind empty for normal schemas and forces 'unknown' for all type names, breaking event_result_type inference.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At bubus-ts/src/types.ts, line 67:

<comment>`getStringTypeName` reads `_def.type`, but Zod stores the schema kind in `_def.typeName`. This makes `kind` empty for normal schemas and forces `'unknown'` for all type names, breaking `event_result_type` inference.</comment>

<file context>
@@ -0,0 +1,94 @@
+    visited.add(value)
+
+    const def = (value as unknown as { _def?: Record<string, unknown> })._def ?? {}
+    const kind = typeof def.type === 'string' ? def.type : ''
+    if (!kind) return 'unknown'
+
</file context>
Fix with Cubic

README.md Outdated
It "just works" with an intuitive, but powerful event JSON format + dispatch API that's consistent across both languages and scales consistently from one even up to millions:
```python
bus.on(SomeEvent, some_function)
bus.emit(SomeEvent({some_data: 132}))
Copy link

@cubic-dev-ai cubic-dev-ai bot Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: README Python quickstart uses bus.emit and SomeEvent({some_data: 132}), but Python EventBus exposes dispatch() and events are constructed with keyword args. This snippet is not runnable and will mislead users.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At README.md, line 16:

<comment>README Python quickstart uses `bus.emit` and `SomeEvent({some_data: 132})`, but Python `EventBus` exposes `dispatch()` and events are constructed with keyword args. This snippet is not runnable and will mislead users.</comment>

<file context>
@@ -1,12 +1,35 @@
+It "just works" with an intuitive, but powerful event JSON format + dispatch API that's consistent across both languages and scales consistently from one even up to millions:
+```python
+bus.on(SomeEvent, some_function)
+bus.emit(SomeEvent({some_data: 132}))
+```
+
</file context>
Suggested change
bus.emit(SomeEvent({some_data: 132}))
bus.dispatch(SomeEvent(some_data=132))
Fix with Cubic

@pirate pirate closed this Feb 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants