Skip to content

Conversation

Rhayxz
Copy link

@Rhayxz Rhayxz commented Aug 31, 2025

Why

With short-lived JWTs, the client would reconnect endlessly using a stale token embedded in the ws URL query param, causing repeated 401s.

What

  • Provide PartySocket with a URL provider function instead of a static string.
  • Snapshot the original interceptor call (path/input/context/next) and re-invoke it to refetch a fresh token on every reconnect.
  • Build a new ws URL with the new token for each attempt.
  • Keep existing plugin behavior and API; no breaking changes.

How

  • First connection uses the initially returned token.
  • On reconnect, call the upstream interceptor again to obtain a fresh token, then reconnect with that token.

Testing

  • Verified locally with 10s JWT expiry: after expiry, reconnect first POSTs to the token-issuing endpoint, then opens a ws with the new token. RPC calls and event streaming resume without user intervention.

Notes

  • Compatible with ClientRetryPlugin.
  • Improves security posture for short-lived/rotating tokens and forced revocations.

Summary by CodeRabbit

  • New Features

    • Seamless, resilient real-time streaming with automatic token refresh and reconnects.
    • Single, unified WebSocket flow to reduce interruptions.
  • Bug Fixes

    • Prevents failures when integration context is missing or corrupted.
    • Eliminates stale-token disconnects for more stable sessions.
  • Refactor

    • Consolidated configuration into a single options object for simpler setup.
    • Iterator initialization streamlined with an explicit initial token.

Copy link

vercel bot commented Aug 31, 2025

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

Project Deployment Preview Comments Updated (UTC)
orpc Ready Ready Preview Comment Aug 31, 2025 0:34am

@dosubot dosubot bot added the size:M This PR changes 30-99 lines, ignoring generated files. label Aug 31, 2025
Copy link

coderabbitai bot commented Aug 31, 2025

Walkthrough

Refactors the Durable Event Iterator plugin to use a single options object, introduces a two-phase interceptor strategy, implements token lifecycle management with initial and refresh tokens, constructs URLs dynamically, and establishes a reconnectable WebSocket flow using token-based PartySocket URLs with added plugin context validation.

Changes

Cohort / File(s) Summary of Changes
Durable Iterator Plugin refactor
packages/durable-event-iterator/src/client/plugin.ts
Constructor now accepts a single options object and restructures internal destructuring; replaces prior interceptor path with two-phase interceptors; adds plugin context validation; implements initialToken capture, refetchToken, token-aware URL builder, and ReconnectableWebSocket using PartySocket; updates public API to pass token via initial token.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Client
  participant Plugin
  participant ClientInterceptor as Client Interceptor (Phase 1)
  participant Interceptor as Interceptor (Phase 2)
  participant Upstream as Upstream next()
  participant TokenStore as Token Lifecycle
  participant WS as ReconnectableWebSocket
  participant PartySocket as PartySocket Server

  Client->>Plugin: request()
  Plugin->>ClientInterceptor: validate plugin context<br/>mark DEI token header in context
  ClientInterceptor-->>Plugin: proceed

  Plugin->>Interceptor: execute next()
  Interceptor->>Upstream: initial token request
  Upstream-->>Interceptor: response (token)
  Interceptor->>TokenStore: save initialToken

  Note over TokenStore: buildUrl(token)<br/>refetchToken() calls next() with snapshot

  Plugin->>WS: create with URL provider<br/>(uses initialToken first)
  WS->>PartySocket: connect with tokenized URL
  PartySocket-->>WS: events/acks

  loop Reconnects / Expiry
    WS->>TokenStore: need fresh token
    TokenStore->>Upstream: refetchToken() via snapshot
    Upstream-->>TokenStore: new token
    TokenStore-->>WS: provide new tokenized URL
    WS->>PartySocket: reconnect with refreshed token
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

I nibble on tokens, crunchy and bright,
Hop through sockets in the moonlit night.
First bite’s initial, next nibble’s refresh,
URLs woven in a burrowed mesh.
Interceptors align, context held tight—
Reconnect, respawn, the stream stays right. 🐇✨

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.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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 or @coderabbit 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.

Copy link
Contributor

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

Choose a reason for hiding this comment

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

Summary of Changes

Hello @Rhayxz, 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 critical issue where clients using short-lived JSON Web Tokens (JWTs) would continuously fail to reconnect to WebSocket services due to using expired tokens. The solution implements a robust token refresh mechanism for auto-reconnections by leveraging PartySocket's URL provider, ensuring that a fresh JWT is obtained before each reconnection attempt, thereby maintaining continuous service without user intervention.

Highlights

  • Problem Addressed: Resolves an issue where short-lived JWTs caused clients to endlessly attempt reconnections with stale tokens, resulting in repeated 401 Unauthorized errors.
  • Dynamic URL Provisioning: The PartySocket (PartyKit's ReconnectableWebSocket) is now provided with an asynchronous URL provider function instead of a static string.
  • On-Demand Token Refresh: A snapshot of the original interceptor call is taken, allowing it to be re-invoked to fetch a fresh JWT whenever a reconnection is needed.
  • Resilient Connection Establishment: A new WebSocket URL is dynamically constructed with the newly acquired token for each reconnection attempt, ensuring successful authentication.
  • Non-Breaking Change: The implementation maintains existing plugin behavior and API, introducing no breaking changes, and enhances security for short-lived or rotating tokens.
  • Verified Functionality: Local testing confirmed that after JWT expiry, the system successfully fetches a new token and resumes RPC calls and event streaming automatically.
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.

Copy link
Contributor

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

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 mechanism to refresh JWTs on WebSocket auto-reconnects for the Durable Event Iterator. The approach of using a URL provider function with PartySocket is well-suited for this purpose, and the implementation correctly snapshots the original call to refetch tokens. This is a great improvement for handling short-lived tokens and enhances security.

I have a couple of suggestions. One is a potential issue regarding a stale token reference in the created durableIterator, and another is a minor style improvement for better maintainability. Additionally, it would be beneficial to add tests covering the new token refresh logic on reconnect to prevent future regressions.

Comment on lines +58 to +59
if (!ctx)
throw new TypeError('[DurableEventIteratorLinkPlugin] Plugin context has been corrupted or modified by another plugin or interceptor')
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

For improved code clarity and to prevent potential bugs, it's a good practice to always use curly braces for if statements, even for single-line blocks. This ensures that the code's intent is clear and reduces the risk of errors if the block is expanded in the future.

Suggested change
if (!ctx)
throw new TypeError('[DurableEventIteratorLinkPlugin] Plugin context has been corrupted or modified by another plugin or interceptor')
if (!ctx) {
throw new TypeError('[DurableEventIteratorLinkPlugin] Plugin context has been corrupted or modified by another plugin or interceptor')
}

Comment on lines 149 to 151
const durableIterator = createClientDurableEventIterator(iterator, link, {
token,
token: initialToken,
})
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The durableIterator is created here with the initialToken string. This token value is passed by value and will become stale after the first token refresh on WebSocket reconnect. Consequently, any call to getClientDurableEventIteratorToken(durableIterator) will return the original, stale token.

While the core RPC functionality remains correct because ReconnectableWebSocket handles token refreshes independently, this behavior could be misleading for consumers of getClientDurableEventIteratorToken.

The comment on line 115 (// keep latest for visibility) suggests an intent to keep the token updated. If this is the case, createClientDurableEventIterator would need to be adjusted to accept a token provider function (e.g., () => initialToken) instead of a static string. This would allow it to always access the latest token but would require changes in packages/durable-event-iterator/src/client/event-iterator.ts.

If returning the initial token is the intended and acceptable behavior, consider clarifying the comment on line 115 to something like // keep latest for subsequent reconnects to better reflect its purpose and avoid confusion.

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: 2

🧹 Nitpick comments (3)
packages/durable-event-iterator/src/client/plugin.ts (3)

81-83: Type-check the token at runtime.

If the server mistakenly returns a non-string while setting the DEI header, downstream will misbehave. Guard early.

-      let initialToken = output as string
+      if (typeof output !== 'string')
+        throw new TypeError('[DurableEventIteratorLinkPlugin] Expected token as string from DEI endpoint')
+      let initialToken = output

100-104: Support relative URLs (optional).

new URL(await value(this.url)) fails for relative paths. Provide a base when available.

-      const buildUrl = async (token: string): Promise<string> => {
-        const u = new URL(await value(this.url))
+      const buildUrl = async (token: string): Promise<string> => {
+        const raw = await value(this.url)
+        const base = (globalThis as any)?.location?.href
+        const u = new URL(String(raw), base)
         u.searchParams.set(DURABLE_EVENT_ITERATOR_TOKEN_PARAM, token)
         return u.toString()
       }

123-130: Avoid double-injecting ClientRetryPlugin.

If user supplies a ClientRetryPlugin, adding another can cause duplicate retries. Detect and add only if absent.

-      const durableLink = new RPCLink<ClientRetryPluginContext>({
-        ...this.linkOptions,
-        websocket: durableWs,
-        plugins: [
-          ...toArray(this.linkOptions.plugins),
-          new ClientRetryPlugin(),
-        ],
-      })
+      const userPlugins = toArray(this.linkOptions.plugins)
+      const hasRetry = userPlugins.some(p => p instanceof ClientRetryPlugin)
+      const durableLink = new RPCLink<ClientRetryPluginContext>({
+        ...this.linkOptions,
+        websocket: durableWs,
+        plugins: hasRetry ? userPlugins : [...userPlugins, new ClientRetryPlugin()],
+      })
📜 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 216e5a5 and 4eb532d.

📒 Files selected for processing (1)
  • packages/durable-event-iterator/src/client/plugin.ts (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
packages/durable-event-iterator/src/client/plugin.ts (3)
packages/client/src/adapters/standard/plugin.ts (1)
  • StandardLinkPlugin (4-7)
packages/client/src/adapters/standard/link.ts (1)
  • StandardLinkOptions (18-22)
packages/durable-event-iterator/src/consts.ts (3)
  • DURABLE_EVENT_ITERATOR_PLUGIN_HEADER_KEY (2-2)
  • DURABLE_EVENT_ITERATOR_PLUGIN_HEADER_VALUE (3-3)
  • DURABLE_EVENT_ITERATOR_TOKEN_PARAM (1-1)
🔇 Additional comments (4)
packages/durable-event-iterator/src/client/plugin.ts (4)

66-75: Context forwarding pattern LGTM.

Passing a dedicated plugin context symbol through interceptorOptions.next(...) is correct and isolates state.


106-122: Dynamic PartySocket URL provider: solid approach.

This matches the PR goal: refresh token per reconnect while using the initial token for the first dial.


149-151: Confirm iterator token semantics.

createClientDurableEventIterator(..., { token: initialToken }) receives the initial token only. Confirm it’s used solely for the first handshake and not for subsequent operations, since refresh is handled by PartySocket URL provider.


35-49: All instantiations use the new single-opts signature; no legacy calls detected.

Comment on lines +55 to +64
// Mark responses that carry a DEI token
options.clientInterceptors.push(async (clientOptions) => {
const ctx = clientOptions.context[this.CONTEXT_SYMBOL] as DurableEventIteratorLinkPluginContext | undefined
if (!ctx)
throw new TypeError('[DurableEventIteratorLinkPlugin] Plugin context has been corrupted or modified by another plugin or interceptor')

const output = await options.next({
...options,
const res = await clientOptions.next()
ctx.isDurableEventIteratorResponse = res.headers[DURABLE_EVENT_ITERATOR_PLUGIN_HEADER_KEY] === DURABLE_EVENT_ITERATOR_PLUGIN_HEADER_VALUE
return res
})
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

Do not throw when plugin context is absent; pass-through instead (and normalize header lookup).

This client interceptor runs for all requests. Throwing when ctx is missing will break non-DEI calls. Make it a no-op when context isn’t present and perform a case-insensitive header read.

-    options.clientInterceptors.push(async (clientOptions) => {
-      const ctx = clientOptions.context[this.CONTEXT_SYMBOL] as DurableEventIteratorLinkPluginContext | undefined
-      if (!ctx)
-        throw new TypeError('[DurableEventIteratorLinkPlugin] Plugin context has been corrupted or modified by another plugin or interceptor')
-
-      const res = await clientOptions.next()
-      ctx.isDurableEventIteratorResponse = res.headers[DURABLE_EVENT_ITERATOR_PLUGIN_HEADER_KEY] === DURABLE_EVENT_ITERATOR_PLUGIN_HEADER_VALUE
-      return res
-    })
+    options.clientInterceptors.push(async (clientOptions) => {
+      const ctx = clientOptions.context[this.CONTEXT_SYMBOL] as DurableEventIteratorLinkPluginContext | undefined
+      const res = await clientOptions.next()
+      if (ctx) {
+        const headers = res.headers as Record<string, string> | undefined
+        const headerValue =
+          headers?.[DURABLE_EVENT_ITERATOR_PLUGIN_HEADER_KEY] ??
+          headers?.[String(DURABLE_EVENT_ITERATOR_PLUGIN_HEADER_KEY).toLowerCase()]
+        ctx.isDurableEventIteratorResponse = headerValue === DURABLE_EVENT_ITERATOR_PLUGIN_HEADER_VALUE
+      }
+      return res
+    })
📝 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
// Mark responses that carry a DEI token
options.clientInterceptors.push(async (clientOptions) => {
const ctx = clientOptions.context[this.CONTEXT_SYMBOL] as DurableEventIteratorLinkPluginContext | undefined
if (!ctx)
throw new TypeError('[DurableEventIteratorLinkPlugin] Plugin context has been corrupted or modified by another plugin or interceptor')
const output = await options.next({
...options,
const res = await clientOptions.next()
ctx.isDurableEventIteratorResponse = res.headers[DURABLE_EVENT_ITERATOR_PLUGIN_HEADER_KEY] === DURABLE_EVENT_ITERATOR_PLUGIN_HEADER_VALUE
return res
})
// Mark responses that carry a DEI token
options.clientInterceptors.push(async (clientOptions) => {
const ctx = clientOptions.context[this.CONTEXT_SYMBOL] as DurableEventIteratorLinkPluginContext | undefined
const res = await clientOptions.next()
if (ctx) {
const headers = res.headers as Record<string, string> | undefined
const headerValue =
headers?.[DURABLE_EVENT_ITERATOR_PLUGIN_HEADER_KEY] ??
headers?.[String(DURABLE_EVENT_ITERATOR_PLUGIN_HEADER_KEY).toLowerCase()]
ctx.isDurableEventIteratorResponse = headerValue === DURABLE_EVENT_ITERATOR_PLUGIN_HEADER_VALUE
}
return res
})
🤖 Prompt for AI Agents
In packages/durable-event-iterator/src/client/plugin.ts around lines 55 to 64,
the client interceptor currently throws when the plugin context is missing and
checks headers case-sensitively; change it to be a no-op when ctx is undefined
(simply await and return clientOptions.next() without throwing or modifying ctx)
so it doesn’t break non-DEI requests, and when reading the response header
perform a case-insensitive lookup (e.g., normalize header keys or compare
lowercased header names/values) before setting
ctx.isDurableEventIteratorResponse.

Comment on lines +84 to +99
// Save a snapshot of this exact call so we can re-fetch fresh tokens later
const upstreamNext = interceptorOptions.next
const snapshot = {
path: interceptorOptions.path,
input: interceptorOptions.input,
context: { [this.CONTEXT_SYMBOL]: pluginContext, ...interceptorOptions.context },
signal: interceptorOptions.signal,
lastEventId: interceptorOptions.lastEventId,
}

const durableWs = new ReconnectableWebSocket(url.toString(), undefined, {
WebSocket: this.WebSocket,
})
const refetchToken = async (): Promise<string> => {
const fresh = await upstreamNext(snapshot)
// Server sets the header + returns the token string again.
return fresh as string
}

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Avoid reusing the original AbortSignal; also dedupe concurrent refreshes.

Reusing interceptorOptions.signal can cause immediate aborts on refresh. Build fresh options per call. Also, coalesce parallel refresh attempts.

-      const upstreamNext = interceptorOptions.next
-      const snapshot = {
-        path: interceptorOptions.path,
-        input: interceptorOptions.input,
-        context: { [this.CONTEXT_SYMBOL]: pluginContext, ...interceptorOptions.context },
-        signal: interceptorOptions.signal,
-        lastEventId: interceptorOptions.lastEventId,
-      }
-
-      const refetchToken = async (): Promise<string> => {
-        const fresh = await upstreamNext(snapshot)
-        // Server sets the header + returns the token string again.
-        return fresh as string
-      }
+      const upstreamNext = interceptorOptions.next
+      const makeSnapshot = () => ({
+        path: interceptorOptions.path,
+        input: interceptorOptions.input,
+        // Fresh context on each refresh; do not reuse potentially-aborted signals.
+        context: { [this.CONTEXT_SYMBOL]: pluginContext, ...interceptorOptions.context },
+      })
+
+      let inflightToken: Promise<string> | null = null
+      const refetchToken = async (): Promise<string> => {
+        if (!inflightToken) {
+          inflightToken = upstreamNext(makeSnapshot()) as Promise<string>
+          try {
+            const fresh = await inflightToken
+            return fresh
+          } finally {
+            inflightToken = null
+          }
+        }
+        return inflightToken
+      }
📝 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
// Save a snapshot of this exact call so we can re-fetch fresh tokens later
const upstreamNext = interceptorOptions.next
const snapshot = {
path: interceptorOptions.path,
input: interceptorOptions.input,
context: { [this.CONTEXT_SYMBOL]: pluginContext, ...interceptorOptions.context },
signal: interceptorOptions.signal,
lastEventId: interceptorOptions.lastEventId,
}
const durableWs = new ReconnectableWebSocket(url.toString(), undefined, {
WebSocket: this.WebSocket,
})
const refetchToken = async (): Promise<string> => {
const fresh = await upstreamNext(snapshot)
// Server sets the header + returns the token string again.
return fresh as string
}
// Save a snapshot of this exact call so we can re-fetch fresh tokens later
const upstreamNext = interceptorOptions.next
const makeSnapshot = () => ({
path: interceptorOptions.path,
input: interceptorOptions.input,
// Fresh context on each refresh; do not reuse potentially-aborted signals.
context: { [this.CONTEXT_SYMBOL]: pluginContext, ...interceptorOptions.context },
})
let inflightToken: Promise<string> | null = null
const refetchToken = async (): Promise<string> => {
if (!inflightToken) {
inflightToken = upstreamNext(makeSnapshot()) as Promise<string>
try {
const fresh = await inflightToken
return fresh
} finally {
inflightToken = null
}
}
return inflightToken
}
🤖 Prompt for AI Agents
In packages/durable-event-iterator/src/client/plugin.ts around lines 84 to 99,
the refetchToken function reuses interceptorOptions.signal and replays the
original options object, which can cause immediate aborts and duplicate
concurrent refreshes; fix by creating fresh options for each refetch call (clone
path, input, context, lastEventId but set signal to a new
AbortController().signal or undefined) and call upstreamNext with that fresh
options object, and implement a simple dedupe/coalesce so parallel refetchToken
invocations share a single in-flight Promise (store the Promise on the plugin
instance or closure, return it if present, and clear it once resolved or
rejected).

Copy link

codecov bot commented Aug 31, 2025

Codecov Report

❌ Patch coverage is 72.22222% with 15 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...ckages/durable-event-iterator/src/client/plugin.ts 72.22% 15 Missing ⚠️

📢 Thoughts on this report? Let us know!

Copy link

pkg-pr-new bot commented Aug 31, 2025

More templates

@orpc/arktype

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

@orpc/client

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

@orpc/contract

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

@orpc/experimental-durable-event-iterator

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

@orpc/hey-api

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

@orpc/interop

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

@orpc/json-schema

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

@orpc/nest

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

@orpc/openapi

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

@orpc/openapi-client

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

@orpc/otel

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

@orpc/react

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

@orpc/react-query

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

@orpc/experimental-react-swr

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

@orpc/server

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

@orpc/shared

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

@orpc/solid-query

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

@orpc/standard-server

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

@orpc/standard-server-aws-lambda

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

@orpc/standard-server-fetch

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

@orpc/standard-server-node

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

@orpc/standard-server-peer

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

@orpc/svelte-query

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

@orpc/tanstack-query

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

@orpc/trpc

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

@orpc/valibot

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

@orpc/vue-colada

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

@orpc/vue-query

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

@orpc/zod

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

commit: 4eb532d

@@ -101,24 +147,10 @@ export class DurableEventIteratorLinkPlugin<T extends ClientContext> implements
}

const durableIterator = createClientDurableEventIterator(iterator, link, {
token,
token: initialToken,
Copy link
Owner

Choose a reason for hiding this comment

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

We require change createClientDurableEventIterator to reflect exactly what is current token.

WebSocket: this.WebSocket,
})
const refetchToken = async (): Promise<string> => {
const fresh = await upstreamNext(snapshot)
Copy link
Owner

Choose a reason for hiding this comment

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

Just call interceptorOptions.next() here no need snapshot I believe

// Turn the token into a resilient iterator (PartySocket-powered)
options.interceptors.push(async (interceptorOptions) => {
const pluginContext: DurableEventIteratorLinkPluginContext = {}
const output = await interceptorOptions.next({
Copy link
Owner

Choose a reason for hiding this comment

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

I think we can group first call .next inside refetchToken (maybe rename refetchToken too)

@unnoq
Copy link
Owner

unnoq commented Sep 14, 2025

I've created #965 with a different approach that better addresses this issue.

@unnoq unnoq closed this Sep 14, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
size:M This PR changes 30-99 lines, ignoring generated files.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants