Skip to content

Commit

Permalink
Remove node throttle (#6)
Browse files Browse the repository at this point in the history
* Remove node throttle

* Update readme

* Update type for fulfilled promise

* Bump package
  • Loading branch information
rossmartin authored Nov 28, 2022
1 parent e946abe commit e38fc2b
Show file tree
Hide file tree
Showing 8 changed files with 45 additions and 142 deletions.
22 changes: 11 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ yarn add react-native-use-file-upload

## Example App

There is an example app in this repo as shown in the above gif. It is located within `example` and there is a small node server script within `example/server` [here](example/server/server.ts). You can start the node server within `example` using `yarn server`. The upload route in the node server intentionally throttles requests to help simulate a real world scenario.
There is an example app in this repo as shown in the above gif. It is located within `example` and there is a small node server script within `example/server` [here](example/server/server.ts). You can start the node server within `example` using `yarn server`.

## Usage

Expand Down Expand Up @@ -208,23 +208,23 @@ Requests continue when the app is backgrounded on android but they do not on iOS

The React Native team did a heavy lift to polyfill and bridge `XMLHttpRequest` to the native side for us. [There is an open PR in React Native to allow network requests to run in the background for iOS](https://github.com/facebook/react-native/pull/31838). `react-native-background-upload` is great but if backgrounding can be supported without any external native dependencies it is a win for everyone.

### Why send 1 file at a time instead of multiple in a single request?
### How can I throttle the file uploads so that I can simulate a real world scenario where upload progress takes time?

It is possible to to send multiple files in 1 request. There are downsides to this approach though and the main one is that it is slower. A client has the ability to handle multiple server connections simultaneously, allowing the files to stream in parallel. This folds the upload time over on itself.
You can throttle the file uploads by using [ngrok](https://ngrok.com/) and [Network Link Conditioner](https://developer.apple.com/download/more/?q=Additional%20Tools). Once you have ngrok installed you can start a HTTP tunnel forwarding to the local node server on port 8080 via:

Another downside is fault tolerance. By splitting the files into separate requests, this strategy allows for a file upload to fail in isolation. If the connection fails for the request, or the file is invalidated by the server, or any other reason, that file upload will fail by itself and won't affect any of the other uploads.

### How does the local node server throttle the upload requests?
```sh
ngrok http 8080
```

The local node server throttles the upload requests to simulate a real world scenario on a cellular connection or slower network. This helps test out the progress and timeout handling on the client. It does this by using the [node-throttle](https://github.com/TooTallNate/node-throttle) library. See the `/upload` route in [here](example/server/server.ts) for the details.
ngrok will generate a forwarding URL to the local node server and you should set this as the `url` for `useFileUpload`. This will make your device/simulator make the requests against the ngrok forwarding URL.

### How do I bypass the throttling on the local node server?
You can throttle your connection using Network Link Conditioner if needed. The existing Wifi profile with a 33 Mbps upload works well and you can add a custom profile also. If your upload speed is faster than 100 Mbps you'll see a difference by throttling with Network Link Conditioner. You might not need to throttle with Network Link Conditioner depending on your connection upload speed.

Set the `url` in `useFileUpload` to `http://localhost:8080/_upload`.
### Why send 1 file at a time instead of multiple in a single request?

### The `onDone` and promise from `startUpload` take awhile to resolve in the example app.
It is possible to to send multiple files in 1 request. There are downsides to this approach though and the main one is that it is slower. A client has the ability to handle multiple server connections simultaneously, allowing the files to stream in parallel. This folds the upload time over on itself.

This is because of the throttling and can be bypassed.
Another downside is fault tolerance. By splitting the files into separate requests, this strategy allows for a file upload to fail in isolation. If the connection fails for the request, or the file is invalidated by the server, or any other reason, that file upload will fail by itself and won't affect any of the other uploads.

### Why is `type` and `name` required in the `UploadItem` type?

Expand Down
2 changes: 0 additions & 2 deletions example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,10 @@
"@types/multer": "1.4.7",
"@types/node": "18.11.3",
"@types/react-native-sortable-grid": "2.0.4",
"@types/throttle": "1.0.1",
"babel-plugin-module-resolver": "^4.1.0",
"express": "4.18.2",
"metro-react-native-babel-preset": "0.72.3",
"multer": "1.4.5-lts.1",
"throttle": "1.0.3",
"ts-node": "10.9.1"
}
}
51 changes: 14 additions & 37 deletions example/server/server.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import express from 'express';
import multer from 'multer';
import Throttle from 'throttle';
import http from 'http';
import os from 'os';

const app = express();
Expand All @@ -23,41 +21,20 @@ const upload = multer({
);
});

app.post('/upload', (req, res) => {
console.log('/upload');
console.log(`Received headers: ${JSON.stringify(req.headers)}`);

// Using the throttle lib here to simulate a real world
// scenario on a cellular connection or slower network.
// This helps test out the progress and timeout handling.

// The below pipes the request stream to the throttle
// transform stream. Then it pipes the throttled stream data
// to the "/_upload" route on this same server via http.request
// Finally we pipe the response stream received from the http.request
// to the original response stream on this route.
const throttle = new Throttle(100 * 1024); // 100 kilobytes per second
req.pipe(throttle).pipe(
http.request(
{
host: 'localhost',
path: '/_upload',
port,
method: 'POST',
headers: req.headers,
},
(requestResp) => {
requestResp.pipe(res);
}
)
);
});

app.post('/_upload', upload.single('file'), (req, res) => {
console.log('req.file: ', req.file);
console.log(`Wrote to: ${req.file?.path}`);
res.status(200).send({ path: req.file?.path });
});
app.post(
'/upload',
(req, _res, next) => {
console.log('/upload');
console.log(`Received headers: ${JSON.stringify(req.headers)}`);
return next();
},
upload.single('file'),
(req, res) => {
console.log('req.file: ', req.file);
console.log(`Wrote to: ${req.file?.path}`);
res.status(200).send({ path: req.file?.path });
}
);

return app.listen(port, () =>
console.log(`Server listening on port ${port}!`)
Expand Down
45 changes: 11 additions & 34 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import FastImage from 'react-native-fast-image';

import ProgressBar from './components/ProgressBar';
import useFileUpload, { UploadItem, OnProgressData } from '../../src/index';
import { allSettled, sleep } from './util/general';
import { allSettled } from './util/general';
import placeholderImage from './img/placeholder.png';

const hapticFeedbackOptions: HapticOptions = {
Expand All @@ -44,10 +44,9 @@ export default function App() {
method: 'POST',
timeout: 60000, // you can set this lower to cause timeouts to happen
onProgress,
onDone: (_data) => {
//console.log('onDone, data: ', data);
onDone: ({ item }) => {
updateItem({
item: _data.item,
item,
keysAndValues: [
{
key: 'completedAt',
Expand All @@ -56,20 +55,18 @@ export default function App() {
],
});
},
onError: (_data) => {
//console.log('onError, data: ', data);
onError: ({ item }) => {
updateItem({
item: _data.item,
item,
keysAndValues: [
{ key: 'progress', value: undefined },
{ key: 'failed', value: true },
],
});
},
onTimeout: (_data) => {
//console.log('onTimeout, data: ', data);
onTimeout: ({ item }) => {
updateItem({
item: _data.item,
item,
keysAndValues: [
{ key: 'progress', value: undefined },
{ key: 'failed', value: true },
Expand Down Expand Up @@ -114,30 +111,10 @@ export default function App() {
? Math.round((event.loaded / event.total) * 100)
: 0;

// This logic before the else below is a hack to
// simulate progress for any that upload immediately.
// This is needed after moving to FastImage?!?!
const now = new Date().getTime();
const elapsed = now - item.startedAt!;
if (progress === 100 && elapsed <= 200) {
for (let i = 0; i <= 100; i += 25) {
setData((prevState) => {
const newState = [...prevState];
const itemToUpdate = newState.find((s) => s.uri === item.uri);
if (itemToUpdate) {
// item can fail before this hack is done because of the sleep
itemToUpdate.progress = itemToUpdate.failed ? undefined : i;
}
return newState;
});
await sleep(800);
}
} else {
updateItem({
item,
keysAndValues: [{ key: 'progress', value: progress }],
});
}
updateItem({
item,
keysAndValues: [{ key: 'progress', value: progress }],
});
}

const onPressSelectMedia = async () => {
Expand Down
3 changes: 0 additions & 3 deletions example/src/util/general.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
export const sleep = (time: number) =>
new Promise((resolve) => setTimeout(resolve, time));

export const allSettled = (promises: Promise<any>[]) => {
return Promise.all(
promises.map((promise) =>
Expand Down
54 changes: 2 additions & 52 deletions example/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1170,13 +1170,6 @@
"@types/mime" "*"
"@types/node" "*"

"@types/throttle@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@types/throttle/-/throttle-1.0.1.tgz#cbf88ec5c63b11c6466f1b73e3760ae41dc16e05"
integrity sha512-tb2KFn61P0HBt+X5uMGzqlfoSpctymCPp5pQOUDanj7GThQimvrnerQviYhIxz/+tDMEQgWXQiZlznrGIFBsbw==
dependencies:
"@types/node" "*"

"@types/yargs-parser@*":
version "21.0.0"
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b"
Expand Down Expand Up @@ -1545,14 +1538,6 @@ buffer@^5.5.0:
base64-js "^1.3.1"
ieee754 "^1.1.13"

buffer@^6.0.3:
version "6.0.3"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6"
integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==
dependencies:
base64-js "^1.3.1"
ieee754 "^1.2.1"

busboy@^1.0.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893"
Expand Down Expand Up @@ -1886,7 +1871,7 @@ dayjs@^1.8.15:
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.6.tgz#2e79a226314ec3ec904e3ee1dd5a4f5e5b1c7afb"
integrity sha512-zZbY5giJAinCG+7AGaw0wIhNZ6J8AhWuSXKvuc1KAyMiRsvGQWqh4L+MomvhdAYjN+lqvVCMq1I41e3YHvXkyQ==

debug@2, debug@2.6.9, debug@^2.2.0, debug@^2.3.3:
debug@2.6.9, debug@^2.2.0, debug@^2.3.3:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
Expand Down Expand Up @@ -2048,11 +2033,6 @@ event-target-shim@^5.0.0, event-target-shim@^5.0.1:
resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==

events@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==

execa@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8"
Expand Down Expand Up @@ -2443,7 +2423,7 @@ iconv-lite@0.4.24:
dependencies:
safer-buffer ">= 2.1.2 < 3"

ieee754@^1.1.13, ieee754@^1.2.1:
ieee754@^1.1.13:
version "1.2.1"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
Expand Down Expand Up @@ -3657,11 +3637,6 @@ process-nextick-args@~2.0.0:
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==

process@^0.11.10:
version "0.11.10"
resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==

promise@^8.0.3:
version "8.3.0"
resolved "https://registry.yarnpkg.com/promise/-/promise-8.3.0.tgz#8cb333d1edeb61ef23869fbb8a4ea0279ab60e0a"
Expand Down Expand Up @@ -3825,16 +3800,6 @@ react@18.1.0:
dependencies:
loose-envify "^1.1.0"

"readable-stream@>= 0.3.0":
version "4.2.0"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.2.0.tgz#a7ef523d3b39e4962b0db1a1af22777b10eeca46"
integrity sha512-gJrBHsaI3lgBoGMW/jHZsQ/o/TIWiu5ENCJG1BB7fuCKzpFM8GaS2UoBVt9NO+oI+3FcrBNbUkl3ilDe09aY4A==
dependencies:
abort-controller "^3.0.0"
buffer "^6.0.3"
events "^3.3.0"
process "^0.11.10"

readable-stream@^2.2.2, readable-stream@~2.3.6:
version "2.3.7"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
Expand Down Expand Up @@ -4258,13 +4223,6 @@ statuses@~1.5.0:
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==

"stream-parser@>= 0.0.2":
version "0.3.1"
resolved "https://registry.yarnpkg.com/stream-parser/-/stream-parser-0.3.1.tgz#1618548694420021a1182ff0af1911c129761773"
integrity sha512-bJ/HgKq41nlKvlhccD5kaCr/P+Hu0wPNKPJOH7en+YrJu/9EgqUF+88w5Jb6KNcjOFMhfX4B2asfeAtIGuHObQ==
dependencies:
debug "2"

streamsearch@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764"
Expand Down Expand Up @@ -4363,14 +4321,6 @@ throat@^5.0.0:
resolved "https://registry.yarnpkg.com/throat/-/throat-5.0.0.tgz#c5199235803aad18754a667d659b5e72ce16764b"
integrity sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==

throttle@1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/throttle/-/throttle-1.0.3.tgz#8a32e4a15f1763d997948317c5ebe3ad8a41e4b7"
integrity sha512-VYINSQFQeFdmhCds0tTqvQmLmdAjzGX1D6GnRQa4zlq8OpTtWSMddNyRq8Z4Snw/d6QZrWt9cM/cH8xTiGUkYA==
dependencies:
readable-stream ">= 0.3.0"
stream-parser ">= 0.0.2"

through2@^2.0.1:
version "2.0.5"
resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd"
Expand Down
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-native-use-file-upload",
"version": "0.1.4",
"version": "0.1.5",
"description": "A hook for uploading files using multipart form data with React Native. Provides a simple way to track upload progress, abort an upload, and handle timeouts. Written in TypeScript and no dependencies required.",
"main": "lib/commonjs/index",
"module": "lib/module/index",
Expand Down Expand Up @@ -38,7 +38,11 @@
"keywords": [
"react-native",
"ios",
"android"
"android",
"file",
"upload",
"uploader",
"photo"
],
"repository": "https://github.com/rossmartin/react-native-use-file-upload",
"author": "Ross Martin <2498502+rossmartin@users.noreply.github.com> (https://github.com/rossmartin)",
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/useFileUpload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export default function useFileUpload<T extends UploadItem = UploadItem>({
[key: string]: XMLHttpRequest;
}>({});

const startUpload = (item: T): Promise<OnDoneData<T> | OnErrorData<T>> => {
const startUpload = (item: T): Promise<OnDoneData<T>> => {
return new Promise((resolve, reject) => {
const formData = new FormData();
formData.append(field, item);
Expand Down

0 comments on commit e38fc2b

Please sign in to comment.