Skip to content

fix(sdk): fallback for AbortSignal.any in chat transport on Safari < 17.4 (#5098) - #5107

Closed
ghazy2001 wants to merge 1 commit into
triggerdotdev:mainfrom
ghazy2001:fix/chat-transport-safari-abortsignal-any-5098
Closed

ghazy2001 wants to merge 1 commit into
triggerdotdev:mainfrom
ghazy2001:fix/chat-transport-safari-abortsignal-any-5098

Conversation

@ghazy2001

Copy link
Copy Markdown

Overview

Fixes #5098

On Safari < 17.4 (and Node < 20.3), \AbortSignal.any\ is undefined, causing chat transports (\useTriggerChatTransport, \subscribeToSessionStream,
econnectToStream) to throw:
\
TypeError: AbortSignal.any is not a function. (In 'AbortSignal.any([t,o.signal])', 'AbortSignal.any' is undefined)
\\

Changes

  • **\�nySignal\ helper in \�i-shared.ts**:
    • Delegates to native \AbortSignal.any\ when available.
    • Implements an \AbortController-backed fallback on older browsers and runtimes.
    • Returns immediately if an input signal is already aborted, preserving the abort reason.
    • Automatically cleans up event listeners on all other signals once any signal aborts, preventing memory leaks on reused caller signals.
  • Replaced direct \AbortSignal.any\ call sites:
    • \packages/trigger-sdk/src/v3/chat.ts\ (
      econnectToStream\ and \subscribeToSessionStream)
    • \packages/trigger-sdk/src/v3/chat-client.ts\ (\subscribeToSessionStream)
    • \packages/trigger-sdk/src/v3/chat-server.ts\ (\openHandoverSession)
    • \packages/trigger-sdk/src/v3/ai.ts\ (\chatAgent\ and \createChatSession)
  • Unit Tests:
    • \packages/trigger-sdk/src/v3/ai-shared.test.ts: 8 unit tests covering single signal, empty signal list, native \AbortSignal.any, fallback when \AbortSignal.any\ is undefined (already aborted, first aborted, second aborted, listener cleanup).
    • \packages/trigger-sdk/src/v3/chat.test.ts: test verifying
      econnectToStream\ safely handles aborted signals when \AbortSignal.any\ is deleted.

Verification

  • Ran Vitest suite on \src/v3/ai-shared.test.ts\ and \src/v3/chat.test.ts\ (92/92 passed).
  • Verified TypeScript compilation (\ sc --noEmit) with 0 errors in affected files.

@changeset-bot

changeset-bot Bot commented Nov 10, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 47081d6

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@github-actions

Copy link
Copy Markdown
Contributor

Hi @ghazy2001, thanks for your interest in contributing!

This project requires that pull request authors are vouched, and you are not in the list of vouched users.

This PR will be closed automatically. See https://github.com/triggerdotdev/trigger.dev/blob/main/CONTRIBUTING.md for more details.

@github-actions github-actions Bot closed this Nov 10, 2026
@coderabbitai

coderabbitai Bot commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

Explain Change →

Caution

Review failed

The pull request is closed.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

@devin-ai-integration devin-ai-integration 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.

Devin Review found 5 potential issues.

Devin Review

Comment on lines +321 to +323
for (const signal of validSignals) {
signal.addEventListener("abort", onAbort, { once: true });
}

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.

🟡 Completed turns retain abort listeners

On runtimes without AbortSignal.any, anySignal retains listeners after a normal chat turn completes. Neither runSignal nor the per-turn stop signal aborts, so long-lived runs accumulate listeners and controllers.

Learn more

The fallback subscribes to each input signal and removes those listeners only when an input aborts. A managed chat turn creates a new stop controller and combines it with the run-wide signal in the turn setup. Normal completion does not abort either signal, so the run-wide signal keeps every prior turn's closure, controller, and stop signal alive. The custom session turn also replaces its stop controller without aborting the old one in reset. This differs from native AbortSignal.any, which does not keep an otherwise unreferenced composite alive through strong event listeners.

Example: On a runtime without AbortSignal.any, a chat agent finishes 80 normal turns. Its run-wide signal still holds 80 listeners referencing obsolete stop controllers until the run ends.

Recommended fix: Provide a way to dispose the fallback's source listeners when the composite's operation ends, and invoke it at each turn and subscription teardown. Preserve the existing abort propagation and reason while an operation remains active.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

* with listeners on environments where `AbortSignal.any` is not supported (such as
* Safari < 17.4 or Node < 20.3).
*/
export function anySignal(signals: (AbortSignal | undefined | null)[]): AbortSignal {

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.

🟡 Safari fix lacks a release changeset

The anySignal fallback fixes a user-visible SDK failure, but this PR includes no changeset. The SDK release loses its required patch note.

Learn more

The SDK is a public package, and the new fallback lets customers use chat transports on older Safari versions that previously threw. Repository guidance requires a changeset when a public package fixes a bug users could hit. No changeset is added in this PR.

Example: A customer deciding whether to upgrade for Safari support reads the SDK changelog and finds no entry for the fix.

Recommended fix: Add a patch changeset for @trigger.dev/sdk, phrased in terms of older Safari chat transport compatibility.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

const c1 = new AbortController();
const c2 = new AbortController();

const c2RemoveSpy = vi.spyOn(c2.signal, "removeEventListener");

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.

🟡 Fallback test uses a forbidden spy

The listener-cleanup test calls vi.spyOn, despite the repository's explicit ban on spies in tests. Its assertion also couples the test to listener-removal internals.

Learn more

Example: A fallback implementation that manages listeners through another valid mechanism can fail the test solely because it never calls the spied-on method.

Recommended fix: Replace the spy with an observable behavioral test that covers the listener-lifetime regression without intercepting removeEventListener.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +4 to +5
describe("anySignal", () => {
it("returns a non-aborted signal when signals list is empty", () => {

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.

🔍 Identify each unit test’s regression

The new unit tests lack the required // Catches: <bug> comments. Identify each distinct regression they protect and remove redundant cases.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +2472 to +2473
expect(stream).toBeDefined();
abort.abort();

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.

🔍 Transport abort is not asserted

The Safari test calls abort.abort() without consuming the stream. It cannot detect a subscription that remains open after abort.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

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.

bug: Chat transport crashes on Safari < 17.4: AbortSignal.any is not a function

1 participant