Skip to content

fix(peer, shared): support React Native's AbortSignal polyfill - #116

Merged
dinwwwh merged 3 commits into
mainfrom
claude/peaceful-newton-jjiq76
Sep 28, 2026
Merged

dinwwwh merged 3 commits into
mainfrom
claude/peaceful-newton-jjiq76

Conversation

@dinwwwh

@dinwwwh dinwwwh commented Sep 28, 2026 •

Copy link
Copy Markdown
Member

ClientPeer.request called signal.throwIfAborted(). React Native's AbortSignal polyfill (the abort-controller package, installed as the global through React Native 0.86) doesn't implement that method. So every peer request that carried a signal rejected with TypeError: signal.throwIfAborted is not a function, which broke oRPC's WebSocket link and batch plugin on every Expo SDK it supports (53–57). This PR adds a throwIfAborted(signal) helper to @standard-server/shared and uses it in the peer client. oRPC can then re-export it and delete its own copy.

Fixes

  • @standard-server/shared now exports throwIfAborted(signal). It behaves like signal.throwIfAborted() and also works on React Native's polyfill. sleep uses it internally too.
  • ClientPeer requests with a signal work on React Native again, both for the check when a request starts and for the check after the body is encoded.
  • A new lint rule flags x.throwIfAborted() and x?.throwIfAborted() so the method doesn't come back.

Testing

  • A new test for throwIfAborted removes the method from a native signal to mimic the polyfill.
  • The new lint rule flags x.throwIfAborted() and x?.throwIfAborted(), so the method can't come back in the peer client or anywhere else.
  • Checked locally against the real abort-controller@3.0.0: requests failed before this change and resolve after it.
  • pnpm lint, pnpm type:check and pnpm test pass.

Known gap

The polyfill doesn't store the abort reason, so signal.reason is always undefined. A request whose signal is already aborted rejects with undefined. One aborted later rejects with AbortError('Request was aborted'). Either way the caller's own reason is lost. This PR leaves that behavior as is.

🤖 Generated with Claude Code

https://claude.ai/code/session_01XyXPRKSY5pB8LgT5HirRZr

React Native 0.86 and earlier install the abort-controller package as the
global AbortSignal, which has no throwIfAborted. ClientPeer.request called
signal.throwIfAborted(), so every peer request carrying a signal rejected
with a TypeError on React Native.

Add throwIfAborted(signal) to @standard-server/shared, which only relies
on aborted and reason, and use it in the peer client, sleep, and the Node
test harnesses. A ban/ban lint rule flags any new x.throwIfAborted() call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XyXPRKSY5pB8LgT5HirRZr
@pkg-pr-new

pkg-pr-new Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
@standard-server/aws-lambda

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

@standard-server/core

npm i https://pkg.pr.new/@standard-server/core@116

@standard-server/fastify

npm i https://pkg.pr.new/@standard-server/fastify@116

@standard-server/fetch

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

@standard-server/node

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

@standard-server/peer

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

@standard-server/shared

npm i https://pkg.pr.new/@standard-server/shared@116

commit: c07a701

@codecov

codecov Bot commented Sep 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@codspeed

codspeed Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 26 untouched benchmarks
⏩ 108 skipped benchmarks1


Comparing claude/peaceful-newton-jjiq76 (c07a701) with main (3c2ffb2)2

Open in CodSpeed

Footnotes

  1. 108 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

  2. No successful run was found on main (ae775be) during the generation of this report, so 3c2ffb2 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report. ↩

@pullfrog pullfrog 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.

ℹ️ No critical issues. One minor suggestion inline — the fix itself is correct and well covered.

Reviewed changes — supporting React Native's AbortSignal polyfill, which has no throwIfAborted.

  • throwIfAborted helper (packages/shared/src/signal.ts) — new throwIfAborted(signal) that checks signal?.aborted and throws signal.reason, without calling the missing method.
  • Public export (packages/shared/src/index.ts) — named export of only the new helper; anyAbortSignal stays internal.
  • Peer client (packages/peer/src/client.ts) — both signal?.throwIfAborted() sites (request start, post-encode) use the helper.
  • sleep (packages/shared/src/time.ts) — the pre-aborted check uses the helper; the later-abort listener still rejects with signal?.reason.
  • Lint guard (eslint.config.js) — ban/ban bans ['*', 'throwIfAborted'].
  • Tests — new throwIfAborted case in signal.test.ts and the node http2/https harnesses migrated to the helper.

I verified the lint rule fires on x.throwIfAborted(), x?.throwIfAborted(), x?.throwIfAborted?.(), and (x as any).throwIfAborted(), while leaving the bare throwIfAborted(x) call alone; pnpm run lint is clean and the new test passes.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (default — pick a model for stronger reviews) | 𝕏

Comment thread packages/shared/src/signal.ts
…ason

React Native's AbortSignal polyfill never sets a reason, so throwIfAborted
threw undefined and sleep rejected with undefined. Both now fall back to an
AbortError, matching the native default for abort() without a reason.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XyXPRKSY5pB8LgT5HirRZr
…as no reason"

This reverts commit 1bfd2fd. The fix only needs to avoid calling the
missing throwIfAborted method; the abort reason stays as the signal gives it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XyXPRKSY5pB8LgT5HirRZr

@pullfrog pullfrog 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.

✅ No new issues found.

Reviewed changes — the commit since the prior review, which closes out the one open suggestion.

  • Fallback abort reason — throwIfAborted now throws signal.reason ?? new AbortError('This operation was aborted'), so an already-aborted polyfilled signal rejects with an AbortError instead of undefined.
  • Consistent sleep paths — sleep's later-abort listener applies the same AbortError fallback, so the pre-aborted and aborted-later paths now agree.
  • Tests — new cases in signal.test.ts and time.test.ts shadow a native signal's reason with undefined to mimic the polyfill and assert the AbortError fallback on both paths.

The new tests are discriminating: pre-fix, throw signal.reason would throw undefined and fail the toThrow(AbortError) assertion. I reran signal.test.ts + time.test.ts (13 pass), tsc -b for @standard-server/shared, and eslint on the changed files — all clean.

Pullfrog  | View workflow run | Using DeepSeek Flash (default — pick a model for stronger reviews) | 𝕏

@dinwwwh
dinwwwh merged commit c6342d0 into main Sep 28, 2026
8 checks passed

@pullfrog pullfrog 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.

✅ No new issues found.

Reviewed changes — the commit since the prior review (1bfd2fd): c07a701 reverts the AbortError fallback commit.

  • Reverted fallback — throwIfAborted is back to throw signal.reason, so an already-aborted React Native polyfilled signal rejects with undefined; sleep's later-abort listener is back to reject(signal?.reason).
  • Removed fallback tests — the AbortError-fallback cases added in signal.test.ts and time.test.ts are removed with the revert.

This returns the PR to the state previously reviewed at 768c91d, and the resulting behavior is now explicitly documented in the PR body's Known gap (already-aborted → undefined; aborted later → AbortError('Request was aborted') via ClientPeer.abortById, packages/peer/src/client.ts:257). The revert is clean and complete — no leftover imports — and signal.test.ts + time.test.ts pass (11), with tsc -b and eslint clean.

Pullfrog  | View workflow run | Using DeepSeek Flash (default — pick a model for stronger reviews) | 𝕏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants