Skip to content

fix(client): DedupeRequestsPlugin not working well with Event Iterator - #921

Merged
dinwwwh merged 3 commits into
mainfrom
fix/client/dedupe-request-plugin-and-event-iterator
Aug 23, 2025
Merged

fix(client): DedupeRequestsPlugin not working well with Event Iterator#921
dinwwwh merged 3 commits into
mainfrom
fix/client/dedupe-request-plugin-and-event-iterator

Conversation

@dinwwwh

@dinwwwh dinwwwh commented Aug 23, 2025

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features

    • Added a way to detect buffered items in per-ID queues.
  • Bug Fixes

    • Improved reliability when multiple consumers concurrently advance replicated async iterators and during error propagation; initialization now behaves consistently under parallel access. No user-facing API changes.
  • Refactor

    • Simplified internal iterator replication and ID handling for clearer behavior.
  • Tests

    • Expanded tests to cover concurrent advancement, parallel error handling, and buffered-item checks.

@dosubot dosubot Bot added the size:M This PR changes 30-99 lines, ignoring generated files. label Aug 23, 2025
@coderabbitai

coderabbitai Bot commented Aug 23, 2025

Copy link
Copy Markdown

Walkthrough

Refactors AsyncIdQueue internals and replicateAsyncIterator to use clearer naming and string IDs; adds AsyncIdQueue.hasBufferedItems and waiterIds tests; updates replicateAsyncIterator error propagation and per-replica logic; tests adjusted to assert the first two next() calls concurrently and with lazy iterator initialization in with-async-iterator.

Changes

Cohort / File(s) Summary
AsyncIdQueue internals + API
packages/shared/src/queue.ts
Renamed internal maps (itemsqueues, pendingPullswaiters) and added hasBufferedItems(id); no public-signature changes.
AsyncIdQueue tests
packages/shared/src/queue.test.ts
Added tests for waiterIds and hasBufferedItems, covering concurrent pulls/pushes and buffered-state transitions.
replicateAsyncIterator implementation
packages/shared/src/iterator.ts
Switched id handling to string IDs, removed defer import, renamed error param to reason, set error object and closed waiter queues on error, used queue.pull(id) and string ids consistently, changed termination check to if (!queue.length); public API unchanged.
replicateAsyncIterator tests
packages/shared/src/iterator.test.ts
Updated success and error-path tests: the first two next() calls are asserted concurrently via Promise.all(...); error-path delay increased and expectations adjusted (different terminal behavior for replicas).
with-async-iterator logic
packages/standard-server/src/utils.ts
Removed early fast-path returning existing iterator; switched to lazy init replicatedAsyncIteratorObjects ??= replicateAsyncIterator(body, count) and then shift from it when appropriate.
with-async-iterator tests
packages/standard-server/src/utils.test.ts
Changed test to assert the first two body() results concurrently using Promise.all(...).resolves.toEqual(...), preserving remaining assertions and spy checks.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Caller
  participant Wrapper as withAsyncIterator
  participant Body as body()
  participant Replicator as replicateAsyncIterator
  participant Queue as AsyncIdQueue
  participant Replica1 as Replica(id:'0')
  participant Replica2 as Replica(id:'1')
  participant ReplicaN as Replica(id:'2')

  Caller->>Wrapper: call wrapper()
  Wrapper->>Body: resolve body()
  alt body is AsyncIterator
    Wrapper->>Replicator: replicateAsyncIterator(body, count)
    Replicator->>Queue: open("0"), open("1"), open("2")
    Replicator->>Body: source.next() (loop)
    Body-->>Replicator: item / throws reason
    Replicator->>Queue: push(item) for each id
    Note right of Queue: replicas may have buffered items
    par concurrent pulls
      Replica1->>Queue: pull("0")
      Replica2->>Queue: pull("1")
    end
    alt no error
      Queue-->>Replica1: return item
      Queue-->>Replica2: return item
    else on error(reason)
      Replicator->>Queue: close(waiterIds, reason)
      Queue-->>ReplicaX: reject(reason) if waiting and not buffered
    end
    Wrapper->>ReplicaN: shift() -> returns iterator instance
    ReplicaN-->>Caller: AsyncIterator result(s)
  else non-iterator
    Wrapper-->>Caller: body result
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • unnoq/orpc#582: Modifies the same async-iterator and AsyncIdQueue logic (replicateAsyncIterator, queue) — strong overlap.
  • unnoq/orpc#619: Also refactors replicateAsyncIterator internals and iterator handling — closely related.

Suggested labels

size:M

Poem

A rabbit taps the queue with gentle paws,
IDs now strings — tidy, without pause.
Two hops at once, the tests clap and cheer,
Lazy sprouts yield iterators near.
I nibble bugs and bound away — hooray, next() is here! 🥕

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.


📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 02fa79a and 6110cb1.

📒 Files selected for processing (1)
  • packages/shared/src/iterator.ts (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/shared/src/iterator.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). (4)
  • GitHub Check: lint
  • GitHub Check: publish-commit
  • GitHub Check: test
  • GitHub Check: Analyze (javascript-typescript)
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/client/dedupe-request-plugin-and-event-iterator

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@vercel

vercel Bot commented Aug 23, 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 Aug 23, 2025 1:03pm

@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 addresses a bug where the DedupeRequestsPlugin was not functioning correctly with Event Iterator streams. The problem stemmed from an improper re-initialization of replicated async iterators within the replicateStandardLazyResponse utility. The fix ensures that the underlying replicateAsyncIterator is invoked only once per response, guaranteeing consistent stream behavior for multiple consumers. Additionally, the changes include updates to related tests to properly validate concurrent asynchronous operations, preventing future regressions.

Highlights

  • Fixing Async Iterator Replication Logic: The core issue was an incorrect re-initialization of replicated async iterators within the replicateStandardLazyResponse utility. This has been fixed by ensuring that replicateAsyncIterator is called only once per response body using the nullish coalescing operator (??=), preventing inconsistent behavior when multiple consumers access the same stream.
  • Improved Concurrent Test Coverage: Tests for both replicateAsyncIterator and replicateStandardLazyResponse have been updated to use Promise.all for concurrent next() and body() calls. This change ensures that the tests accurately reflect and validate real-world parallel consumption scenarios, preventing potential race conditions in the test suite.
  • Minor Code Refactoring: Minor refactorings were applied in iterator.ts to enhance code consistency and readability. This includes caching id.toString() and simplifying queue.close calls.
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 Aug 23, 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 addresses an issue where DedupeRequestsPlugin was not working correctly with event iterators, likely due to a race condition. The core of the fix is in replicateStandardLazyResponse, where a race condition is resolved by using a nullish coalescing assignment (??=). This ensures that replicateAsyncIterator is invoked only once, even when multiple consumers access the body concurrently. The accompanying test changes are excellent, as they now use Promise.all to simulate parallel execution and verify the fix. The other changes are minor refactorings that improve code clarity. Overall, this is a solid fix that correctly addresses the underlying issue.

@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 (6)
packages/standard-server/src/utils.ts (2)

72-85: Avoid order-dependent shift; map replicas deterministically and in O(1).

shift() ties iterator selection to call order across replicas and does O(n) array work per call. Capture the loop index and pick by index to:

  • Guarantee replica i consistently gets iterators[i] regardless of call timing.
  • Remove subtle scheduling dependencies in tests and production.
  • Avoid the O(n) shift cost.

Apply this refactor:

-  for (let i = 0; i < count; i++) {
-    replicated.push({
+  for (let i = 0; i < count; i++) {
+    const idx = i
+    replicated.push({
       ...response,
       body: once(async () => {
         const body = await (bodyPromise ??= response.body())
         if (!isAsyncIteratorObject(body)) {
           return body
         }
-        replicatedAsyncIteratorObjects ??= replicateAsyncIterator(body, count)
-        return replicatedAsyncIteratorObjects.shift()
+        replicatedAsyncIteratorObjects ??= replicateAsyncIterator(body, count)
+        return replicatedAsyncIteratorObjects[idx]!
       }),
     })
   }

2-2: Import the AsyncIteratorObject type if it isn’t globally available.

replicatedAsyncIteratorObjects is typed as AsyncIteratorObject<...>[] here but the type isn’t imported in this file. If your TS config doesn’t expose it globally, add a type-only import:

-import { isAsyncIteratorObject, once, replicateAsyncIterator, toArray, tryDecodeURIComponent } from '@orpc/shared'
+import type { AsyncIteratorObject } from '@orpc/shared'
+import { isAsyncIteratorObject, once, replicateAsyncIterator, toArray, tryDecodeURIComponent } from '@orpc/shared'

If AsyncIteratorObject is globally declared, you can ignore this.

Also applies to: 69-71

packages/shared/src/iterator.ts (2)

156-160: Rename shadowed “id” for readability.

In replicated.every((_, id) => ...), id is a numeric index that shadows the outer string id. Rename to index to avoid confusion:

-          if (replicated.every((_, id) => !queue.isOpen(id.toString()))) {
+          if (replicated.every((_, index) => !queue.isOpen(index.toString()))) {

115-121: Micro-optimization: precompute string IDs once.

If count is modest this is negligible, but you can avoid repeated toString() calls by precomputing ids:

   const replicated: AsyncIteratorClass<T, TReturn, TNext>[] = []
+  const ids = Array.from({ length: count }, (_, i) => i.toString())

   const start = once(async () => {
     try {
       while (true) {
         const item = await source.next()
-        for (let i = 0; i < count; i++) {
-          const id = i.toString()
+        for (let i = 0; i < count; i++) {
+          const id = ids[i]
           if (queue.isOpen(id)) {
             queue.push(id, item)
           }
         }
         if (item.done) break
-  for (let i = 0; i < count; i++) {
-    const id = i.toString()
+  for (let i = 0; i < count; i++) {
+    const id = ids[i]
     queue.open(id)
     replicated.push(new AsyncIteratorClass(
       () => {
         start()
         return new Promise((resolve, reject) => {
-          queue.pull(id)
+          queue.pull(id)
             .then(resolve)
             .catch(reject)
           defer(() => {
             if (error) {
               reject(error.value)
             }
           })
         })
       },
       async (reason) => {
-        queue.close({ id })
+        queue.close({ id })
         if (reason !== 'next') {
-          if (replicated.every((_, index) => !queue.isOpen(index.toString()))) {
+          if (replicated.every((_, index) => !queue.isOpen(ids[index]))) {
             await source?.return?.()
           }
         }
       },
     ))
   }

Also applies to: 133-137, 156-160

packages/standard-server/src/utils.test.ts (2)

100-104: Optional: assert lazy creation right after replicateStandardLazyResponse.

To harden the test’s intent (no eager replication), assert the spy count immediately after constructing replicated and before invoking body():

   const replicated = replicateStandardLazyResponse(response, 3)
   expect(replicated.length).toBe(3)
+  expect(replicateAsyncIteratorSpy).toHaveBeenCalledTimes(0)

112-118: Consider testing out-of-order consumption.

An additional check where replicated[2].body() is invoked before replicated[0]/[1] would ensure iterator selection remains correct even when consumers arrive out of order.

Do you want me to draft that test case?

Also applies to: 120-124

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d7c4772 and 29907f0.

📒 Files selected for processing (4)
  • packages/shared/src/iterator.test.ts (2 hunks)
  • packages/shared/src/iterator.ts (3 hunks)
  • packages/standard-server/src/utils.test.ts (1 hunks)
  • packages/standard-server/src/utils.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
packages/standard-server/src/utils.ts (1)
packages/shared/src/iterator.ts (1)
  • replicateAsyncIterator (100-166)
⏰ 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
🔇 Additional comments (6)
packages/standard-server/src/utils.ts (1)

69-85: Lazily initializing replicated iterators fixes the race with Event Iterator.

Using bodyPromise ??= response.body() and replicatedAsyncIteratorObjects ??= replicateAsyncIterator(...) inside once(...) ensures:

  • response.body() is invoked at most once even under concurrent body() calls.
  • replicateAsyncIterator is created exactly once per response body.
    This aligns with the PR goal (dedup + SSE) and removes the eager path that previously raced. Good change.
packages/shared/src/iterator.ts (2)

115-121: String ID reuse is simpler and less error-prone.

Consolidating id = i.toString() and reusing it across queue operations removes repeated conversions and keeps identifiers consistent across open/push/pull/close. Looks good.


133-137: Consistently use the precomputed string id.

Using const id = i.toString() once per replica and passing id into queue.pull is the right direction and avoids mismatches. Nice cleanup.

Also applies to: 142-142

packages/standard-server/src/utils.test.ts (1)

114-118: Parallel body() assertions validate the intended concurrency path.

Good addition. This catches regressions where iterator replication creates more than once or where selection depends on evaluation order.

packages/shared/src/iterator.test.ts (2)

266-269: Nice: exercising initial parallel next() calls.

This better reflects real-world concurrent consumers and guards against serialization bugs in replication.


320-323: Parallel rejection checks cover late-error propagation well.

Good to see concurrency verified in the error path too.

@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. and removed size:M This PR changes 30-99 lines, ignoring generated files. labels Aug 23, 2025

@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

Caution

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

⚠️ Outside diff range comments (1)
packages/shared/src/queue.test.ts (1)

89-103: Bug in test: pullPromise2 listens on the wrong queue ID.

This test intends to validate custom-reason rejection for both per-ID close and global close. However, both pullPromise1 and pullPromise2 pull from queueId1, so the second assertion doesn’t actually exercise the global close path.

Fix by pulling on queueId2 for the second promise:

-    const pullPromise1 = queue.pull(queueId1)
-    const pullPromise2 = queue.pull(queueId1)
+    const pullPromise1 = queue.pull(queueId1)
+    const pullPromise2 = queue.pull(queueId2)
🧹 Nitpick comments (5)
packages/shared/src/queue.ts (3)

34-53: Avoid O(n) Array.shift() in hot paths.

Both the waiter and item queues use shift(), which is O(n) due to reindexing. If these queues can grow (e.g., bursts from an event stream), consider using a small ring buffer or tracking a head index to make pops O(1).

Optional direction (no API change, keeps semantics):

  • Replace Map<string, T[]> with Map<string, { buf: T[]; head: number }> and advance head instead of shifting.
  • Compact occasionally when head crosses a threshold.

Happy to draft a minimal refactor if you want to pursue this.


55-82: Promise waiter bookkeeping is solid; consider minor clarity tweak.

The waiter enqueuing logic is correct and deletes the waiters entry when it drains. As a small readability nit, rename the local waitingPulls (Line 71) to waitersForId to match the waiters map naming.


84-105: Close semantics are consistent; add an option to discard buffered items with a reason?

Current behavior:

  • Per-ID close rejects pending pulls for that ID and drops buffered items.
  • Global close rejects all pending pulls and clears everything.

This is good. If upper layers need to propagate an error reason to consumers that might otherwise drain buffered items after an upstream failure, you could expose a closeAll(reason) convenience or a clearBufferedItems(id?, reason?) method so higher layers don't need to iterate waiterIds. Not required for this PR—just a thought based on usage in replicateAsyncIterator.

packages/shared/src/iterator.ts (2)

137-166: Minor readability tweak in “all replicas closed?” check.

The every callback parameter is named id (numeric index), which shadows the outer string id variable and can be confusing. Use idx for the numeric index to make intent explicit:

-          if (replicated.every((_, id) => !queue.isOpen(id.toString()))) {
+          if (replicated.every((_, idx) => !queue.isOpen(idx.toString()))) {

156-163: Source cleanup trigger looks right; consider catching return() errors.

Calling source?.return?.() only when all queues are closed is correct. If return() can throw (some sources do on abort), you might want to catch and ignore it here to avoid surfacing cleanup errors to consumers after they already decided to stop:

-          if (replicated.every((_, idx) => !queue.isOpen(idx.toString()))) {
-            await source?.return?.()
-          }
+          if (replicated.every((_, idx) => !queue.isOpen(idx.toString()))) {
+            try {
+              await source?.return?.()
+            } catch { /* noop: best-effort cleanup */ }
+          }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 29907f0 and 02fa79a.

📒 Files selected for processing (4)
  • packages/shared/src/iterator.test.ts (3 hunks)
  • packages/shared/src/iterator.ts (3 hunks)
  • packages/shared/src/queue.test.ts (1 hunks)
  • packages/shared/src/queue.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/shared/src/iterator.test.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
🔇 Additional comments (6)
packages/shared/src/queue.ts (1)

15-21: Nice, lightweight observability for waiters and buffers.

The new waiterIds getter and hasBufferedItems(id) are clean and minimal. These make the queue’s state easier to reason about from higher layers (e.g., iterator replication/error propagation) without leaking internals. Looks good.

packages/shared/src/queue.test.ts (2)

136-152: waiterIds test is precise and valuable.

Great coverage for insertion order and cleanup. Verifies the exact keys and that the set clears after resolution. Nice.


154-174: hasBufferedItems test accurately exercises FIFO buffering across IDs.

Good end-to-end assertions before/after pulls and across isolated IDs. This will catch regressions in queue bookkeeping.

packages/shared/src/iterator.ts (3)

115-121: Replicating to string IDs aligns with AsyncIdQueue and removes toString noise.

The switch to string IDs (and checking queue.isOpen(id) before push) is clean and avoids accidental pushes after a replica closes. Good change.


128-134: Error propagation: closing only waiter queues is a good balance.

Catching reason, storing it in error, then closing just waiterIds ensures:

  • Waiting replicas reject immediately with the upstream reason.
  • Replicas with buffered items can still drain before surfacing the error on the next call.

This matches expected iterator semantics for many streaming sources. Nicely done.


145-154: TOCTOU guard around hasBufferedItems vs pull is acceptable here.

Because each replica has its own per-ID queue and AsyncIteratorClass serializes next() calls via sequential, the “check then pull” is safe from cross-replica interference. If you ever allow multiple concurrent next() calls per replica, you’d want to invert logic (attempt pull and handle immediate rejection) to avoid a tiny window. Not needed today.

If you plan to relax serialization in the future, I can prototype a resilient pullOrError(id, error) helper to encapsulate this pattern.

@pkg-pr-new

pkg-pr-new Bot commented Aug 23, 2025

Copy link
Copy Markdown
More templates

@orpc/arktype

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

@orpc/client

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

@orpc/contract

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

@orpc/experimental-durable-event-iterator

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

@orpc/hey-api

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

@orpc/interop

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

@orpc/json-schema

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

@orpc/nest

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

@orpc/openapi

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

@orpc/openapi-client

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

@orpc/otel

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

@orpc/react

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

@orpc/react-query

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

@orpc/experimental-react-swr

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

@orpc/server

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

@orpc/shared

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

@orpc/solid-query

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

@orpc/standard-server

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

@orpc/standard-server-aws-lambda

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

@orpc/standard-server-fetch

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

@orpc/standard-server-node

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

@orpc/standard-server-peer

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

@orpc/svelte-query

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

@orpc/tanstack-query

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

@orpc/trpc

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

@orpc/valibot

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

@orpc/vue-colada

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

@orpc/vue-query

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

@orpc/zod

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

commit: 6110cb1

@dinwwwh
dinwwwh merged commit b38be35 into main Aug 23, 2025
11 checks passed
@dinwwwh
dinwwwh deleted the fix/client/dedupe-request-plugin-and-event-iterator branch June 22, 2026 01:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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