Skip to content

Commit 6830f4a

Browse files
VIVAAN-DHAWANSumit Dhawan
andauthored
fix: use text response path for React Native fetches (#970)
Co-authored-by: Sumit Dhawan <sumitdhawan@Sumits-MacBook-Air.local>
1 parent 09de0c3 commit 6830f4a

8 files changed

Lines changed: 110 additions & 1 deletion

File tree

.changeset/fresh-phones-fix.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@powersync/react-native': patch
3+
---
4+
5+
Force React Native remote HTTP requests onto the text response path so RN 0.85 does not drop non-streaming response bodies.

packages/react-native/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"build": "tsc -b && rollup -c rollup.config.mjs",
1919
"build:prod": "tsc -b && rollup -c rollup.config.mjs",
2020
"clean": "rm -rf lib dist tsconfig.tsbuildinfo",
21+
"test": "vitest --config vitest.config.ts",
2122
"watch": "tsc -b -w",
2223
"test:exports": "attw --pack ."
2324
},

packages/react-native/src/sync/stream/ReactNativeRemote.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ import { fetch } from 'react-native-fetch-api';
1515

1616
export const STREAMING_POST_TIMEOUT_MS = 30_000;
1717

18+
type ReactNativeFetchOptions = RequestInit & {
19+
reactNative?: Record<string, unknown>;
20+
};
21+
1822
/**
1923
* Directly imports fetch implementation from react-native-fetch-api.
2024
* This removes the requirement for the global `fetch` to be overridden by
@@ -38,6 +42,21 @@ export class ReactNativeRemote extends AbstractRemote {
3842
});
3943
}
4044

45+
get fetch(): FetchImplementation {
46+
const fetchImplementation = super.fetch;
47+
return ((input, init) => {
48+
const options = (init ?? {}) as ReactNativeFetchOptions;
49+
50+
return fetchImplementation(input, {
51+
...options,
52+
reactNative: {
53+
...(options.reactNative ?? {}),
54+
textStreaming: true
55+
}
56+
} as RequestInit);
57+
}) as FetchImplementation;
58+
}
59+
4160
getUserAgent(): string {
4261
return [
4362
super.getUserAgent(),
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const fetch = async () => {
2+
throw new Error('react-native-fetch-api should be injected in tests');
3+
};
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export const Platform = {
2+
OS: 'ios',
3+
Version: '26',
4+
constants: {
5+
reactNativeVersion: {
6+
major: 0,
7+
minor: 85
8+
}
9+
}
10+
};
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { describe, expect, it, vi } from 'vitest';
2+
3+
import { ReactNativeRemote } from '../../src/sync/stream/ReactNativeRemote';
4+
5+
const connector = {
6+
fetchCredentials: async () => ({
7+
endpoint: 'https://example.com',
8+
token: 'token'
9+
})
10+
};
11+
12+
function jsonResponse(body: unknown): Response {
13+
return {
14+
ok: true,
15+
status: 200,
16+
statusText: 'OK',
17+
json: async () => body,
18+
text: async () => JSON.stringify(body)
19+
} as Response;
20+
}
21+
22+
describe('ReactNativeRemote', () => {
23+
it('uses text streaming for non-streaming GET requests', async () => {
24+
const fetchImplementation = vi.fn(async () => jsonResponse({ ok: true }));
25+
const remote = new ReactNativeRemote(connector, undefined, { fetchImplementation });
26+
27+
await remote.get('/write-checkpoint2.json');
28+
29+
expect(fetchImplementation).toHaveBeenCalledWith(
30+
'https://example.com/write-checkpoint2.json',
31+
expect.objectContaining({
32+
method: 'GET',
33+
reactNative: expect.objectContaining({
34+
textStreaming: true
35+
})
36+
})
37+
);
38+
});
39+
40+
it('uses text streaming for non-streaming POST requests', async () => {
41+
const fetchImplementation = vi.fn(async () => jsonResponse({ ok: true }));
42+
const remote = new ReactNativeRemote(connector, undefined, { fetchImplementation });
43+
44+
await remote.post('/crud', { op: 'put' });
45+
46+
expect(fetchImplementation).toHaveBeenCalledWith(
47+
'https://example.com/crud',
48+
expect.objectContaining({
49+
method: 'POST',
50+
reactNative: expect.objectContaining({
51+
textStreaming: true
52+
})
53+
})
54+
);
55+
});
56+
});

packages/react-native/tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,6 @@
1414
{
1515
"path": "../react"
1616
}
17-
]
17+
],
18+
"include": ["src/**/*"]
1819
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import path from 'path';
2+
import { defineConfig } from 'vitest/config';
3+
4+
export default defineConfig({
5+
resolve: {
6+
alias: {
7+
'react-native': path.resolve(__dirname, './tests/mocks/react-native.ts'),
8+
'react-native-fetch-api': path.resolve(__dirname, './tests/mocks/react-native-fetch-api.ts')
9+
}
10+
},
11+
test: {
12+
include: ['tests/**/*.test.ts']
13+
}
14+
});

0 commit comments

Comments
 (0)