fix(peer, shared): support React Native's AbortSignal polyfill - #116
Conversation
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
@standard-server/aws-lambda
@standard-server/core
@standard-server/fastify
@standard-server/fetch
@standard-server/node
@standard-server/peer
@standard-server/shared
commit: |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Merging this PR will not alter performance
Comparing Footnotes
|
There was a problem hiding this comment.
ℹ️ 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.
throwIfAbortedhelper (packages/shared/src/signal.ts) — newthrowIfAborted(signal)that checkssignal?.abortedand throwssignal.reason, without calling the missing method.- Public export (
packages/shared/src/index.ts) — named export of only the new helper;anyAbortSignalstays internal. - Peer client (
packages/peer/src/client.ts) — bothsignal?.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 withsignal?.reason.- Lint guard (
eslint.config.js) —ban/banbans['*', 'throwIfAborted']. - Tests — new
throwIfAbortedcase insignal.test.tsand 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.
DeepSeek Flash (default — pick a model for stronger reviews) | 𝕏
…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
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — the commit since the prior review, which closes out the one open suggestion.
- Fallback abort reason —
throwIfAbortednow throwssignal.reason ?? new AbortError('This operation was aborted'), so an already-aborted polyfilled signal rejects with anAbortErrorinstead ofundefined. - Consistent
sleeppaths —sleep's later-abort listener applies the sameAbortErrorfallback, so the pre-aborted and aborted-later paths now agree. - Tests — new cases in
signal.test.tsandtime.test.tsshadow a native signal'sreasonwithundefinedto mimic the polyfill and assert theAbortErrorfallback 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.
DeepSeek Flash (default — pick a model for stronger reviews) | 𝕏
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — the commit since the prior review (1bfd2fd): c07a701 reverts the AbortError fallback commit.
- Reverted fallback —
throwIfAbortedis back tothrow signal.reason, so an already-aborted React Native polyfilled signal rejects withundefined;sleep's later-abort listener is back toreject(signal?.reason). - Removed fallback tests — the
AbortError-fallback cases added insignal.test.tsandtime.test.tsare 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.
DeepSeek Flash (default — pick a model for stronger reviews) | 𝕏

ClientPeer.requestcalledsignal.throwIfAborted(). React Native's AbortSignal polyfill (theabort-controllerpackage, installed as the global through React Native 0.86) doesn't implement that method. So every peer request that carried a signal rejected withTypeError: 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 athrowIfAborted(signal)helper to@standard-server/sharedand uses it in the peer client. oRPC can then re-export it and delete its own copy.Fixes
@standard-server/sharednow exportsthrowIfAborted(signal). It behaves likesignal.throwIfAborted()and also works on React Native's polyfill.sleepuses it internally too.ClientPeerrequests 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.x.throwIfAborted()andx?.throwIfAborted()so the method doesn't come back.Testing
throwIfAbortedremoves the method from a native signal to mimic the polyfill.x.throwIfAborted()andx?.throwIfAborted(), so the method can't come back in the peer client or anywhere else.abort-controller@3.0.0: requests failed before this change and resolve after it.pnpm lint,pnpm type:checkandpnpm testpass.Known gap
The polyfill doesn't store the abort reason, so
signal.reasonis alwaysundefined. A request whose signal is already aborted rejects withundefined. One aborted later rejects withAbortError('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