Skip to content

Commit 796a708

Browse files
committed
chore: cleanup
1 parent d85b358 commit 796a708

6 files changed

Lines changed: 212 additions & 111 deletions

File tree

package/expo-package/src/native/multipartUploader.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import NativeStreamMultipartUploader, {
88
type UploadResponse as NativeUploadResponse,
99
} from './NativeStreamMultipartUploader';
1010

11+
import type { NativeMultipartAbortSignal } from '../../../src/native';
12+
1113
const STREAM_MULTIPART_UPLOAD_PROGRESS_EVENT = 'streamMultipartUploadProgress';
1214

1315
const multipartUploadEventEmitter = new NativeEventEmitter(NativeStreamMultipartUploader);
@@ -40,7 +42,7 @@ type MultipartUploadRequest = {
4042
onProgress?: (event: { loaded: number; total?: number }) => void;
4143
parts: UploadPart[];
4244
progress?: UploadProgressConfig;
43-
signal?: AbortSignal;
45+
signal?: NativeMultipartAbortSignal;
4446
uploadId: string;
4547
url: string;
4648
};

package/native-package/src/native/multipartUploader.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import NativeStreamMultipartUploader, {
88
type UploadResponse as NativeUploadResponse,
99
} from './NativeStreamMultipartUploader';
1010

11+
import type { NativeMultipartAbortSignal } from '../../../src/native';
12+
1113
const STREAM_MULTIPART_UPLOAD_PROGRESS_EVENT = 'streamMultipartUploadProgress';
1214

1315
const multipartUploadEventEmitter = new NativeEventEmitter(NativeStreamMultipartUploader);
@@ -40,7 +42,7 @@ type MultipartUploadRequest = {
4042
onProgress?: (event: { loaded: number; total?: number }) => void;
4143
parts: UploadPart[];
4244
progress?: UploadProgressConfig;
43-
signal?: AbortSignal;
45+
signal?: NativeMultipartAbortSignal;
4446
uploadId: string;
4547
url: string;
4648
};

package/src/components/Chat/Chat.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import { NativeHandlers } from '../../native';
2626
import { OfflineDB } from '../../store/OfflineDB';
2727

2828
import type { Streami18n } from '../../utils/i18n/Streami18n';
29-
import { installNativeMultipartInterceptor } from '../../utils/installNativeMultipartInterceptor';
29+
import { installNativeMultipartAdapter } from '../../utils/installNativeMultipartAdapter';
3030
import { version } from '../../version.json';
3131

3232
init();
@@ -242,7 +242,9 @@ const ChatWithContext = (props: PropsWithChildren<ChatProps>) => {
242242
};
243243
}, [client]);
244244

245-
useEffect(() => installNativeMultipartInterceptor(client), [client]);
245+
useEffect(() => {
246+
installNativeMultipartAdapter(client);
247+
}, [client]);
246248

247249
const initialisedDatabase = !!offlineDbInitialized && userID === offlineDbUserId;
248250

package/src/native.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,22 @@ type CompressImage = ({
2828

2929
type DeleteFile = ({ uri }: { uri: string }) => Promise<boolean> | never;
3030

31+
// Axios uses a looser GenericAbortSignal type than the DOM AbortSignal and
32+
// the native multipart path only needs this shared subset for cancellation
33+
export type NativeMultipartAbortSignal = {
34+
aborted: boolean;
35+
addEventListener?: (...args: unknown[]) => unknown;
36+
onabort?: ((...args: unknown[]) => unknown) | null;
37+
removeEventListener?: (...args: unknown[]) => unknown;
38+
};
39+
3140
export type NativeMultipartUploadRequest = {
3241
headers: Record<string, string>;
3342
method: string;
3443
onProgress?: (progress: { loaded: number; total?: number }) => void;
3544
parts: NativeMultipartUploadPart[];
3645
progress?: NativeMultipartUploadProgressConfig;
37-
signal?: AbortSignal;
46+
signal?: NativeMultipartAbortSignal;
3847
url: string;
3948
};
4049

package/src/utils/__tests__/installNativeMultipartInterceptor.test.ts renamed to package/src/utils/__tests__/installNativeMultipartAdapter.test.ts

Lines changed: 118 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import { getTestClient } from '../../mock-builders/mock';
22
import { NativeHandlers } from '../../native';
3-
import { installNativeMultipartInterceptor } from '../installNativeMultipartInterceptor';
3+
import {
4+
installNativeMultipartAdapter,
5+
wrapAxiosAdapterWithNativeMultipart,
6+
} from '../installNativeMultipartAdapter';
47

5-
describe('installNativeMultipartInterceptor', () => {
8+
describe('installNativeMultipartAdapter', () => {
69
const originalMultipartUpload = NativeHandlers.multipartUpload;
710

811
beforeEach(() => {
@@ -14,13 +17,18 @@ describe('installNativeMultipartInterceptor', () => {
1417
});
1518
});
1619

20+
const preserveRequestData = (client: ReturnType<typeof getTestClient>) => {
21+
client.axiosInstance.defaults.transformRequest = [(data) => data];
22+
};
23+
1724
afterEach(() => {
1825
NativeHandlers.multipartUpload = originalMultipartUpload;
1926
jest.clearAllMocks();
2027
});
2128

2229
it('routes multipart requests through the native handler', async () => {
2330
const client = getTestClient();
31+
preserveRequestData(client);
2432
const defaultAdapter = jest.fn().mockResolvedValue({
2533
config: {},
2634
data: 'default',
@@ -31,7 +39,7 @@ describe('installNativeMultipartInterceptor', () => {
3139

3240
client.axiosInstance.defaults.adapter = defaultAdapter;
3341

34-
const dispose = installNativeMultipartInterceptor(client);
42+
installNativeMultipartAdapter(client);
3543
const formData = {
3644
_parts: [
3745
[
@@ -82,12 +90,11 @@ describe('installNativeMultipartInterceptor', () => {
8290
}),
8391
);
8492
expect(response.status).toBe(200);
85-
86-
dispose();
8793
});
8894

89-
it('leaves non-multipart requests on the default adapter', async () => {
95+
it('leaves non-multipart requests on the fallback adapter', async () => {
9096
const client = getTestClient();
97+
preserveRequestData(client);
9198
const defaultAdapter = jest.fn().mockResolvedValue({
9299
config: {},
93100
data: 'default',
@@ -98,14 +105,12 @@ describe('installNativeMultipartInterceptor', () => {
98105

99106
client.axiosInstance.defaults.adapter = defaultAdapter;
100107

101-
const dispose = installNativeMultipartInterceptor(client);
108+
installNativeMultipartAdapter(client);
102109

103110
await client.axiosInstance.post('/messages', { text: 'hello' });
104111

105112
expect(defaultAdapter).toHaveBeenCalled();
106113
expect(NativeHandlers.multipartUpload).not.toHaveBeenCalled();
107-
108-
dispose();
109114
});
110115

111116
it('forwards native upload progress to axios upload progress callbacks', async () => {
@@ -124,7 +129,8 @@ describe('installNativeMultipartInterceptor', () => {
124129
});
125130

126131
const client = getTestClient();
127-
const dispose = installNativeMultipartInterceptor(client);
132+
preserveRequestData(client);
133+
installNativeMultipartAdapter(client);
128134
const onUploadProgress = jest.fn();
129135
const uploadProgress = jest.fn();
130136
const formData = {
@@ -173,12 +179,11 @@ describe('installNativeMultipartInterceptor', () => {
173179
},
174180
}),
175181
);
176-
177-
dispose();
178182
});
179183

180-
it('removes the interceptor on dispose', async () => {
184+
it('uses the final config after user request interceptors run', async () => {
181185
const client = getTestClient();
186+
preserveRequestData(client);
182187
const defaultAdapter = jest.fn().mockResolvedValue({
183188
config: {},
184189
data: 'default',
@@ -189,9 +194,65 @@ describe('installNativeMultipartInterceptor', () => {
189194

190195
client.axiosInstance.defaults.adapter = defaultAdapter;
191196

192-
const dispose = installNativeMultipartInterceptor(client);
197+
const interceptorId = client.axiosInstance.interceptors.request.use((config) => ({
198+
...config,
199+
headers: {
200+
...config.headers,
201+
'X-CDN-Route': 'custom-cdn',
202+
},
203+
url: '/uploads/file',
204+
}));
193205

194-
dispose();
206+
installNativeMultipartAdapter(client);
207+
const formData = {
208+
_parts: [
209+
[
210+
'file',
211+
{
212+
name: 'test.jpg',
213+
type: 'image/jpeg',
214+
uri: 'file:///tmp/test.jpg',
215+
},
216+
],
217+
],
218+
};
219+
220+
await client.axiosInstance.post('/uploads/image', formData, {
221+
headers: {
222+
Authorization: 'token',
223+
},
224+
});
225+
226+
expect(NativeHandlers.multipartUpload).toHaveBeenCalledWith(
227+
expect.objectContaining({
228+
headers: expect.objectContaining({
229+
Authorization: 'token',
230+
'X-CDN-Route': 'custom-cdn',
231+
}),
232+
url: expect.stringContaining('/uploads/file'),
233+
}),
234+
);
235+
expect(defaultAdapter).not.toHaveBeenCalled();
236+
237+
client.axiosInstance.interceptors.request.eject(interceptorId);
238+
});
239+
240+
it('installs only once per client', async () => {
241+
const client = getTestClient();
242+
preserveRequestData(client);
243+
const defaultAdapter = jest.fn().mockResolvedValue({
244+
config: {},
245+
data: 'default',
246+
headers: {},
247+
status: 200,
248+
statusText: 'OK',
249+
});
250+
251+
client.axiosInstance.defaults.adapter = defaultAdapter;
252+
253+
installNativeMultipartAdapter(client);
254+
const installedAdapter = client.axiosInstance.defaults.adapter;
255+
installNativeMultipartAdapter(client);
195256

196257
const formData = {
197258
_parts: [
@@ -208,7 +269,47 @@ describe('installNativeMultipartInterceptor', () => {
208269

209270
await client.axiosInstance.post('/uploads/image', formData);
210271

211-
expect(defaultAdapter).toHaveBeenCalled();
212-
expect(NativeHandlers.multipartUpload).not.toHaveBeenCalled();
272+
expect(client.axiosInstance.defaults.adapter).toBe(installedAdapter);
273+
expect(defaultAdapter).not.toHaveBeenCalled();
274+
expect(NativeHandlers.multipartUpload).toHaveBeenCalled();
275+
});
276+
277+
it('composes explicitly with a custom adapter', async () => {
278+
const client = getTestClient();
279+
preserveRequestData(client);
280+
const customAdapter = jest.fn().mockResolvedValue({
281+
config: {},
282+
data: 'custom',
283+
headers: {},
284+
status: 200,
285+
statusText: 'OK',
286+
});
287+
288+
client.axiosInstance.defaults.adapter = wrapAxiosAdapterWithNativeMultipart(
289+
client,
290+
customAdapter,
291+
);
292+
293+
const multipartFormData = {
294+
_parts: [
295+
[
296+
'file',
297+
{
298+
name: 'test.jpg',
299+
type: 'image/jpeg',
300+
uri: 'file:///tmp/test.jpg',
301+
},
302+
],
303+
],
304+
};
305+
306+
await client.axiosInstance.post('/uploads/image', multipartFormData);
307+
308+
expect(NativeHandlers.multipartUpload).toHaveBeenCalled();
309+
expect(customAdapter).not.toHaveBeenCalled();
310+
311+
await client.axiosInstance.post('/messages', { text: 'hello' });
312+
313+
expect(customAdapter).toHaveBeenCalled();
213314
});
214315
});

0 commit comments

Comments
 (0)