Skip to content

Commit f6fe2a3

Browse files
sunnylqmclaude
andcommitted
fix(types): type-check the shipped source against React Native 0.87
The package's "types" entry is src/index.ts, so consumer projects compile this source with their own compiler options and skipLibCheck does not cover it. Against RN 0.87's globals it failed in three places: - client.ts requestCheckResult merged the caller's AbortSignal into the RequestInit. With RN 0.87 plus DOM-style fetch types (@types/bun, which loads Node's), the global AbortSignal and RequestInit.signal come from different declarations and the spread is rejected (onabort nullable vs not). fetchWithTimeout now takes the caller's signal as its own argument and chains it as before; params.signal still works. - setTimeout(resolve) in the logger wait and retry backoff: RN 0.87 types the callback as () => void. Wrap the resolve call. - core.ts used `global`, which RN 0.87's typings no longer declare (only @types/node did). Declare it module-locally; no runtime change. Verified with tsc on RN 0.87.1 and react-native-tvos 0.87.1-1 projects (template tsconfig; with types ["bun"], ["jest","node"]; with the DOM lib) and an Expo 58 preview project. With the 10.58.0 source, the plain template fails on setTimeout and `global`, types ["bun"] fails on the signal, and the DOM-lib and Expo configs fail on `global`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 83869ff commit f6fe2a3

4 files changed

Lines changed: 44 additions & 8 deletions

File tree

‎src/__tests__/utils.test.ts‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,30 @@ describe('fetchWithTimeout', () => {
148148
expect(capturedSignal?.aborted).toBe(true);
149149
});
150150

151+
test('a caller signal passed separately cancels the request, not as a timeout', async () => {
152+
let capturedSignal: AbortSignal | undefined;
153+
(globalThis as any).fetch = mock((_url: string, params: any) => {
154+
capturedSignal = params?.signal;
155+
return new Promise((_, reject) => {
156+
params?.signal?.addEventListener('abort', () =>
157+
reject(new Error('Aborted'))
158+
);
159+
});
160+
});
161+
const caller = new AbortController();
162+
163+
const pending = fetchWithTimeout(
164+
'https://example.com/slow',
165+
{ method: 'POST' },
166+
10_000,
167+
caller.signal
168+
);
169+
caller.abort();
170+
171+
await expect(pending).rejects.toThrow('Aborted');
172+
expect(capturedSignal?.aborted).toBe(true);
173+
});
174+
151175
test('resolves normally before the timeout', async () => {
152176
const response = { ok: true } as Response;
153177
(globalThis as any).fetch = mock(async () => response);

‎src/client.ts‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -742,8 +742,8 @@ export class Pushy {
742742
let timer: ReturnType<typeof setTimeout> | undefined;
743743
await Promise.race([
744744
this.loggerPromise.promise,
745-
new Promise((resolve) => {
746-
timer = setTimeout(resolve, 10 * 1000);
745+
new Promise<void>((resolve) => {
746+
timer = setTimeout(() => resolve(), 10 * 1000);
747747
}),
748748
]);
749749
clearTimeout(timer);
@@ -977,8 +977,9 @@ export class Pushy {
977977
) => {
978978
const resp = await fetchWithTimeout(
979979
this.getCheckUrl(endpoint),
980-
signal ? { ...fetchPayload, signal } : fetchPayload,
981-
DEFAULT_FETCH_TIMEOUT_MS
980+
fetchPayload,
981+
DEFAULT_FETCH_TIMEOUT_MS,
982+
signal
982983
);
983984

984985
if (!resp.ok) {
@@ -1532,7 +1533,7 @@ export class Pushy {
15321533
(0.75 + Math.random() * 0.5)
15331534
);
15341535
log(`retry attempt ${attempt}/${maxRetries}, waiting ${backoffMs}ms`);
1535-
await new Promise((r) => setTimeout(r, backoffMs));
1536+
await new Promise<void>((r) => setTimeout(() => r(), backoffMs));
15361537
errorMessages.length = 0;
15371538
errorMessages.push(...exhaustedStrategies.values());
15381539
lastError = undefined;

‎src/core.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,20 @@ import { UpdateError } from './error';
33
import i18n from './i18n';
44
import { emptyModule, error, log } from './utils';
55

6+
// RN's JS global object. Declared locally because RN 0.87's own typings no
7+
// longer define `global` (only @types/node did), and this source is
8+
// type-checked in consumer projects; the module-scoped declaration also
9+
// shadows @types/node's without conflicting.
10+
declare const global: any;
11+
612
/* eslint-disable @react-native/no-deep-imports */
713
const {
814
version: v,
915
} = require('react-native/Libraries/Core/ReactNativeVersion');
1016
const RNVersion = `${v.major}.${v.minor}.${v.patch}`;
1117
const isTurboModuleEnabled =
1218
// https://github.com/facebook/react-native/pull/48362
13-
(global as any).__turboModuleProxy || (global as any).RN$Bridgeless;
19+
global.__turboModuleProxy || global.RN$Bridgeless;
1420

1521
const isWebPlatform = Platform.OS === 'web';
1622

‎src/utils.ts‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,11 @@ export const computeProgress = (received: number, total: number): number =>
132132
export const fetchWithTimeout = (
133133
url: string,
134134
params: Parameters<typeof fetch>[1],
135-
timeoutMs = DEFAULT_FETCH_TIMEOUT_MS
135+
timeoutMs = DEFAULT_FETCH_TIMEOUT_MS,
136+
// Taken apart from params: merging it into the RequestInit only type-checks
137+
// when the global AbortSignal and fetch's RequestInit come from the same
138+
// declarations, which RN 0.87 globals mixed with DOM/Node/bun types break.
139+
signal?: AbortSignal
136140
): Promise<Response> => {
137141
// AbortController landed in the RN fetch polyfill around 0.60; we support
138142
// older peers, so fall back to a plain timer race when it is unavailable
@@ -161,7 +165,8 @@ export const fetchWithTimeout = (
161165
// The timeout controller replaces params.signal on the fetch call, so a
162166
// caller-provided signal (e.g. the hedged endpoint race cancelling losers)
163167
// must be chained onto it manually.
164-
const externalSignal = (params as any)?.signal as AbortSignal | undefined;
168+
const externalSignal =
169+
signal ?? ((params as any)?.signal as AbortSignal | undefined);
165170
const onExternalAbort = () => controller.abort();
166171
if (externalSignal?.aborted) {
167172
controller.abort();

0 commit comments

Comments
 (0)