Skip to content

feat(client): consumeEventIterator utility - #976

Merged
dinwwwh merged 4 commits into
mainfrom
feat/client/consumeEventIterator-util
Sep 11, 2025
Merged

feat(client): consumeEventIterator utility#976
dinwwwh merged 4 commits into
mainfrom
feat/client/consumeEventIterator-util

Conversation

@dinwwwh

@dinwwwh dinwwwh commented Sep 9, 2025

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features

    • Added consumeEventIterator: a public utility to consume streaming/event iterators with lifecycle callbacks (onEvent, onError, onSuccess, onFinish) and an unsubscribe function for graceful cancellation.
  • Documentation

    • Added "Using consumeEventIterator" guide with a TypeScript example showing setup, callbacks, cancellation, and finish-state behavior.
  • Tests

    • Added comprehensive tests covering success, error paths, cancellation/unsubscribe, promise-rejection, and related edge cases.

@vercel

vercel Bot commented Sep 9, 2025

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
orpc Ready Ready Preview Comment Sep 9, 2025 8:52am

@dosubot dosubot Bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Sep 9, 2025
@coderabbitai

coderabbitai Bot commented Sep 9, 2025

Copy link
Copy Markdown

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds a new exported utility consumeEventIterator with a typed options interface and unsubscribe behavior, a ClientPromiseResult type export, runtime and type tests for the helper, and a docs section demonstrating usage and cancellation. No other public API signatures changed.

Changes

Cohort / File(s) Summary
Docs: Event iterator usage
apps/content/docs/client/event-iterator.md
Appends “Using consumeEventIterator” documentation showing TypeScript usage with lifecycle callbacks (onEvent, onError, onSuccess, onFinish) and cancellation via the returned cancel/unsubscribe handle.
Client utils: Implementation
packages/client/src/utils.ts
Adds exported ConsumeEventIteratorOptions<T,TReturn,TError> and consumeEventIterator(...) which accepts an AsyncIterator or ClientPromiseResult, iterates calling onEvent, surfaces onError/onSuccess, always calls onFinish with OnFinishState, and returns an async unsubscribe() that calls iterator.return().
Client types: Export
packages/client/src/types.ts
Adds/exported type ClientPromiseResult to allow promise-wrapped iterators to be accepted by consumeEventIterator.
Client utils: Runtime tests
packages/client/src/utils.test.ts
Adds tests exercising consumeEventIterator success, error, missing handlers (unhandledRejection), unsubscribe, error-on-unsubscribe, and iterator-promise-rejection paths; imports and uses consumeEventIterator.
Client utils: Type tests
packages/client/src/utils.test-d.ts
Adds type-level tests for consumeEventIterator with ClientPromiseResult and AsyncIterator shapes; updates imports (OnFinishState, ClientPromiseResult, consumeEventIterator).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor App as App Code
  participant CEI as consumeEventIterator
  participant AI as AsyncIterator
  participant CB as Callbacks

  App->>CEI: consumeEventIterator(iteratorOrPromise, { onEvent, onError, onSuccess, onFinish })
  CEI->>AI: await iteratorOrPromise

  loop iterate until done or error
    CEI->>AI: next()
    alt yields value (done=false)
      AI-->>CEI: { value, done:false }
      CEI-->>CB: onEvent(value)
    else completes (done=true)
      AI-->>CEI: { value, done:true }
      CEI-->>CB: onSuccess(value?)
      CEI-->>CB: onFinish([null, value?, true])
    end
  end

  rect rgba(240,80,80,0.08)
    note right of CEI: Error path
    AI--xCEI: throw / rejected promise
    CEI-->>CB: onError?(error)
    CEI-->>CB: onFinish([error, undefined, false])
  end

  %% Unsubscribe flow
  App-->>CEI: const unsubscribe = (...)
  App->>CEI: await unsubscribe()
  CEI->>AI: return() // graceful termination
  CEI-->>CB: onFinish([null, undefined, true])
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Pre-merge checks (1 passed, 2 warnings)

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description Check ⚠️ Warning The pull request lacks any description, leaving no context or summary of the changes made, so it fails to document the intent or scope of the utility addition. Please add a description summarizing the purpose of consumeEventIterator, its behavior, and any relevant usage notes to provide context for reviewers.
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (1 passed)
Check name Status Explanation
Title Check ✅ Passed The title “feat(client): consumeEventIterator utility” succinctly captures the primary addition of the new consumeEventIterator helper in the client package, is concise, and directly relates to the main changeset without extraneous detail.

Poem

I nibble streams of events so bright,
One hop per yield beneath the light,
onEvent crunch, onError sigh,
onSuccess grin, onFinish high.
Unsubscribe twitch — I bound away.


📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9576c5d and c9101be.

📒 Files selected for processing (2)
  • packages/client/src/utils.test-d.ts (2 hunks)
  • packages/client/src/utils.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/client/src/utils.test-d.ts
  • packages/client/src/utils.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: publish-commit
  • GitHub Check: lint
  • GitHub Check: test
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/client/consumeEventIterator-util

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.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Summary of Changes

Hello @unnoq, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a new utility function, consumeEventIterator, to the client library. Its purpose is to streamline the consumption of asynchronous event streams by providing a structured way to handle events, errors, and completion states through lifecycle callbacks, simplifying the management of streaming data.

Highlights

  • New Utility Function: Introduces consumeEventIterator in @orpc/client for managing event streams.
  • Lifecycle Callbacks: Provides onEvent, onError, onSuccess, and onFinish callbacks for comprehensive stream handling.
  • Unsubscribe Mechanism: The utility returns a function to gracefully stop the event stream.
  • Documentation: Adds detailed documentation and usage examples for the new utility.
  • Robust Testing: Includes extensive unit and type tests to ensure correctness and proper type inference.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in issue comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@codecov

codecov Bot commented Sep 9, 2025

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a new consumeEventIterator utility, which is a great addition for simplifying event stream consumption. The implementation is solid, and it's well-documented and tested. I've identified a high-severity type safety issue in the error handling logic and a medium-severity issue in one of the tests concerning test isolation. My feedback includes suggestions to address these points to improve the robustness and correctness of the new utility and its tests.

Comment thread packages/client/src/utils.ts
Comment thread packages/client/src/utils.test.ts
@pkg-pr-new

pkg-pr-new Bot commented Sep 9, 2025

Copy link
Copy Markdown
More templates

@orpc/arktype

npm i https://pkg.pr.new/@orpc/arktype@976

@orpc/client

npm i https://pkg.pr.new/@orpc/client@976

@orpc/contract

npm i https://pkg.pr.new/@orpc/contract@976

@orpc/experimental-durable-event-iterator

npm i https://pkg.pr.new/@orpc/experimental-durable-event-iterator@976

@orpc/hey-api

npm i https://pkg.pr.new/@orpc/hey-api@976

@orpc/interop

npm i https://pkg.pr.new/@orpc/interop@976

@orpc/json-schema

npm i https://pkg.pr.new/@orpc/json-schema@976

@orpc/nest

npm i https://pkg.pr.new/@orpc/nest@976

@orpc/openapi

npm i https://pkg.pr.new/@orpc/openapi@976

@orpc/openapi-client

npm i https://pkg.pr.new/@orpc/openapi-client@976

@orpc/otel

npm i https://pkg.pr.new/@orpc/otel@976

@orpc/react

npm i https://pkg.pr.new/@orpc/react@976

@orpc/react-query

npm i https://pkg.pr.new/@orpc/react-query@976

@orpc/experimental-react-swr

npm i https://pkg.pr.new/@orpc/experimental-react-swr@976

@orpc/server

npm i https://pkg.pr.new/@orpc/server@976

@orpc/shared

npm i https://pkg.pr.new/@orpc/shared@976

@orpc/solid-query

npm i https://pkg.pr.new/@orpc/solid-query@976

@orpc/standard-server

npm i https://pkg.pr.new/@orpc/standard-server@976

@orpc/standard-server-aws-lambda

npm i https://pkg.pr.new/@orpc/standard-server-aws-lambda@976

@orpc/standard-server-fetch

npm i https://pkg.pr.new/@orpc/standard-server-fetch@976

@orpc/standard-server-node

npm i https://pkg.pr.new/@orpc/standard-server-node@976

@orpc/standard-server-peer

npm i https://pkg.pr.new/@orpc/standard-server-peer@976

@orpc/svelte-query

npm i https://pkg.pr.new/@orpc/svelte-query@976

@orpc/tanstack-query

npm i https://pkg.pr.new/@orpc/tanstack-query@976

@orpc/trpc

npm i https://pkg.pr.new/@orpc/trpc@976

@orpc/valibot

npm i https://pkg.pr.new/@orpc/valibot@976

@orpc/vue-colada

npm i https://pkg.pr.new/@orpc/vue-colada@976

@orpc/vue-query

npm i https://pkg.pr.new/@orpc/vue-query@976

@orpc/zod

npm i https://pkg.pr.new/@orpc/zod@976

commit: c9101be

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (7)
packages/client/src/utils.ts (3)

85-86: Make unsubscribe wait for onFinish to avoid a race on teardown.

Currently, cancel() resolves after iterator.return() completes but before onFinish is guaranteed to run. Tie the returned promise to the internal runner so callers who await cancel() know finalizers have fired.

-  void (async () => {
+  const runner = (async () => {
@@
-  })()
+  })()
@@
-  return async () => {
-    await (await iterator)?.return?.()
-  }
+  return async () => {
+    const it = await iterator
+    try {
+      await it?.return?.()
+    } finally {
+      if (options.onFinish) {
+        // Ensure onFinish has fired before resolving cancel()
+        await runner.catch(() => undefined)
+      }
+    }
+  }

Also applies to: 123-125


81-84: Improve type inference with overloads for plain vs. promised iterators.

Overloads make TError infer as unknown for plain iterators and as the provided error type for promised ones (matching your dts tests).

 export function consumeEventIterator<T, TReturn, TError>(
-  iterator: AsyncIteratorObject<T, TReturn> | ClientPromiseResult<AsyncIteratorObject<T, TReturn>, TError>,
-  options: ConsumeEventIteratorOptions<T, TReturn, TError>,
-): () => Promise<void> {
+  iterator: ClientPromiseResult<AsyncIteratorObject<T, TReturn>, TError>,
+  options: ConsumeEventIteratorOptions<T, TReturn, TError>,
+): () => Promise<void>
+export function consumeEventIterator<T, TReturn>(
+  iterator: AsyncIteratorObject<T, TReturn>,
+  options: ConsumeEventIteratorOptions<T, TReturn, unknown>,
+): () => Promise<void>
+export function consumeEventIterator<T, TReturn, TError>(
+  iterator: AsyncIteratorObject<T, TReturn> | ClientPromiseResult<AsyncIteratorObject<T, TReturn>, TError>,
+  options: ConsumeEventIteratorOptions<T, TReturn, TError>,
+): () => Promise<void> {

102-120: Callback exceptions will surface as unhandled rejections.

onEvent/onError/onSuccess/onFinish exceptions will reject the internal task (even if handlers are present). If that’s intentional, document it; otherwise, wrap callbacks in try/catch and report via a user-provided logger hook.

apps/content/docs/client/event-iterator.md (1)

72-98: Add a short note about required error handling and cancel semantics.

Make it explicit that at least one of onError or onFinish should be provided and that await cancel() waits for graceful shutdown.

 ## Using `consumeEventIterator`
 
 oRPC provides a utility function `consumeEventIterator` to consume an event iterator with lifecycle callbacks.
 
+::: warning
+Provide at least one of `onError` or `onFinish`. If neither is supplied, rejections bubble as unhandled promise rejections.
+:::
+
 ```ts
 import { consumeEventIterator } from '@orpc/client'
@@
 setTimeout(async () => {
   // Stop the stream after 1 second
-  await cancel()
+  // Awaiting ensures the iterator has been gracefully closed before proceeding.
+  await cancel()
 }, 1000)

</blockquote></details>
<details>
<summary>packages/client/src/utils.test.ts (3)</summary><blockquote>

`131-170`: **Stabilize “unsubscribe” timing to avoid flakiness.**

The 1ms delay races the generator’s first yield (25ms). Wait for the first event, then unsubscribe.

Apply:

```diff
-    await new Promise(resolve => setTimeout(resolve, 1))
-    await unsubscribe()
+    await vi.waitFor(() => expect(onEvent).toHaveBeenCalledTimes(1))
+    await unsubscribe()

44-49: Avoid unused variable in tests.

unsubscribe isn’t used in this case; drop the binding.

Apply:

-    const unsubscribe = consumeEventIterator(iterator, {
+    void consumeEventIterator(iterator, {

78-83: Same nit: drop unused unsubscribe binding.

Apply:

-    const unsubscribe = consumeEventIterator(iterator, {
+    void consumeEventIterator(iterator, {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 316d5b9 and 53db45d.

📒 Files selected for processing (4)
  • apps/content/docs/client/event-iterator.md (1 hunks)
  • packages/client/src/utils.test-d.ts (2 hunks)
  • packages/client/src/utils.test.ts (2 hunks)
  • packages/client/src/utils.ts (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
packages/client/src/utils.test-d.ts (3)
packages/client/src/utils.ts (1)
  • consumeEventIterator (81-126)
packages/client/src/types.ts (1)
  • ClientPromiseResult (24-24)
packages/shared/src/interceptor.ts (1)
  • OnFinishState (64-66)
packages/client/src/utils.ts (2)
packages/shared/src/interceptor.ts (1)
  • OnFinishState (64-66)
packages/client/src/types.ts (1)
  • ClientPromiseResult (24-24)
packages/client/src/utils.test.ts (2)
packages/shared/src/interceptor.ts (3)
  • onError (46-62)
  • onSuccess (33-41)
  • onFinish (71-97)
packages/client/src/utils.ts (1)
  • consumeEventIterator (81-126)
🪛 Biome (2.1.2)
packages/client/src/utils.test.ts

[error] 185-185: Unsafe usage of 'throw'.

'throw' in 'finally' overwrites the control flow statements inside 'try' and 'catch'.

(lint/correctness/noUnsafeFinally)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: publish-commit
  • GitHub Check: lint
🔇 Additional comments (4)
packages/client/src/utils.test-d.ts (1)

70-104: Type inference checks look solid.

Covers both promised and plain iterator inputs; TError inference matches expectations.

packages/client/src/utils.test.ts (3)

31-63: Great success-path coverage for consumeEventIterator.

Assertions for per-yield onEvent, onSuccess, and onFinish state look correct and match the implementation.


65-98: Error-path behavior verified well.

Good checks for onError firing, onFinish state, and absence of onSuccess.


216-243: Iterator-promise rejection test looks solid.

Covers the branch where iterator is a rejected promise; correct expectations for onError/onFinish and zero events.

Comment thread packages/client/src/utils.test.ts
Comment thread packages/client/src/utils.test.ts
Comment thread packages/client/src/utils.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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)
packages/client/src/utils.test-d.ts (2)

70-86: Also assert the unsubscribe function’s type.

consumeEventIterator returns () => Promise<void>. Asserting it here hardens the contract and guards regressions.

-    void consumeEventIterator({} as ClientPromiseResult<AsyncGenerator<'message-value', 'done-value'>, 'error-value'>, {
+    const cancel = consumeEventIterator({} as ClientPromiseResult<AsyncGenerator<'message-value', 'done-value'>, 'error-value'>, {
       onEvent: (message) => {
         expectTypeOf(message).toEqualTypeOf<'message-value'>()
       },
       onError: (error) => {
         expectTypeOf(error).toEqualTypeOf<'error-value'>()
       },
       onSuccess: (value) => {
         expectTypeOf(value).toEqualTypeOf<'done-value' | undefined>()
       },
       onFinish: (state) => {
         expectTypeOf(state).toEqualTypeOf<OnFinishState<'done-value' | undefined, 'error-value'>>()
       },
     })
+    expectTypeOf(cancel).toEqualTypeOf<() => Promise<void>>()

88-103: Mirror the return-type assertion for the plain AsyncIterator case.

Keeps both branches equally covered.

-    void consumeEventIterator({} as AsyncIterator<'message-value', 'done-value'>, {
+    const cancel = consumeEventIterator({} as AsyncIterator<'message-value', 'done-value'>, {
       onEvent: (message) => {
         expectTypeOf(message).toEqualTypeOf<'message-value'>()
       },
       onError: (error) => {
         expectTypeOf(error).toEqualTypeOf<unknown>()
       },
       onSuccess: (value) => {
         expectTypeOf(value).toEqualTypeOf<'done-value' | undefined>()
       },
       onFinish: (state) => {
         expectTypeOf(state).toEqualTypeOf<OnFinishState<'done-value' | undefined, unknown>>()
       },
     })
+    expectTypeOf(cancel).toEqualTypeOf<() => Promise<void>>()
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 53db45d and d275a77.

📒 Files selected for processing (2)
  • packages/client/src/utils.test-d.ts (2 hunks)
  • packages/client/src/utils.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/client/src/utils.ts
🧰 Additional context used
🧬 Code graph analysis (1)
packages/client/src/utils.test-d.ts (3)
packages/client/src/utils.ts (1)
  • consumeEventIterator (81-126)
packages/client/src/types.ts (1)
  • ClientPromiseResult (24-24)
packages/shared/src/interceptor.ts (1)
  • OnFinishState (64-66)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: publish-commit
  • GitHub Check: lint
🔇 Additional comments (1)
packages/client/src/utils.test-d.ts (1)

1-1: Imports LGTM and correctly marked as type-only.

The new public types and util are imported cleanly. No issues.

Also applies to: 3-3, 5-5

@dinwwwh dinwwwh added the lgtm This PR has been approved by a maintainer label Sep 10, 2025
@dinwwwh
dinwwwh merged commit 965aae5 into main Sep 11, 2025
11 checks passed
@dinwwwh
dinwwwh deleted the feat/client/consumeEventIterator-util branch June 22, 2026 01:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lgtm This PR has been approved by a maintainer size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant