Skip to content

Conversation

@theodorDiaconu
Copy link
Collaborator

@theodorDiaconu theodorDiaconu commented Nov 30, 2025

Summary by CodeRabbit

  • New Features

    • Optional parallel execution for same-priority event listeners with preserved batch ordering and a builder API toggle; runtime cycle-detection for event emissions; improved client error decoding for typed server errors.
  • Documentation

    • New docs and examples for parallel-event semantics and a validation-schema example; minor wording/formatting refinements.
  • Bug Fixes / Behavior

    • HTTP exposure guards now return 403 FORBIDDEN for non-exposed resources.
  • Tests

    • Extensive test coverage for parallel batching, ordering, propagation, error aggregation, cycle detection, and registry behavior.
  • Chores

    • Formatting/tooling and script refinements.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Nov 30, 2025

Walkthrough

Adds an optional parallel flag to events and implements batched parallel listener execution (per-order concurrency), plus registry, executors, interceptor composition, and runtime cycle-detection; threads parallel through builders/definitions, expands tests for parallel/failure semantics, and updates docs and tooling.

Changes

Cohort / File(s) Summary
Event Definition & Builder
src/types/event.ts, src/definers/defineEvent.ts, src/definers/builders/event.ts
Introduced parallel?: boolean on IEventDefinition; propagate eventConfig.parallel onto returned IEvent; add parallel(enabled?: boolean) fluent builder method and thread flag into builder state.
Core Types & Contracts
src/models/event/types.ts
New event types: IListenerStorage, IEventHandlerOptions, EventEmissionInterceptor, HookExecutionInterceptor, HookExecutor, IEmissionFrame, and HandlerOptionsDefaults.
Listener Registry
src/models/event/ListenerRegistry.ts
New ListenerRegistry with per-event/global listeners, sorted insertion, merged caching, cache invalidation, and createListener helper.
Emission Executors
src/models/event/EmissionExecutor.ts
New executeSequentially, executeInParallel, and shouldExecuteListener; batching by order, parallel batch execution, filtering, propagation checks between batches, and aggregated error enrichment.
Cycle Detection
src/models/event/CycleContext.ts
New CycleContext using platform AsyncLocalStorage (when available) for runtime emission cycle detection and hook scoping; exposes runEmission and runHook.
Interceptor Composition
src/models/event/InterceptorPipeline.ts
Added composeInterceptors to compose interceptor arrays into a single callable wrapper.
EventManager Refactor
src/models/EventManager.ts
Reworked to use ListenerRegistry, InterceptorPipeline, EmissionExecutor, and CycleContext; removed inline listener/merge helpers and re-exported event-related types for compatibility.
Tests — Events & Registry
src/__tests__/models/EventManagerParallel.test.ts, src/__tests__/models/EventManagerFailure.test.ts, src/__tests__/models/EventManager.test.ts, src/__tests__/models/event/CycleContext.test.ts, src/__tests__/models/event/ListenerRegistry.test.ts
New and expanded tests covering parallel batches, propagation semantics, source exclusion, filtering, aggregated errors, cycle detection, and registry caching behavior.
Tests — Builder
src/__tests__/definers/event.builder.test.ts
Tests for builder parallel flag, payloadSchema, meta, tags, and internal filePath symbol.
Docs & Examples
README.md, AI.md, examples/fastify-mikroorm/readmes/runner-AI.md
Added "Parallel Event Execution" docs and .parallel(true) guidance; added IValidationSchema<T> snippet and minor textual/formatting tweaks.
Node HTTP & Error Handling
src/node/http-smart-client.model.ts, src/node/http-mixed-client.ts
Add optional errorRegistry typed as Map<string, IErrorHelper<any>>; introduce rethrowTyped and propagate registry into mixed client to unwrap/rethrow typed errors.
Node Exposure & Tests
src/node/exposure/allowList.ts, src/node/__tests__/**/*.test.ts, src/node/exposure/__tests__/requestHandlers.auth-and-missing.unit.test.ts
Change non-exposed responses from 404 to 403 in allow-list guards; refactor tests to stronger typed mock req/res helpers and updated expectations.
Prettier & Chatmode
prettier.config.js, .github/chatmodes/code-reviewer.chatmode.md
Add Prettier ignore for .github/**/*.md; add new chatmode config file.
Scripts
scripts/check-large-files.sh
Enhance script with token estimate, relative path output, and styled table header.

Sequence Diagram(s)

sequenceDiagram
    participant Emitter as Emitter
    participant Manager as EventManager
    participant Cycle as CycleContext
    participant Registry as ListenerRegistry
    participant Executor as EmissionExecutor

    Emitter->>Manager: emit(event, { source, payload })
    Manager->>Cycle: runEmission(frame, source, processEmission)
    Cycle->>Manager: (invokes) processEmission()
    Manager->>Registry: getListenersForEmit(event)
    Registry-->>Manager: ordered listeners
    Manager->>Manager: group listeners into batches by order
    alt event.parallel == true
        Manager->>Executor: executeInParallel(batch0, event)
        Executor->>Executor: run listeners in batch concurrently (Promise.allSettled)
        Executor-->>Manager: batch0 results / aggregated errors
        opt no fatal error
            Manager->>Executor: executeInParallel(nextBatch, event)
        end
    else
        Manager->>Executor: executeSequentially(allListeners, event)
        Executor-->>Manager: sequential results/errors (stops on propagation)
    end
    Manager-->>Cycle: emission finished
    Cycle-->>Emitter: resolve / reject (AggregateError if multiple)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Attention areas:
    • src/models/EventManager.ts — concurrency flow, interceptor composition, and re-export compatibility.
    • src/models/event/EmissionExecutor.ts — batching, Promise.allSettled semantics, error augmentation/aggregation.
    • src/models/event/ListenerRegistry.ts — sorted insert, merge correctness, cache invalidation.
    • src/models/event/CycleContext.ts — AsyncLocalStorage usage and correct cycle detection/error messaging.
    • Timing-sensitive tests — ensure deterministic delays and avoid flakiness.

Possibly related PRs

  • Event cycle-prevention solution. #67 — overlapping event cycle-detection work (runtimeCycleDetection / CycleContext / event cycle errors).
  • [WIP] v4 #62 — related event-system refactor touching EventManager, types, registry, and executor concepts.
  • Builders & tunnels #72 — related changes to fluent builders and event builder surface (intersects with new parallel API).

Poem

🐰
I hopped through queues both swift and slow,
Batches danced together, then let the next go,
Errors piled like pebbles, counted and neat,
A tiny "stop" whispered — later hops stayed in their seat,
Carrots for tests, and docs tidy as a row 🥕✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.70% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: implement EventManager with parallel event execution' accurately and clearly describes the main change—adding parallel execution capability to the EventManager, which is the primary focus across the comprehensive changeset.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/parallel-events

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
examples/fastify-mikroorm/readmes/runner-AI.md (1)

644-666: Section ordering appears incorrect.

The "## Validation" header at line 644 is immediately followed by "## Event Cycle Safety" at line 646, but the validation content (interface definition at lines 660-666 and examples at lines 668-697) appears after the Event Cycle Safety section. This makes the document structure confusing.

Consider reordering so that the Event Cycle Safety section is placed before or after the complete Validation section, or make Event Cycle Safety a subsection (###) if it's meant to be nested under Validation.

🧹 Nitpick comments (1)
src/models/EventManager.ts (1)

555-598: Consider extracting shared listener execution logic.

The source-skip check (lines 565-567) and filter application (lines 569-571) are duplicated between executeSequentially and executeInParallel. Consider extracting a helper method like shouldExecuteListener(listener, event) to reduce duplication and ensure consistent behavior if the logic evolves.

+ private shouldExecuteListener(listener: IListenerStorage, event: IEventEmission<any>): boolean {
+   // Skip handlers that identify themselves as the source of this event
+   if (listener.id && listener.id === event.source) {
+     return false;
+   }
+   return !listener.filter || listener.filter(event);
+ }

Then use if (this.shouldExecuteListener(listener, event)) in both executors.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 13075e3 and 10a8f72.

📒 Files selected for processing (5)
  • examples/fastify-mikroorm/readmes/runner-AI.md (4 hunks)
  • src/__tests__/models/EventManagerParallel.test.ts (1 hunks)
  • src/definers/defineEvent.ts (1 hunks)
  • src/models/EventManager.ts (3 hunks)
  • src/types/event.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
src/**/*.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

src/**/*.ts: Format code with Prettier for all TypeScript source files (npx prettier --write src)
Check code formatting with Prettier for all TypeScript source files (npx prettier --check src)

Files:

  • src/__tests__/models/EventManagerParallel.test.ts
  • src/models/EventManager.ts
  • src/types/event.ts
  • src/definers/defineEvent.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Type-strictness is a must; avoid type casting unless absolutely necessary

Files:

  • src/__tests__/models/EventManagerParallel.test.ts
  • src/models/EventManager.ts
  • src/types/event.ts
  • src/definers/defineEvent.ts
**/*.{ts,tsx,js,jsx,mjs,cjs}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx,mjs,cjs}: Use only relative imports
Place imports at the top of the file by default; inline imports only for specific optimizations

Files:

  • src/__tests__/models/EventManagerParallel.test.ts
  • src/models/EventManager.ts
  • src/types/event.ts
  • src/definers/defineEvent.ts
🧠 Learnings (2)
📚 Learning: 2025-09-26T12:31:39.948Z
Learnt from: CR
Repo: bluelibs/runner PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-26T12:31:39.948Z
Learning: Applies to {AI.md,README.md} : Update AI.md and README.md when introducing features/enhancements/changes

Applied to files:

  • examples/fastify-mikroorm/readmes/runner-AI.md
📚 Learning: 2025-09-01T12:21:34.022Z
Learnt from: CR
Repo: bluelibs/runner PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-09-01T12:21:34.022Z
Learning: Applies to {README.md,AI.md} : Update README.md and AI.md after any feature or change

Applied to files:

  • examples/fastify-mikroorm/readmes/runner-AI.md
🧬 Code graph analysis (2)
src/__tests__/models/EventManagerParallel.test.ts (3)
src/models/EventManager.ts (1)
  • EventManager (65-599)
src/types/event.ts (1)
  • IEvent (48-58)
src/definers/defineEvent.ts (1)
  • defineEvent (11-30)
src/models/EventManager.ts (1)
src/types/event.ts (1)
  • IEventEmission (63-97)
🔇 Additional comments (10)
src/types/event.ts (1)

37-41: LGTM!

The new parallel property is well-documented and correctly typed as an optional boolean. The JSDoc clearly communicates the behavioral implication regarding execution order.

src/definers/defineEvent.ts (1)

22-22: LGTM!

The parallel property is correctly propagated from the event configuration to the returned event object, maintaining consistency with how other properties like id and tags are explicitly assigned.

src/models/EventManager.ts (2)

183-191: LGTM!

The branching logic cleanly delegates to the appropriate executor based on the event's parallel configuration. The propagation closure is correctly passed to executeSequentially to maintain the existing stop-propagation behavior.


528-547: LGTM!

The executeSequentially method correctly implements sequential execution with proper propagation checking, source listener exclusion, and filter application.

src/__tests__/models/EventManagerParallel.test.ts (6)

10-17: LGTM!

Good test setup with runtimeCycleDetection enabled and a parallel event definition. The beforeEach ensures clean state for each test.


19-44: LGTM!

This test correctly validates that within a single batch (same order), faster listeners complete before slower ones, demonstrating true parallel execution.


46-81: LGTM!

Good test coverage for batch sequencing - validates that batches with different orders execute sequentially while listeners within the same batch execute in parallel.


83-136: LGTM!

Excellent coverage of propagation semantics in parallel mode. The tests correctly validate that stopPropagation() blocks subsequent batches (lines 83-108) but does not affect other listeners in the same parallel batch (lines 110-136).


138-158: LGTM!

Good error propagation test. Validates that errors thrown within parallel listeners correctly propagate to the caller via Promise.all.


159-212: LGTM!

Good coverage for source listener exclusion and filter functionality in parallel execution mode. Both tests correctly validate that these features work consistently with the parallel execution path.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
src/__tests__/models/EventManagerParallel.test.ts (1)

14-60: Optional: factor out the delay helper to reduce repetition

The delay helper is redefined in multiple tests; you could move it to a small shared utility within this file (or a common test helper) to avoid duplication and keep each test focused purely on behavior setup and assertions.

Also applies to: 41-76

src/models/EventManager.ts (1)

526-540: Centralized listener filtering/source-exclusion is a good improvement; consider tightening event typing

Moving source-id exclusion and filter evaluation into shouldExecuteListener and using it from both sequential and parallel executors reduces duplication and keeps behavior consistent. One possible improvement is to make executeSequentially, executeInParallel, and shouldExecuteListener generic over the payload type (e.g., IEventEmission<TPayload> instead of IEventEmission<any>) to better respect the strict typing guideline without changing runtime behavior.

Also applies to: 548-566, 598-607

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1aace33 and 53ec266.

📒 Files selected for processing (3)
  • src/__tests__/models/EventManagerParallel.test.ts (1 hunks)
  • src/models/EventManager.ts (2 hunks)
  • src/types/event.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/types/event.ts
🧰 Additional context used
📓 Path-based instructions (3)
src/**/*.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

src/**/*.ts: Format code with Prettier for all TypeScript source files (npx prettier --write src)
Check code formatting with Prettier for all TypeScript source files (npx prettier --check src)

Files:

  • src/__tests__/models/EventManagerParallel.test.ts
  • src/models/EventManager.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Type-strictness is a must; avoid type casting unless absolutely necessary

Files:

  • src/__tests__/models/EventManagerParallel.test.ts
  • src/models/EventManager.ts
**/*.{ts,tsx,js,jsx,mjs,cjs}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx,mjs,cjs}: Use only relative imports
Place imports at the top of the file by default; inline imports only for specific optimizations

Files:

  • src/__tests__/models/EventManagerParallel.test.ts
  • src/models/EventManager.ts
🧬 Code graph analysis (2)
src/__tests__/models/EventManagerParallel.test.ts (2)
src/models/EventManager.ts (1)
  • EventManager (65-608)
src/definers/defineEvent.ts (1)
  • defineEvent (11-30)
src/models/EventManager.ts (1)
src/types/event.ts (1)
  • IEventEmission (63-97)
🔇 Additional comments (3)
src/__tests__/models/EventManagerParallel.test.ts (1)

5-12: Comprehensive coverage of parallel execution semantics looks solid

The suite nicely exercises parallel vs sequential batches, propagation stop across/within batches, interceptor interaction, error propagation, source-id skipping, and per-listener filters for parallel events. The expectations align with the new EventManager.executeInParallel and shouldExecuteListener behavior, so from a behavior standpoint this file looks good.

Also applies to: 14-236

src/models/EventManager.ts (2)

150-176: Emit branching on eventDefinition.parallel cleanly separates sequential vs parallel paths

The new parallel-based branch reuses the same listener selection and interceptor chain while delegating execution strategy to executeSequentially vs executeInParallel. The early allListeners.length === 0 check also guards executeInParallel from empty arrays, so this integration looks correct and non-breaking for non-parallel events.

Also applies to: 183-191


519-588: Parallel batch execution and propagation semantics match the intended behavior

executeInParallel correctly:

  • Groups listeners by order, running each group as a batch.
  • Executes each batch concurrently, waiting for completion before moving to the next batch.
  • Re-checks event.isPropagationStopped() between batches so a stopper in one batch prevents subsequent batches, while not cancelling peers in the same batch.

This aligns with the new tests (same-order parallelism, sequential batches, stopPropagation behavior) and should behave well with interceptors that stop propagation before reaching the executor.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
src/__tests__/models/EventManagerParallel.test.ts (1)

17-43: Same-order parallelism test is clear; consider extracting delay helper

This test cleanly distinguishes true parallel execution from sequential execution by asserting ["fast", "slow"] even though the slow listener is registered first; it will reliably fail if same‑order listeners are run sequentially. To avoid repeating the delay helper and keep tests DRY, you could lift it to the describe scope and reuse it here and in the next test.

 describe("EventManager Parallel Execution", () => {
   let eventManager: EventManager;
   let parallelEvent: IEvent<string>;
+
+  const delay = (ms: number) =>
+    new Promise((resolve) => setTimeout(resolve, ms));
@@
   it("should execute listeners with the same order in parallel", async () => {
     const results: string[] = [];
-    const delay = (ms: number) =>
-      new Promise((resolve) => setTimeout(resolve, ms));

(And similarly drop the local delay in the batch test below.) Optionally, you could also reduce the delay to a smaller value (e.g., 5–10 ms) to keep the suite fast as it grows.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 53ec266 and 8912b7d.

📒 Files selected for processing (1)
  • src/__tests__/models/EventManagerParallel.test.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
src/**/*.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

src/**/*.ts: Format code with Prettier for all TypeScript source files (npx prettier --write src)
Check code formatting with Prettier for all TypeScript source files (npx prettier --check src)

Files:

  • src/__tests__/models/EventManagerParallel.test.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Type-strictness is a must; avoid type casting unless absolutely necessary

Files:

  • src/__tests__/models/EventManagerParallel.test.ts
**/*.{ts,tsx,js,jsx,mjs,cjs}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx,mjs,cjs}: Use only relative imports
Place imports at the top of the file by default; inline imports only for specific optimizations

Files:

  • src/__tests__/models/EventManagerParallel.test.ts
🧬 Code graph analysis (1)
src/__tests__/models/EventManagerParallel.test.ts (3)
src/models/EventManager.ts (1)
  • EventManager (65-608)
src/types/event.ts (1)
  • IEvent (48-58)
src/definers/defineEvent.ts (1)
  • defineEvent (11-30)
🔇 Additional comments (7)
src/__tests__/models/EventManagerParallel.test.ts (7)

1-15: Setup correctly exercises parallel event definitions under cycle detection

The imports are purely relative, and the shared beforeEach uses defineEvent with parallel: true plus EventManager({ runtimeCycleDetection: true }), which is a good baseline for all the following parallel semantics tests. No changes needed here.

As per coding guidelines, this adheres to the relative‑import and type‑strictness expectations.


45-81: Batch sequencing test accurately validates inter-batch ordering

This test nicely complements the previous one: within batch 1 (order 0) you still assert ["batch1-fast", "batch1-slow"], and then you verify that batch 2 (order 1) only runs after batch 1 completes, yielding ["batch1-fast", "batch1-slow", "batch2"]. This will catch regressions where batches are not awaited sequentially or where intra-batch execution becomes inadvertently sequential.


83-136: Propagation tests clearly distinguish between inter-batch and intra-batch semantics

The pair of tests here do a good job of locking in the contract:

  • Stopping propagation in a batch with order 0 prevents later batches (order 1) from running at all.
  • Stopping propagation within a batch does not prevent other listeners in that same batch from executing.

Together they ensure executeInParallel performs propagation checks between batches rather than per-listener, which matches the described EventManager semantics.


138-165: Interceptor propagation test correctly asserts short-circuiting at entry

By having an interceptor call event.stopPropagation() and still invoking await next(event), this test ensures that EventManager checks isPropagationStopped() before scheduling any listeners, even when interceptors choose to forward the call. The expectation of results staying empty captures that contract well. No changes needed.


167-187: Parallel error propagation test is minimal but sufficient

This test verifies that an error thrown by one listener in a parallel batch rejects emit with that error, which is the critical behaviour to lock in for callers. Given other tests already cover ordering semantics, this focused assertion is reasonable as-is; you can always extend it later to also assert that successful listeners still get executed if you need stronger guarantees.


188-211: Source self-invocation skipping is well covered

Using id: sourceId on one listener and passing sourceId as the third argument to emit is a clear way to validate that shouldExecuteListener skips the source listener while allowing others in the same batch to run. The expected ["should-run"] result precisely targets that behaviour.


213-241: Filter semantics test cleanly validates conditional execution in parallel mode

The two listeners with opposing filter predicates ensure that parallel execution respects listener-level filters: the false filter never fires, while the true filter does, resulting in exactly ["filtered-in"]. This is a succinct and robust assertion of filter handling in the parallel path.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (4)
.github/chatmodes/code-reviewer.chatmode.md (1)

55-58: Minor style improvement: Vary sentence structure in constraints.

The static analysis tool flagged three successive sentences beginning with "Do not". Consider varying the phrasing for better readability.

 Mode-Specific Constraints:
 
 - Do not provide feedback on code that is not provided by the user.
-- Do not engage in discussions unrelated to code review.
-- Do not provide feedback on code that is not in a programming language that you are familiar with.
-- Do not provide feedback on code that is not in a format that you can understand.
+- Avoid engaging in discussions unrelated to code review.
+- Only provide feedback on code in programming languages you are familiar with.
+- Ensure the code is in a format you can understand before providing feedback.
README.md (1)

476-491: Consider adding a payload schema to the example for clarity.

The example references event.data in doWork(event.data) (Line 483), but the event has no payloadSchema, making event.data type void. Adding a schema would make the example more realistic.

-const parallelEvent = r.event("app.events.parallel").parallel(true).build();
+const parallelEvent = r
+  .event("app.events.parallel")
+  .payloadSchema<{ id: string }>({ parse: (v) => v })
+  .parallel(true)
+  .build();

 r.hook("app.hooks.first")
   .on(parallelEvent)
   .order(0)
   .run(async (event) => {
-    await doWork(event.data);
+    await doWork(event.data); // { id: string }
   })
   .build();
src/__tests__/models/EventManagerParallel.test.ts (2)

17-43: Timing-based test may be flaky under load.

The test relies on a 50ms delay to demonstrate parallel execution. While reasonable, this could occasionally fail in slow CI environments.

Consider using a deterministic approach with shared state or increasing the delay margin if flakiness is observed.


214-225: Jest's fail() function may not be available in all configurations.

The fail() function is not part of Jest's core API in newer versions. Consider using an alternative pattern:

-    try {
-      await eventManager.emit(parallelEvent, "data", "test");
-      fail("Should have thrown");
-    } catch (err: any) {
+    let caughtError: any;
+    try {
+      await eventManager.emit(parallelEvent, "data", "test");
+      expect.fail("Should have thrown");
+    } catch (err) {
+      caughtError = err;
+    }
+    expect(caughtError).toBeDefined();
+    expect(caughtError.name).toBe("AggregateError");

Alternatively, use expect.assertions(n) at the start and let the test fail if assertions aren't hit.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8912b7d and 6e934bb.

📒 Files selected for processing (7)
  • .github/chatmodes/code-reviewer.chatmode.md (1 hunks)
  • AI.md (2 hunks)
  • README.md (1 hunks)
  • prettier.config.js (1 hunks)
  • src/__tests__/models/EventManagerParallel.test.ts (1 hunks)
  • src/definers/builders/event.ts (5 hunks)
  • src/models/EventManager.ts (2 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
src/**/*.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

src/**/*.ts: Format code with Prettier for all TypeScript source files (npx prettier --write src)
Check code formatting with Prettier for all TypeScript source files (npx prettier --check src)

Files:

  • src/models/EventManager.ts
  • src/definers/builders/event.ts
  • src/__tests__/models/EventManagerParallel.test.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Type-strictness is a must; avoid type casting unless absolutely necessary

Files:

  • src/models/EventManager.ts
  • src/definers/builders/event.ts
  • src/__tests__/models/EventManagerParallel.test.ts
**/*.{ts,tsx,js,jsx,mjs,cjs}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx,mjs,cjs}: Use only relative imports
Place imports at the top of the file by default; inline imports only for specific optimizations

Files:

  • src/models/EventManager.ts
  • prettier.config.js
  • src/definers/builders/event.ts
  • src/__tests__/models/EventManagerParallel.test.ts
{README.md,AI.md}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Update README.md and AI.md after any feature or change

Files:

  • README.md
  • AI.md
{AI.md,README.md}

📄 CodeRabbit inference engine (AGENTS.md)

Update AI.md and README.md when introducing features/enhancements/changes

Files:

  • README.md
  • AI.md
🧠 Learnings (1)
📚 Learning: 2025-09-26T12:31:39.948Z
Learnt from: CR
Repo: bluelibs/runner PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-26T12:31:39.948Z
Learning: Applies to {AI.md,README.md} : Update AI.md and README.md when introducing features/enhancements/changes

Applied to files:

  • README.md
🧬 Code graph analysis (3)
src/models/EventManager.ts (1)
src/types/event.ts (1)
  • IEventEmission (63-97)
src/definers/builders/event.ts (2)
src/types/event.ts (1)
  • IEventDefinition (28-42)
src/types/tag.ts (1)
  • TagType (83-86)
src/__tests__/models/EventManagerParallel.test.ts (4)
src/models/EventManager.ts (1)
  • EventManager (65-640)
src/types/event.ts (1)
  • IEvent (48-58)
src/definers/defineEvent.ts (1)
  • defineEvent (11-30)
src/definers/builders/event.ts (1)
  • event (100-100)
🪛 LanguageTool
README.md

[style] ~470-~470: As an alternative to the over-used intensifier ‘really’, consider replacing this phrase.
Context: ... }) .build(); ``` > runtime: "'A really good office messenger.' That's me in rollerb...

(EN_WEAK_ADJECTIVE)

.github/chatmodes/code-reviewer.chatmode.md

[style] ~57-~57: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...discussions unrelated to code review. - Do not provide feedback on code that is no...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)


[style] ~58-~58: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ... language that you are familiar with. - Do not provide feedback on code that is no...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)

🔇 Additional comments (16)
src/definers/builders/event.ts (3)

39-46: LGTM! Well-documented parallel API.

The JSDoc clearly explains the behavior: listeners with the same order run concurrently within a batch, and batches execute sequentially. The default true for enabled provides ergonomic usage.


72-75: LGTM! Clean implementation following builder pattern.

The parallel() method correctly clones state with the new flag and returns a new builder instance, consistent with the existing fluent API methods.


95-95: LGTM! Correct default value.

Using undefined as the initial value allows distinguishing between "parallel not configured" vs "parallel explicitly disabled", which is appropriate for optional configuration.

README.md (2)

472-498: LGTM! Clear documentation of parallel execution semantics.

The section effectively explains the parallel execution model with a practical example. The execution semantics bullet points cover important edge cases like AggregateError and stopPropagation behavior between batches.


500-505: LGTM! Well-documented execution semantics.

The bullet points clearly explain the parallel execution guarantees, error aggregation behavior, and the important caveat that stopPropagation() only works between batches—not within a parallel batch.

AI.md (2)

18-19: LGTM! TOC additions for HTTP client documentation.


156-161: LGTM! Concise parallel execution guidance.

The bullet points effectively summarize the parallel execution semantics in a token-friendly format, covering batch behavior, error aggregation, propagation rules, and failure handling.

src/models/EventManager.ts (4)

183-191: LGTM! Clean dispatch to parallel/sequential executors.

The branching logic correctly delegates based on eventDefinition.parallel, with the sequential path properly passing the propagation-stopped callback. The separation of concerns between execution strategies improves maintainability.


526-540: LGTM! Sequential execution is clean and correct.

The implementation properly checks propagation status before each listener and delegates filtering to shouldExecuteListener. The early break on propagation stop is the expected behavior for sequential execution.


630-639: LGTM! Clean extraction of listener filtering logic.

The method correctly handles both source-exclusion (preventing self-invocation) and optional filter conditions. Good refactoring to centralize this logic for use by both sequential and parallel executors.


590-595: The current implementation correctly uses manual AggregateError construction because the project targets ES2020 (per tsconfig.json). Native AggregateError was introduced in ES2021, which is not available in the project's target environment. The manual construction with the as any type cast is necessary and appropriate for ES2020 compatibility.

Likely an incorrect or invalid review comment.

src/__tests__/models/EventManagerParallel.test.ts (5)

1-15: LGTM! Well-structured test setup.

The test file properly imports dependencies and sets up a fresh EventManager with runtimeCycleDetection: true before each test. The parallel event is correctly defined with parallel: true.


45-136: LGTM! Comprehensive batch and propagation tests.

These tests correctly validate:

  1. Sequential execution across batches with parallel execution within batches
  2. Propagation stopping prevents subsequent batches from running
  3. Propagation stop within a batch doesn't affect other listeners in the same batch

Good coverage of the core parallel execution semantics.


227-280: LGTM! Good coverage of listener filtering scenarios.

The tests for source exclusion and filter functions correctly validate the shouldExecuteListener logic. Both scenarios are well-structured and verify the expected behavior.


282-342: LGTM! Edge cases are well covered.

Good test coverage for:

  • Empty listeners array (graceful no-op)
  • Single listener (no batching overhead)
  • All different orders (effectively sequential execution)

These ensure the parallel execution handles boundary conditions correctly.


344-370: LGTM! Error propagation correctly stops subsequent batches.

This test validates that when a batch throws an error, subsequent batches do not execute. The assertion correctly verifies both the error propagation and that batch 1 never ran.

trailingComma: "all",
tabWidth: 2,
singleQuote: false,
ignoreList: [".github/**/*.md"],
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Prettier ignoreList configuration option prettier.config.js documentation 2024 2025

💡 Result:

Short answer: Prettier does not support an ignoreList (or similar) option inside prettier.config.js as of 2024–2025. Use .prettierignore or the CLI/--ignore-path behavior instead.

Sources:

  • Prettier docs — Ignoring files (.prettierignore and ignore behavior). [1]
  • Prettier docs — Configuration and CLI options (mentions --ignore-path and config file types). [2]
  • Prettier GitHub issue requesting an “ignore paths” config option (proposal, not implemented). [3]

References:
[1] https://prettier.io/docs/next/ignore/
[2] https://prettier.io/docs/configuration/ and https://prettier.io/docs/next/cli/
[3] prettier/prettier#4708


ignoreList is not a valid Prettier configuration option.

Prettier does not support ignoreList in prettier.config.js. Use a .prettierignore file in the project root to ignore files instead.

Create a .prettierignore file with:

.github/**/*.md

Or remove the invalid option from prettier.config.js:

 module.exports = {
   trailingComma: "all",
   tabWidth: 2,
   singleQuote: false,
-  ignoreList: [".github/**/*.md"],
   overrides: [
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ignoreList: [".github/**/*.md"],
module.exports = {
trailingComma: "all",
tabWidth: 2,
singleQuote: false,
overrides: [
🤖 Prompt for AI Agents
In prettier.config.js around line 5, the configuration uses an invalid option
`ignoreList`; Prettier does not support this key. Remove the `ignoreList:
[".github/**/*.md"],` entry from prettier.config.js, and instead create a
.prettierignore file at the project root containing `.github/**/*.md` (or add
that pattern to an existing .prettierignore) to achieve the intended ignore
behavior.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
src/models/EventManager.ts (2)

542-647: Consider using native AggregateError if available.

The parallel execution logic is well-implemented with proper batching, error handling, and metadata annotation. However, consider using the native AggregateError constructor (available in modern Node.js and browsers) instead of manually creating an error object and setting its name.

Apply this diff to use native AggregateError when available:

       if (errors.length > 0) {
         if (errors.length === 1) {
           throw errors[0];
         }
-        // Create a composite error that contains all failures and their listener ids on each error object
-        const aggregateError = new Error(
-          `${errors.length} listeners failed in parallel batch`,
-        );
-        (aggregateError as any).errors = errors;
-        aggregateError.name = "AggregateError";
-        throw aggregateError;
+        // Use native AggregateError if available, otherwise polyfill
+        if (typeof AggregateError !== "undefined") {
+          throw new AggregateError(
+            errors,
+            `${errors.length} listeners failed in parallel batch`,
+          );
+        } else {
+          // Fallback for environments without native AggregateError
+          const aggregateError = new Error(
+            `${errors.length} listeners failed in parallel batch`,
+          ) as any;
+          aggregateError.errors = errors;
+          aggregateError.name = "AggregateError";
+          throw aggregateError;
+        }
       }

This eliminates the type cast and uses the standard error type when available.


574-590: Improve type safety in error annotation.

The error annotation logic uses type casting that could be more type-safe. Consider defining a proper interface for annotated errors.

Add a type definition near the top of the file and update the annotation function:

// Near other type definitions (after imports)
interface AnnotatedError extends Error {
  listenerId?: string;
  listenerOrder?: number;
}

Then update the annotateError function:

       const annotateError = (
         reason: unknown,
         listener: IListenerStorage,
-      ) => {
+      ): AnnotatedError => {
         const base =
           reason && typeof reason === "object"
-            ? (reason as any)
-            : (new Error(String(reason)) as any);
+            ? (reason as AnnotatedError)
+            : (new Error(String(reason)) as AnnotatedError);

         if (base.listenerId === undefined) {
           base.listenerId = listener.id;
         }
         if (base.listenerOrder === undefined) {
           base.listenerOrder = listener.order;
         }

-        return base as Error & { listenerId?: string; listenerOrder?: number };
+        return base;
       };

This improves type safety and aligns with the coding guideline to avoid unnecessary type casting.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6e934bb and f0683b1.

📒 Files selected for processing (3)
  • src/__tests__/definers/event.builder.test.ts (1 hunks)
  • src/__tests__/models/EventManager.test.ts (1 hunks)
  • src/models/EventManager.ts (2 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
src/**/*.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

src/**/*.ts: Format code with Prettier for all TypeScript source files (npx prettier --write src)
Check code formatting with Prettier for all TypeScript source files (npx prettier --check src)

Files:

  • src/__tests__/models/EventManager.test.ts
  • src/__tests__/definers/event.builder.test.ts
  • src/models/EventManager.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Type-strictness is a must; avoid type casting unless absolutely necessary

Files:

  • src/__tests__/models/EventManager.test.ts
  • src/__tests__/definers/event.builder.test.ts
  • src/models/EventManager.ts
**/*.{ts,tsx,js,jsx,mjs,cjs}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx,mjs,cjs}: Use only relative imports
Place imports at the top of the file by default; inline imports only for specific optimizations

Files:

  • src/__tests__/models/EventManager.test.ts
  • src/__tests__/definers/event.builder.test.ts
  • src/models/EventManager.ts
🧬 Code graph analysis (2)
src/__tests__/models/EventManager.test.ts (2)
src/index.ts (1)
  • defineEvent (48-48)
src/define.ts (1)
  • defineEvent (5-5)
src/__tests__/definers/event.builder.test.ts (1)
src/index.ts (1)
  • r (64-76)
🔇 Additional comments (8)
src/__tests__/models/EventManager.test.ts (3)

268-327: LGTM! Comprehensive test for parallel error aggregation.

This test thoroughly validates the parallel execution error handling, including:

  • Multiple concurrent failures aggregated into AggregateError
  • Listener metadata (listenerId, listenerOrder) properly attached to errors
  • Propagation stop after batch failure (order: 1 listener doesn't run)
  • Mixed success/failure scenarios within a batch

329-350: LGTM! Single error annotation validated correctly.

This test confirms that when only one listener fails in a parallel batch, the error is thrown directly (not wrapped in AggregateError) with proper listener metadata attached.


352-373: LGTM! Primitive error handling covered.

Good coverage of the edge case where a listener throws a non-Error value (primitive string). The test validates that the error is properly wrapped and annotated with listener metadata.

src/__tests__/definers/event.builder.test.ts (2)

4-28: LGTM! Builder API for parallel execution well-tested.

The test validates:

  • Parallel flag propagation through the builder chain
  • Default behavior when parallel() is called without arguments (defaults to true)
  • Explicit parallel(false) setting
  • Proper integration with other builder methods (payloadSchema, meta)
  • Internal symbolFilePath assignment

30-58: LGTM! Tag behavior thoroughly tested.

Comprehensive coverage of tag management:

  • Append behavior (default)
  • Override behavior with explicit { override: true }
  • Default options handling with empty object {}
src/models/EventManager.ts (3)

183-191: LGTM! Clean delegation to execution strategies.

The branching logic properly delegates to the appropriate executor based on the parallel flag, passing the necessary context for propagation control in the sequential case.


519-540: LGTM! Sequential execution correctly implemented.

The method properly:

  • Checks propagation before each listener
  • Uses the extracted shouldExecuteListener helper for source/filter checks
  • Maintains backward compatibility with existing behavior

649-666: LGTM! Helper method properly encapsulates listener execution checks.

The shouldExecuteListener method cleanly extracts the source exclusion and filter logic, making it reusable across both sequential and parallel execution paths.

…d modularity and maintainability

- Moved event listener management to ListenerRegistry for better separation of concerns.
- Introduced CycleContext for handling cycle detection in event emissions.
- Simplified event emission execution with dedicated functions for sequential and parallel execution.
- Enhanced interceptor handling with a utility function to compose interceptors.
- Updated tests to cover new structures and functionalities in ListenerRegistry and CycleContext.
- Removed deprecated code and interfaces to streamline the EventManager class.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (7)
src/__tests__/models/event/CycleContext.test.ts (1)

3-12: Consider expanding test coverage for CycleContext.

The test correctly verifies the disabled path, but CycleContext has significant logic that remains untested:

  • runEmission with cycle detection
  • Enabled mode when AsyncLocalStorage is available
  • The safeReEmitBySameHook bypass logic
  • Error throwing via eventCycleError
src/__tests__/models/event/ListenerRegistry.test.ts (1)

5-5: Avoid as any type casting per coding guidelines.

Multiple uses of as any for event objects. Consider defining a minimal test interface that satisfies the registry's requirements.

+interface TestEvent {
+  id: string;
+  exclude: boolean;
+}
+
 describe("ListenerRegistry", () => {
   it("getListenersForEmit respects excludeFromGlobal hook", () => {
-    const registry = new ListenerRegistry((event: any) => event.exclude === true);
+    const registry = new ListenerRegistry((event: TestEvent) => event.exclude === true);
     // ...
-    const includeEvent = { id: eventId, exclude: false } as any;
-    const excludeEvent = { id: eventId, exclude: true } as any;
+    const includeEvent: TestEvent = { id: eventId, exclude: false };
+    const excludeEvent: TestEvent = { id: eventId, exclude: true };

Also applies to: 14-15, 30-31

src/models/event/EmissionExecutor.ts (1)

69-79: Consider using native AggregateError if targeting ES2021+.

The manual construction works but native AggregateError provides better stack traces and is recognized by error handling tools.

     if (errors.length > 0) {
       if (errors.length === 1) {
         throw errors[0];
       }
-      const aggregateError = new Error(
+      throw new AggregateError(
+        errors,
         `${errors.length} listeners failed in parallel batch`,
       );
-      (aggregateError as any).errors = errors;
-      aggregateError.name = "AggregateError";
-      throw aggregateError;
     }

If ES2021 isn't guaranteed, the current approach is acceptable.

src/models/event/CycleContext.ts (1)

10-22: Minor: Cache getPlatform() result to avoid repeated calls.

getPlatform() is called three times in the constructor. While likely cheap, caching improves clarity.

   constructor(runtimeCycleDetection: boolean) {
-    if (getPlatform().hasAsyncLocalStorage() && runtimeCycleDetection) {
+    const platform = getPlatform();
+    if (platform.hasAsyncLocalStorage() && runtimeCycleDetection) {
       this.emissionStack =
-        getPlatform().createAsyncLocalStorage<IEmissionFrame[]>();
+        platform.createAsyncLocalStorage<IEmissionFrame[]>();
       this.currentHookIdContext =
-        getPlatform().createAsyncLocalStorage<string>();
+        platform.createAsyncLocalStorage<string>();
       this.isEnabled = true;
src/models/EventManager.ts (3)

31-34: Consider refactoring tests to avoid exposing internal registry references.

The direct exposure of listeners, globalListeners, and cachedMergedListeners from the registry creates tight coupling. While the comments acknowledge this is for backward compatibility, consider incrementally updating tests to use public methods like hasListeners() or adding test-specific public accessors instead of direct field references.

Also applies to: 54-57


105-111: Potential code duplication with registry method.

The logic here manually checks excludeFromGlobal and selects listeners, which duplicates the logic in registry.getListenersForEmit(). Consider using:

-    const excludeFromGlobal =
-      globalTags.excludeFromGlobalHooks.exists(eventDefinition);
-
-    // Choose listeners: if globals are excluded, only use event-specific listeners
-    const allListeners = excludeFromGlobal
-      ? this.listeners.get(eventDefinition.id) || []
-      : this.getCachedMergedListeners(eventDefinition.id);
+    const allListeners = this.registry.getListenersForEmit(eventDefinition);

This centralizes the listener selection logic and improves maintainability.


282-288: Consider simplifying the cycle context conditional.

The conditional logic works correctly but could be more concise:

-  if (this.cycleContext.isEnabled) {
-    return await this.cycleContext.runHook(hook.id, () =>
-      executeWithInterceptors(hook, event),
-    );
-  }
-
-  return await executeWithInterceptors(hook, event);
+  return this.cycleContext.isEnabled
+    ? await this.cycleContext.runHook(hook.id, () => executeWithInterceptors(hook, event))
+    : await executeWithInterceptors(hook, event);

Or keep the if but remove the early return:

+  let result: any;
   if (this.cycleContext.isEnabled) {
-    return await this.cycleContext.runHook(hook.id, () =>
+    result = await this.cycleContext.runHook(hook.id, () =>
       executeWithInterceptors(hook, event),
     );
+  } else {
+    result = await executeWithInterceptors(hook, event);
   }
-
-  return await executeWithInterceptors(hook, event);
+  return result;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f0683b1 and edd1b21.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (8)
  • src/__tests__/models/event/CycleContext.test.ts (1 hunks)
  • src/__tests__/models/event/ListenerRegistry.test.ts (1 hunks)
  • src/models/EventManager.ts (8 hunks)
  • src/models/event/CycleContext.ts (1 hunks)
  • src/models/event/EmissionExecutor.ts (1 hunks)
  • src/models/event/InterceptorPipeline.ts (1 hunks)
  • src/models/event/ListenerRegistry.ts (1 hunks)
  • src/models/event/types.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
src/**/*.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

src/**/*.ts: Format code with Prettier for all TypeScript source files (npx prettier --write src)
Check code formatting with Prettier for all TypeScript source files (npx prettier --check src)

Files:

  • src/models/event/InterceptorPipeline.ts
  • src/models/event/CycleContext.ts
  • src/__tests__/models/event/ListenerRegistry.test.ts
  • src/models/event/types.ts
  • src/__tests__/models/event/CycleContext.test.ts
  • src/models/event/ListenerRegistry.ts
  • src/models/EventManager.ts
  • src/models/event/EmissionExecutor.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Type-strictness is a must; avoid type casting unless absolutely necessary

Files:

  • src/models/event/InterceptorPipeline.ts
  • src/models/event/CycleContext.ts
  • src/__tests__/models/event/ListenerRegistry.test.ts
  • src/models/event/types.ts
  • src/__tests__/models/event/CycleContext.test.ts
  • src/models/event/ListenerRegistry.ts
  • src/models/EventManager.ts
  • src/models/event/EmissionExecutor.ts
**/*.{ts,tsx,js,jsx,mjs,cjs}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx,mjs,cjs}: Use only relative imports
Place imports at the top of the file by default; inline imports only for specific optimizations

Files:

  • src/models/event/InterceptorPipeline.ts
  • src/models/event/CycleContext.ts
  • src/__tests__/models/event/ListenerRegistry.test.ts
  • src/models/event/types.ts
  • src/__tests__/models/event/CycleContext.test.ts
  • src/models/event/ListenerRegistry.ts
  • src/models/EventManager.ts
  • src/models/event/EmissionExecutor.ts
🧬 Code graph analysis (5)
src/models/event/CycleContext.ts (3)
src/models/event/types.ts (1)
  • IEmissionFrame (68-71)
src/platform/index.ts (1)
  • getPlatform (18-24)
src/errors.ts (1)
  • eventCycleError (146-153)
src/__tests__/models/event/ListenerRegistry.test.ts (1)
src/models/event/ListenerRegistry.ts (2)
  • ListenerRegistry (14-142)
  • createListener (144-152)
src/__tests__/models/event/CycleContext.test.ts (1)
src/models/event/CycleContext.ts (1)
  • CycleContext (5-58)
src/models/EventManager.ts (5)
src/models/event/types.ts (3)
  • IListenerStorage (17-25)
  • EventEmissionInterceptor (42-45)
  • HookExecutionInterceptor (50-54)
src/models/event/ListenerRegistry.ts (2)
  • ListenerRegistry (14-142)
  • createListener (144-152)
src/models/event/CycleContext.ts (1)
  • CycleContext (5-58)
src/models/event/EmissionExecutor.ts (2)
  • executeInParallel (26-98)
  • executeSequentially (10-24)
src/models/event/InterceptorPipeline.ts (1)
  • composeInterceptors (5-18)
src/models/event/EmissionExecutor.ts (2)
src/models/event/types.ts (1)
  • IListenerStorage (17-25)
src/types/event.ts (1)
  • IEventEmission (63-97)
🔇 Additional comments (17)
src/__tests__/models/event/ListenerRegistry.test.ts (1)

36-47: LGTM!

Good test for cache invalidation lifecycle - verifies the getter starts true, becomes false after adding a global listener, and returns to true after cache rebuild.

src/models/event/InterceptorPipeline.ts (1)

1-18: LGTM!

Clean implementation of the middleware composition pattern. The slice() before reverse() correctly avoids mutating the input array, and the LIFO ordering is clearly documented.

src/models/event/EmissionExecutor.ts (4)

52-66: Error augmentation logic is sound.

The as any casting here is justified to dynamically add listenerId and listenerOrder properties for debugging purposes. The conditional checks prevent overwriting existing properties.


10-24: LGTM!

Sequential execution correctly handles propagation stopping and listener filtering.


100-108: LGTM!

Clean implementation that prevents self-invocation loops and respects optional filters.


82-97: No verification needed—listeners are guaranteed pre-sorted.

The ListenerRegistry maintains a sorting invariant using binary insertion (see insertListener method). Both event-specific and global listeners are kept in sorted order, and getCachedMergedListeners merges them while preserving order. The batching logic correctly relies on this guarantee.

src/models/event/CycleContext.ts (2)

24-50: LGTM!

The cycle detection logic is well-designed:

  • Correctly identifies cycles by matching frame.id in the emission stack
  • The safeReEmitBySameHook check appropriately allows a hook to re-emit its own event without triggering a false cycle error
  • Stack immutability via spread operator prevents accidental mutations

52-57: LGTM!

Clean implementation that correctly handles the disabled path and wraps hook execution with context tracking when enabled.

src/models/EventManager.ts (4)

1-23: LGTM: Clean modular imports.

The imports are well-organized, use relative paths as required, and properly separate the new modular components (types, registry, executors, interceptors, cycle context) from existing dependencies.


136-144: LGTM: Parallel execution correctly integrated.

The branching logic properly delegates to executeInParallel for parallel events and executeSequentially for sequential events. The different parameter signatures are appropriate: parallel execution checks event.isPropagationStopped() internally between order-based batches, while sequential execution receives the callback for per-listener checks.


167-209: LGTM: Proper listener registration delegation.

Both addListener and addGlobalListener correctly delegate to the registry, use the createListener factory for consistent listener creation, and perform lock checks before modifications.


310-313: LGTM: Proper type re-exports for API compatibility.

Re-exporting the types from the event module maintains backward compatibility for consumers who import these types from EventManager.

src/models/event/types.ts (1)

1-71: LGTM: Well-structured type definitions.

The type definitions are clean, well-documented, and properly organized. The separation into a dedicated types module improves maintainability and allows for easy reuse across the event system components.

Key strengths:

  • Clear JSDoc comments for each type
  • Consistent naming conventions
  • Proper separation between internal (IListenerStorage) and public (IEventHandlerOptions) types
  • Standard middleware pattern for interceptor types
src/models/event/ListenerRegistry.ts (4)

14-30: LGTM: Well-designed registry structure.

The registry properly encapsulates listener storage with appropriate visibility modifiers. The constructor's optional isExcludedFromGlobal parameter with a sensible default allows for testability while maintaining production behavior.


32-45: LGTM: Proper listener addition with cache invalidation.

Both methods correctly handle listener insertion using binary insertion to maintain order, and properly invalidate caches. The distinction between event-specific cache invalidation (line 39) and global cache invalidation (line 44) is correct.


47-94: LGTM: Efficient listener retrieval with proper caching.

The retrieval methods are well-optimized:

  • getListenersForEmit respects global exclusion rules
  • hasListeners short-circuits efficiently
  • getCachedMergedListeners handles edge cases (empty lists) before merging and properly manages cache invalidation

The caching strategy balances performance with correctness.


104-141: LGTM: Efficient sorting and insertion algorithms.

The private helper methods implement optimal algorithms:

  • mergeSortedListeners: O(n) merge of two sorted arrays
  • insertListener: O(log n) binary search insertion

Both maintain the order invariant correctly and are well-implemented for performance.

Comment on lines +144 to +152
export function createListener(newListener: Partial<IListenerStorage>): IListenerStorage {
return {
handler: newListener.handler!,
order: newListener.order ?? HandlerOptionsDefaults.order,
filter: newListener.filter,
id: newListener.id,
isGlobal: newListener.isGlobal ?? false,
};
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Handler non-null assertion may be unsafe.

Line 146 uses a non-null assertion (newListener.handler!) but the type signature allows handler to be undefined. Consider making the handler required in the parameter type:

-export function createListener(newListener: Partial<IListenerStorage>): IListenerStorage {
+export function createListener(
+  newListener: Partial<IListenerStorage> & Pick<IListenerStorage, 'handler'>
+): IListenerStorage {
   return {
-    handler: newListener.handler!,
+    handler: newListener.handler,
     order: newListener.order ?? HandlerOptionsDefaults.order,
     filter: newListener.filter,
     id: newListener.id,
     isGlobal: newListener.isGlobal ?? false,
   };
 }

This eliminates the need for the non-null assertion and makes the requirement explicit in the type system.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function createListener(newListener: Partial<IListenerStorage>): IListenerStorage {
return {
handler: newListener.handler!,
order: newListener.order ?? HandlerOptionsDefaults.order,
filter: newListener.filter,
id: newListener.id,
isGlobal: newListener.isGlobal ?? false,
};
}
export function createListener(
newListener: Partial<IListenerStorage> & Pick<IListenerStorage, 'handler'>
): IListenerStorage {
return {
handler: newListener.handler,
order: newListener.order ?? HandlerOptionsDefaults.order,
filter: newListener.filter,
id: newListener.id,
isGlobal: newListener.isGlobal ?? false,
};
}

- Updated `check-large-files.sh` to improve portability of awk commands.
- Added backward compatibility test for `getCachedMergedListeners` in `EventManager`.
- Introduced cycle detection tests in `CycleContext` to ensure proper handling of emission cycles.
- Enhanced `ListenerRegistry` tests to utilize a more structured event creation approach.
- Refactored `EventManager` to streamline listener retrieval logic.
- Improved `CycleContext` constructor for better readability and efficiency.
- Expanded `nodeExposure` tests to cover more edge cases and ensure proper error handling.
- Modified `allowList` logic to return 403 for non-exposed tasks/events instead of 404.
- Updated HTTP client interfaces to include error registry for better error handling.
- Added comprehensive tests for error handling in HTTP smart and mixed clients.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/node/__tests__/exposure.resource.more-coverage.test.ts (1)

482-564: Wrap cleanup in try/finally to prevent test pollution.

The http.createServer restoration at line 562 is not protected by a try/finally block. If rr.dispose() or earlier assertions throw, the mock won't be restored, potentially affecting subsequent tests.

     const rr = await run(app);
-    const handlers = await rr.getResourceValue(exposure.resource);
-
-    expect(typeof capturedHandler).toBe("function");
-    const req = createBaseReq();
-    // ... rest of test ...
-    await rr.dispose();
-    httpWithMutableCreate.createServer = realCreate;
-    expect(handlers.server).toBe(server);
+    try {
+      const handlers = await rr.getResourceValue(exposure.resource);
+
+      expect(typeof capturedHandler).toBe("function");
+      const req = createBaseReq();
+      // ... rest of test assertions ...
+      await rr.dispose();
+      expect(handlers.server).toBe(server);
+    } finally {
+      httpWithMutableCreate.createServer = realCreate;
+    }
🧹 Nitpick comments (11)
src/node/__tests__/http-smart-client.errors.test.ts (1)

16-29: Consider improving mock robustness.

The makeSink mock stubs on and setTimeout to return sink for chaining, but the stub for on ignores all event types. If future code relies on specific event listeners (e.g., 'error'), tests may silently pass when they shouldn't.

A more targeted mock could selectively handle events:

-function makeSink(): any {
-  const sink = new Writable({
-    write(_c, _e, n) {
-      n();
-    },
-    final(n) {
-      n();
-    },
-  }) as any;
-  sink.on = (_: any, __: any) => sink;
-  sink.setTimeout = () => sink;
-  sink.destroy = () => undefined;
-  return sink;
-}
+function makeSink(): any {
+  const sink = new Writable({
+    write(_c, _e, n) {
+      n();
+    },
+    final(n) {
+      n();
+    },
+  }) as any;
+  const originalOn = sink.on.bind(sink);
+  sink.on = (event: string, handler: any) => {
+    originalOn(event, handler);
+    return sink;
+  };
+  sink.setTimeout = () => sink;
+  sink.destroy = () => undefined;
+  return sink;
+}
src/node/__tests__/exposure.resource.auto-discovery.test.ts (1)

15-43: Well-structured mock request factory.

The helper correctly constructs a mock IncomingMessage with all required properties. One minor consideration:

Using new Socket() (line 29) creates a real socket instance. Consider using a minimal mock object instead to avoid any potential resource overhead in tests:

-      socket: new Socket(),
+      socket: {} as any,

This is a nit since the socket isn't actually used in these tests, but it avoids creating real system resources.

src/node/__tests__/exposure.resource.more-coverage.test.ts (2)

12-18: Consider simplifying the MockRes type.

The union type with intersection is somewhat convoluted:

type MockRes = (
  | ServerResponse
  | (ServerResponse & { body?: Buffer | null })
) & {
  body?: Buffer | null;
};

This simplifies to just adding the optional body property to ServerResponse:

-  type MockRes = (
-    | ServerResponse
-    | (ServerResponse & { body?: Buffer | null })
-  ) & {
-    body?: Buffer | null;
-  };
+  type MockRes = ServerResponse & { body?: Buffer | null };

20-37: Potential for shared test utility.

This createBaseReq helper is nearly identical to makeReq in exposure.resource.auto-discovery.test.ts. Consider extracting to a shared test utility if more test files need similar mocks.

This is a minor nit for test maintainability - acceptable to leave as-is if the tests are expected to evolve independently.

src/node/exposure/__tests__/requestHandlers.auth-and-missing.unit.test.ts (2)

66-84: Consider extracting common mock setup into a helper function.

The mock construction for store, taskRunner, eventManager, and logger is duplicated across all three tests. Extracting these into a helper function like createMockDeps() would reduce duplication and improve maintainability.

For example:

function createBasicMocks() {
  return {
    store: {
      tasks: new Map(),
      events: new Map(),
      asyncContexts: new Map(),
      resources: new Map(),
    } as unknown as NodeExposureDeps["store"],
    taskRunner: {
      async run() {
        return 1;
      },
    } as unknown as NodeExposureDeps["taskRunner"],
    eventManager: {
      emit: async () => {},
    } as unknown as NodeExposureDeps["eventManager"],
    logger: {
      info: async () => {},
      warn: async () => {},
      error: async () => {},
    } as unknown as NodeExposureDeps["logger"],
  };
}

Also applies to: 115-133, 158-190


222-228: Consider avoiding router mutation for clearer test structure.

Mutating deps.router.extract after creating the handlers (line 225) works but makes the test logic harder to follow. A cleaner approach would be to create a fresh set of deps for the event test or use a router that can handle both task and event paths.

For example:

// Event request blocked by allow-list
const eventReq = makeJsonReq("/api/event/blocked.event");
const eventRes = makeRes();
const eventDeps = {
  ...deps,
  router: makeRouter((_p: string) => ({
    kind: "event",
    id: "blocked.event",
  })),
};
const { handleEvent: handleEventForBlocked } = createRequestHandlers(eventDeps);
await handleEventForBlocked(eventReq, eventRes);
src/models/event/CycleContext.ts (1)

23-49: Clarify the source invariant and consider simplifying the API

The core cycle detection logic and stack handling look correct: you detect prior frames by id, build a path from the first occurrence, and throw a structured eventCycleError with the full chain. The safeReEmitBySameHook guard is a subtle but reasonable exception, but it currently relies on an important invariant: the source argument passed into runEmission must match the hookId stored via runHook whenever the emission originates from that hook.

If any future call sites pass a frame whose source differs from the separate source parameter, this guard could behave unexpectedly. You can make this safer and easier to reason about by deriving the comparison purely from frame:

-  runEmission(
-    frame: IEmissionFrame,
-    source: string,
-    processEmission: () => Promise<void>,
-  ): Promise<void> {
+  runEmission(
+    frame: IEmissionFrame,
+    processEmission: () => Promise<void>,
+  ): Promise<void> {
@@
-        const safeReEmitBySameHook =
-          top.id === frame.id && currentHookId && currentHookId === source;
+        const safeReEmitBySameHook =
+          top.id === frame.id &&
+          currentHookId &&
+          currentHookId === frame.source;
@@
-    const nextStack = currentStack ? [...currentStack, frame] : [frame];
-    return this.emissionStack.run(nextStack, processEmission);
+    const nextStack = currentStack ? [...currentStack, frame] : [frame];
+    return this.emissionStack.run(nextStack, processEmission);
   }

and updating the single caller in EventManager.emit accordingly:

-    const frame = { id: eventDefinition.id, source };
+    const frame = { id: eventDefinition.id, source };
@@
-    await this.cycleContext.runEmission(frame, source, processEmission);
+    await this.cycleContext.runEmission(frame, processEmission);

This removes an extra parameter, encodes the invariant in a single place (frame.source), and reduces the risk of accidental mismatch.

src/models/EventManager.ts (4)

30-41: Registry and cycle-context wiring look correct; aliasing assumption should be kept stable

Initializing ListenerRegistry and CycleContext in the constructor and then aliasing listeners, globalListeners, and cachedMergedListeners to the registry’s collections is a good way to preserve test back-compat while moving logic into the registry.

This does rely on ListenerRegistry keeping those collections as long-lived instances (mutating in place rather than reassigning the fields). As long as that holds, tests that reach into EventManager.listeners/globalListeners will continue to see up‑to‑date data; it’s worth documenting this contract in ListenerRegistry to avoid accidental refactors that break the aliasing.

Also applies to: 48-57


102-149: Emit flow refactor is sound; document propagation semantics for parallel events

The new emission path looks good overall:

  • Listener resolution is delegated to this.registry.getListenersForEmit(eventDefinition).
  • The IEventEmission object is built once and passed through composeInterceptors.
  • Sequential vs parallel listener execution is chosen via eventDefinition.parallel, using executeSequentially vs executeInParallel.

One behavioral nuance worth calling out: in the parallel branch you no longer pass isPropagationStopped into the executor, so event.stopPropagation() cannot prevent other listeners from running once dispatch has begun. That’s likely the only sensible behavior for fully parallel dispatch, but it’s a change from purely sequential semantics and is easy to miss by users.

I recommend making this explicit in event/parallel documentation and tests (e.g., asserting that stopPropagation() is a no‑op when parallel: true) so the behavior is clearly intentional and guarded against regressions.


160-180: Listener registration via ListenerRegistry is correct; minor clean-up possible for array case

Creating listeners through createListener and delegating registration to this.registry.addListener is the right direction and keeps listener shape internal to the event subsystem.

One minor tidy-up: when event is an array, you compute newListener but don’t use it in the outer call, since you delegate to this.addListener for each element. You can avoid that extra createListener call by early‑returning on the array branch:

  addListener<T>(
    event: IEvent<T> | Array<IEvent<T>>,
    handler: EventHandlerType<T>,
    options: IEventHandlerOptions<T> = HandlerOptionsDefaults,
  ): void {
    this.checkLock();
-    const newListener = createListener({
-      handler,
-      order: options.order,
-      filter: options.filter,
-      id: options.id,
-      isGlobal: false,
-    });
-
-    if (Array.isArray(event)) {
-      event.forEach((id) => this.addListener(id, handler, options));
-    } else {
-      const eventId = event.id;
-      this.registry.addListener(eventId, newListener);
-    }
+    if (Array.isArray(event)) {
+      event.forEach((single) => this.addListener(single, handler, options));
+      return;
+    }
+
+    const newListener = createListener({
+      handler,
+      order: options.order,
+      filter: options.filter,
+      id: options.id,
+      isGlobal: false,
+    });
+
+    this.registry.addListener(event.id, newListener);
  }

Purely a small efficiency/readability win; behavior stays the same.


269-279: Hook execution pipeline is correct; outer isEnabled check is redundant

Composing hook interceptors with composeInterceptors and then optionally executing within cycleContext.runHook is the right place to integrate hook context with cycle detection.

Given that CycleContext.runHook already checks isEnabled and falls back to executing the callback directly, the ternary here is effectively duplicating that check. You can simplify without changing behavior:

-    return this.cycleContext.isEnabled
-      ? await this.cycleContext.runHook(hook.id, () =>
-          executeWithInterceptors(hook, event),
-        )
-      : await executeWithInterceptors(hook, event);
+    return this.cycleContext.runHook(hook.id, () =>
+      executeWithInterceptors(hook, event),
+    );

This centralizes the enable/disable logic inside CycleContext and slightly reduces branching here.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3e1fff4 and 833ce85.

📒 Files selected for processing (13)
  • scripts/check-large-files.sh (2 hunks)
  • src/__tests__/models/EventManager.test.ts (2 hunks)
  • src/__tests__/models/event/CycleContext.test.ts (1 hunks)
  • src/__tests__/models/event/ListenerRegistry.test.ts (1 hunks)
  • src/models/EventManager.ts (9 hunks)
  • src/models/event/CycleContext.ts (1 hunks)
  • src/node/__tests__/exposure.resource.auto-discovery.test.ts (2 hunks)
  • src/node/__tests__/exposure.resource.more-coverage.test.ts (16 hunks)
  • src/node/__tests__/http-smart-client.errors.test.ts (1 hunks)
  • src/node/exposure/__tests__/requestHandlers.auth-and-missing.unit.test.ts (2 hunks)
  • src/node/exposure/allowList.ts (2 hunks)
  • src/node/http-mixed-client.ts (3 hunks)
  • src/node/http-smart-client.model.ts (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/tests/models/event/ListenerRegistry.test.ts
  • src/tests/models/event/CycleContext.test.ts
  • src/tests/models/EventManager.test.ts
🧰 Additional context used
📓 Path-based instructions (4)
src/**/*.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

src/**/*.ts: Format code with Prettier for all TypeScript source files (npx prettier --write src)
Check code formatting with Prettier for all TypeScript source files (npx prettier --check src)

Files:

  • src/node/exposure/__tests__/requestHandlers.auth-and-missing.unit.test.ts
  • src/models/event/CycleContext.ts
  • src/node/__tests__/exposure.resource.auto-discovery.test.ts
  • src/node/__tests__/http-smart-client.errors.test.ts
  • src/node/http-mixed-client.ts
  • src/node/exposure/allowList.ts
  • src/models/EventManager.ts
  • src/node/http-smart-client.model.ts
  • src/node/__tests__/exposure.resource.more-coverage.test.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Type-strictness is a must; avoid type casting unless absolutely necessary

Files:

  • src/node/exposure/__tests__/requestHandlers.auth-and-missing.unit.test.ts
  • src/models/event/CycleContext.ts
  • src/node/__tests__/exposure.resource.auto-discovery.test.ts
  • src/node/__tests__/http-smart-client.errors.test.ts
  • src/node/http-mixed-client.ts
  • src/node/exposure/allowList.ts
  • src/models/EventManager.ts
  • src/node/http-smart-client.model.ts
  • src/node/__tests__/exposure.resource.more-coverage.test.ts
**/*.{ts,tsx,js,jsx,mjs,cjs}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx,mjs,cjs}: Use only relative imports
Place imports at the top of the file by default; inline imports only for specific optimizations

Files:

  • src/node/exposure/__tests__/requestHandlers.auth-and-missing.unit.test.ts
  • src/models/event/CycleContext.ts
  • src/node/__tests__/exposure.resource.auto-discovery.test.ts
  • src/node/__tests__/http-smart-client.errors.test.ts
  • src/node/http-mixed-client.ts
  • src/node/exposure/allowList.ts
  • src/models/EventManager.ts
  • src/node/http-smart-client.model.ts
  • src/node/__tests__/exposure.resource.more-coverage.test.ts
src/node/**

📄 CodeRabbit inference engine (AGENTS.md)

Place Node-specific code under ./src/node/

Files:

  • src/node/exposure/__tests__/requestHandlers.auth-and-missing.unit.test.ts
  • src/node/__tests__/exposure.resource.auto-discovery.test.ts
  • src/node/__tests__/http-smart-client.errors.test.ts
  • src/node/http-mixed-client.ts
  • src/node/exposure/allowList.ts
  • src/node/http-smart-client.model.ts
  • src/node/__tests__/exposure.resource.more-coverage.test.ts
🧠 Learnings (5)
📚 Learning: 2025-09-12T05:57:37.217Z
Learnt from: CR
Repo: bluelibs/runner PR: 0
File: examples/fastify-mikroorm/AGENTS.md:0-0
Timestamp: 2025-09-12T05:57:37.217Z
Learning: Applies to examples/fastify-mikroorm/src/**/*.test.ts : Ensure tests cover validation errors (400), auth paths (401/403), happy paths (200), and HTTPError mapping

Applied to files:

  • src/node/exposure/__tests__/requestHandlers.auth-and-missing.unit.test.ts
  • src/node/__tests__/exposure.resource.auto-discovery.test.ts
  • src/node/__tests__/http-smart-client.errors.test.ts
  • src/node/__tests__/exposure.resource.more-coverage.test.ts
📚 Learning: 2025-09-12T05:57:37.217Z
Learnt from: CR
Repo: bluelibs/runner PR: 0
File: examples/fastify-mikroorm/AGENTS.md:0-0
Timestamp: 2025-09-12T05:57:37.217Z
Learning: Applies to examples/fastify-mikroorm/src/**/*.test.ts : In HTTP tests, register httpRoute, fastify, and fastifyRouter with buildTestRunner

Applied to files:

  • src/node/exposure/__tests__/requestHandlers.auth-and-missing.unit.test.ts
  • src/node/__tests__/exposure.resource.auto-discovery.test.ts
  • src/node/__tests__/http-smart-client.errors.test.ts
  • src/node/__tests__/exposure.resource.more-coverage.test.ts
📚 Learning: 2025-09-12T05:57:37.217Z
Learnt from: CR
Repo: bluelibs/runner PR: 0
File: examples/fastify-mikroorm/AGENTS.md:0-0
Timestamp: 2025-09-12T05:57:37.217Z
Learning: Applies to examples/fastify-mikroorm/src/**/tasks/*.task.ts : Tag all HTTP-exposed tasks with httpRoute and configure method, path, auth, and inputFrom as needed

Applied to files:

  • src/node/exposure/__tests__/requestHandlers.auth-and-missing.unit.test.ts
  • src/node/__tests__/exposure.resource.auto-discovery.test.ts
  • src/node/__tests__/exposure.resource.more-coverage.test.ts
📚 Learning: 2025-09-12T05:57:37.217Z
Learnt from: CR
Repo: bluelibs/runner PR: 0
File: examples/fastify-mikroorm/AGENTS.md:0-0
Timestamp: 2025-09-12T05:57:37.217Z
Learning: Applies to examples/fastify-mikroorm/src/**/tasks/*.task.ts : Set httpRoute auth to public | optional | required appropriately for each route

Applied to files:

  • src/node/exposure/__tests__/requestHandlers.auth-and-missing.unit.test.ts
📚 Learning: 2025-09-12T05:57:37.217Z
Learnt from: CR
Repo: bluelibs/runner PR: 0
File: examples/fastify-mikroorm/AGENTS.md:0-0
Timestamp: 2025-09-12T05:57:37.217Z
Learning: Applies to examples/fastify-mikroorm/src/**/tasks/*.task.ts : Throw HTTPError for expected HTTP failures (e.g., new HTTPError(status, message)) from tasks

Applied to files:

  • src/node/exposure/__tests__/requestHandlers.auth-and-missing.unit.test.ts
  • src/node/__tests__/exposure.resource.auto-discovery.test.ts
  • src/node/__tests__/http-smart-client.errors.test.ts
  • src/node/__tests__/exposure.resource.more-coverage.test.ts
🧬 Code graph analysis (4)
src/node/exposure/__tests__/requestHandlers.auth-and-missing.unit.test.ts (3)
src/globals/resources/tunnel/serializer.ts (1)
  • getDefaultSerializer (27-29)
src/globals/globalTags.ts (1)
  • globalTags (6-30)
src/node/exposure/allowList.ts (1)
  • createAllowListGuard (8-40)
src/models/event/CycleContext.ts (3)
src/models/event/types.ts (1)
  • IEmissionFrame (68-71)
src/platform/index.ts (1)
  • getPlatform (18-24)
src/errors.ts (1)
  • eventCycleError (146-153)
src/node/__tests__/http-smart-client.errors.test.ts (4)
src/node/http-mixed-client.ts (2)
  • Readable (109-109)
  • createHttpMixedClient (63-107)
src/node/http-smart-client.model.ts (2)
  • Readable (425-425)
  • createHttpSmartClient (358-423)
src/globals/globalResources.ts (1)
  • serializer (25-33)
src/globals/resources/tunnel/serializer.ts (1)
  • getDefaultSerializer (27-29)
src/node/exposure/allowList.ts (2)
src/node/exposure/httpResponse.ts (1)
  • jsonErrorResponse (26-42)
src/definers/defineError.ts (1)
  • id (26-28)
🔇 Additional comments (26)
scripts/check-large-files.sh (2)

45-52: ✓ Token estimation and relative path storage implemented correctly.

The token approximation formula (chars + 3) / 4 is a standard GPT-style heuristic, and the arithmetic is sound (integer division naturally rounds up). The transition to storing relative paths and restructuring results as "count chars tokens rel_path" is clean and maintains compatibility with the null-delimited find pipeline. Parameter expansion via ${file#"$ROOT_DIR/"} is the idiomatic bash approach.


61-83: ✓ Terminal styling and output formatting well-implemented.

The ANSI escape codes for bold (\033[1m) and reset (\033[0m) are standard and portable. Passing them to awk via -v is the correct approach for cross-platform compatibility. The awk formatting logic correctly:

  • Reconstructs the path from fields 4+ to preserve spaces in file paths
  • Uses aligned printf format specifiers for readable columnar output
  • Generates a bold, dashed header

One minor note: ensure printf format widths (%-16s, %-12s) remain adequate as the codebase evolves (e.g., if files exceed 9,999 lines or 999,999 characters). If needed, these can be made dynamic via awk variables, but current defaults appear reasonable for typical codebases.

src/node/http-mixed-client.ts (1)

6-6: LGTM! Error registry correctly propagated to both client paths.

The errorRegistry is properly typed using IErrorHelper<any> and consistently passed to both fetchClient and smartClient, ensuring typed error handling works across all HTTP client paths.

Also applies to: 26-26, 87-87

src/node/http-smart-client.model.ts (4)

347-356: Well-structured error transformation helper.

The rethrowTyped function correctly uses return type never and provides a clean pattern for registry-based error transformation. The conditional check for registry && error && id && data ensures safe access before attempting typed rethrow.


370-374: Verify: Streaming duplex path lacks typed error handling.

The octet-stream path bypasses rethrowTyped, unlike the multipart and JSON paths. If this is intentional (raw streams don't return JSON error envelopes), consider adding a brief comment. Otherwise, wrap in try/catch for consistency.


382-410: LGTM! Error handling correctly integrated into multipart and JSON paths.

Both paths now properly wrap network calls in try/catch and route errors through rethrowTyped, enabling typed error propagation when the error envelope contains a matching id in the registry.


415-420: LGTM! Event path error handling is consistent.

The event path mirrors the task paths with proper try/catch wrapping and typed error rethrow.

src/node/__tests__/http-smart-client.errors.test.ts (4)

31-45: Good test setup with proper cleanup.

The shared helper mock correctly implements the IErrorHelper interface pattern, and afterEach ensures mocks are restored between tests, preventing test pollution.


47-76: LGTM! JSON path error handling test is comprehensive.

The test correctly mocks http.request to return an error envelope and verifies that the client rethrows typed errors via the error registry. Using regex /typed:7/ for assertion is appropriate.


78-112: LGTM! Smart path test validates multipart error handling.

The test correctly uses createNodeFile to trigger the multipart code path via the mixed client, verifying that typed errors flow through even for file uploads.


114-143: LGTM! Event path error handling test completes the coverage.

The test verifies that event calls also benefit from the typed error registry, ensuring consistent error handling across all client operations.

src/node/exposure/allowList.ts (1)

24-27: LGTM! Correct semantic choice for access control.

Changing from 404 to 403 is the right HTTP semantics here. A 403 FORBIDDEN correctly indicates that the resource exists but access is denied, whereas 404 would imply the task/event doesn't exist at all. This prevents information leakage about non-allowlisted resources while being semantically accurate.

Also applies to: 34-37

src/node/__tests__/exposure.resource.auto-discovery.test.ts (2)

45-59: LGTM!

Minimal and sufficient mock for these test scenarios. The chaining support via return values is correctly implemented.


98-134: LGTM! Clean test structure with correct assertions.

The removal of type casting on exposure.resource (line 98) and the updated 403 expectation (line 121) correctly align with the improved typing and access control semantics. Good coverage of allowed/not-allowed scenarios for both tasks and events.

src/node/__tests__/exposure.resource.more-coverage.test.ts (2)

340-365: LGTM! Well-structured server mock.

Good use of Jest spy with proper finally block cleanup. The typed listener map and minimal Server interface are appropriate for the test scenario.


437-448: LGTM! Assertions correctly updated.

The 403 expectations and descriptive comments correctly align with the access control semantics change in allowList.ts. Good coverage for both tasks and events in the not-allowlisted scenarios.

Also applies to: 463-477

src/node/exposure/__tests__/requestHandlers.auth-and-missing.unit.test.ts (3)

1-12: LGTM!

The imports are well-organized and follow the coding guidelines. The type definitions for MockReq and MockRes clearly document the mock structure.


14-62: LGTM!

The mock helper functions effectively standardize test infrastructure. The implementations provide sufficient mock behavior for the request handlers under test.


157-235: LGTM! Good coverage of allow-list blocking scenarios.

The test correctly verifies that both tasks and events return 403 FORBIDDEN when blocked by the allow-list. The assertions check both status codes and error codes, providing comprehensive coverage. Based on learnings, HTTP tests should cover auth paths including 403, which this test does.

src/models/event/CycleContext.ts (2)

5-21: Platform-aware enabling/disablement looks solid

The constructor correctly gates cycle detection on both runtimeCycleDetection and platform.hasAsyncLocalStorage(), and falls back cleanly to a disabled state with null storages and isEnabled = false. This keeps the rest of the API simple (runEmission/runHook can just check isEnabled) and avoids partial initialization states.


51-56: Hook context scoping is minimal and correct

runHook’s contract is clear and small: it either executes the callback directly when disabled or wraps it in currentHookIdContext.run when enabled. This matches how EventManager uses it and keeps hook-related context management well encapsulated.

src/models/EventManager.ts (5)

7-22: Imports and modularization align with guidelines

The new imports cleanly separate concerns into ./event/* modules and maintain relative paths only, matching the repo’s guidelines. Pulling shared types and executors out of EventManager also makes the file significantly easier to navigate.


189-202: Global listener registration is consistent with per-event listeners

Using createListener with isGlobal: true and delegating to this.registry.addGlobalListener is consistent with the per-event path and keeps all ordering/filter logic centralized in the registry. No issues here.


210-212: Listener existence check is correctly delegated

hasListeners now simply forwards to this.registry.hasListeners(eventDefinition), which keeps the querying logic in one place alongside the storage. This is a straightforward and correct delegation.


295-299: Cached merged listeners compatibility shim is appropriate

Keeping getCachedMergedListeners as a thin passthrough to this.registry.getCachedMergedListeners is a good way to preserve existing test hooks while migrating the actual caching logic into the registry.


302-304: Type re-exports preserve public API surface

Re-exporting IEventHandlerOptions, EventEmissionInterceptor, and HookExecutionInterceptor from EventManager maintains compatibility for external consumers while allowing their definitions to live in ./event/types. This is a clean way to evolve internals without forcing breaking changes.

@theodorDiaconu theodorDiaconu merged commit 959491d into main Dec 5, 2025
4 checks passed
@theodorDiaconu theodorDiaconu deleted the feat/parallel-events branch December 5, 2025 11:21
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