Simplify fetchWithTimeout + add tests for utils

This commit is contained in:
ponas committed 2026-07-30 01:45:32 +02:00
1 parent b63ca008eb
commit 39fdfef6f1
3 files changed
+45 -21

No files matched your search

-3
View File
@@ -105,9 +105,6 @@ history, code and binary blobs related to React Native functionality filtered ou
- try [`@expo/ui`](https://docs.expo.dev/versions/latest/sdk/ui/), production-ready as of SDK 56: - try [`@expo/ui`](https://docs.expo.dev/versions/latest/sdk/ui/), production-ready as of SDK 56:
real SwiftUI / Jetpack Compose components that could back the homegrown ones in real SwiftUI / Jetpack Compose components that could back the homegrown ones in
`src/components/` and finally retire `react-native-simple-radio-button` `src/components/` and finally retire `react-native-simple-radio-button`
- simplify `fetchWithTimeout` in `src/utils.ts`: `expo/fetch` is now `globalThis.fetch`, so the
wrapper collapses to `fetch(url, { signal: AbortSignal.timeout(ms) })`. Opt back out with
`EXPO_PUBLIC_USE_RN_FETCH=1` if it misbehaves
- swap `@react-native-async-storage/async-storage` (used by `redux-persist` in - swap `@react-native-async-storage/async-storage` (used by `redux-persist` in
`src/reduxStore.ts`) for `src/reduxStore.ts`) for
[`expo-sqlite/localStorage`](https://docs.expo.dev/versions/latest/sdk/sqlite/#localstorage), [`expo-sqlite/localStorage`](https://docs.expo.dev/versions/latest/sdk/sqlite/#localstorage),
+43 -1
View File
@@ -1,4 +1,4 @@
import { withAppSource } from '@/utils'; import { fetchWithTimeout, snakeToCamelCase, withAppSource } from '@/utils';
describe('withAppSource', () => { describe('withAppSource', () => {
it('tags links to our own sites with UTM params', () => { it('tags links to our own sites with UTM params', () => {
@@ -40,3 +40,45 @@ describe('withAppSource', () => {
expect(withAppSource('')).toBe(''); expect(withAppSource('')).toBe('');
}); });
}); });
describe('snakeToCamelCase', () => {
it('converts snake_case to camelCase', () => {
expect(snakeToCamelCase('date_time_start')).toBe('dateTimeStart');
expect(snakeToCamelCase('is_member')).toBe('isMember');
});
it('leaves strings without underscores untouched', () => {
expect(snakeToCamelCase('events')).toBe('events');
expect(snakeToCamelCase('dateTimeStart')).toBe('dateTimeStart');
});
});
describe('fetchWithTimeout', () => {
const realFetch = globalThis.fetch;
afterEach(() => {
globalThis.fetch = realFetch;
});
it('forwards the url and options and attaches an abort signal', async () => {
const fetchMock = jest.fn().mockResolvedValue({ ok: true });
globalThis.fetch = fetchMock as unknown as typeof fetch;
await fetchWithTimeout('https://galtinn.neuf.no/api/me/', { method: 'POST' });
expect(fetchMock).toHaveBeenCalledWith(
'https://galtinn.neuf.no/api/me/',
expect.objectContaining({ method: 'POST', signal: expect.any(AbortSignal) }),
);
});
it('rejects with a TimeoutError that aborts the request when the timeout elapses', async () => {
globalThis.fetch = ((_url: string, options?: RequestInit) =>
new Promise((_resolve, reject) => {
options?.signal?.addEventListener('abort', () => reject(options.signal?.reason));
})) as unknown as typeof fetch;
await expect(fetchWithTimeout('https://galtinn.neuf.no/api/me/', undefined, 10)).rejects.toMatchObject({
name: 'TimeoutError',
});
});
});
+2 -17
View File
@@ -25,22 +25,7 @@ export function snakeToCamelCase(val: string): string {
} }
export function fetchWithTimeout(url: string, options?: RequestInit, timeout = 10000) { export function fetchWithTimeout(url: string, options?: RequestInit, timeout = 10000) {
/** Wraps fetch to allow it to time out */ /** Fetch that rejects with a TimeoutError and aborts the request after `timeout` ms */
// FIXME: show timeout message in UI // FIXME: show timeout message in UI
return new Promise<Response>((resolve, reject) => { return fetch(url, { ...options, signal: AbortSignal.timeout(timeout) });
const timer = setTimeout(() => {
reject(new Error('Fetch request timed out'));
}, timeout);
fetch(url, options).then(
(response) => {
clearTimeout(timer);
resolve(response);
},
(error) => {
clearTimeout(timer);
reject(error);
},
);
});
} }