From e38fc2b0810b0af53a65002bd94fb5b052b1b651 Mon Sep 17 00:00:00 2001 From: Ross Martin <2498502+rossmartin@users.noreply.github.com> Date: Sun, 27 Nov 2022 18:28:50 -0600 Subject: [PATCH] Remove node throttle (#6) * Remove node throttle * Update readme * Update type for fulfilled promise * Bump package --- README.md | 22 +++++++-------- example/package.json | 2 -- example/server/server.ts | 51 ++++++++++------------------------- example/src/App.tsx | 45 ++++++++----------------------- example/src/util/general.ts | 3 --- example/yarn.lock | 54 ++----------------------------------- package.json | 8 ++++-- src/hooks/useFileUpload.ts | 2 +- 8 files changed, 45 insertions(+), 142 deletions(-) diff --git a/README.md b/README.md index 9d25ad0..259a8d2 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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? diff --git a/example/package.json b/example/package.json index e2753a8..afb8cf9 100644 --- a/example/package.json +++ b/example/package.json @@ -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" } } diff --git a/example/server/server.ts b/example/server/server.ts index 043ef11..7dfbba4 100644 --- a/example/server/server.ts +++ b/example/server/server.ts @@ -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(); @@ -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}!`) diff --git a/example/src/App.tsx b/example/src/App.tsx index a3e7358..147e1d6 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -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 = { @@ -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', @@ -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 }, @@ -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 () => { diff --git a/example/src/util/general.ts b/example/src/util/general.ts index 0ddbb3e..e4f736c 100644 --- a/example/src/util/general.ts +++ b/example/src/util/general.ts @@ -1,6 +1,3 @@ -export const sleep = (time: number) => - new Promise((resolve) => setTimeout(resolve, time)); - export const allSettled = (promises: Promise[]) => { return Promise.all( promises.map((promise) => diff --git a/example/yarn.lock b/example/yarn.lock index 21263de..9eff281 100644 --- a/example/yarn.lock +++ b/example/yarn.lock @@ -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" @@ -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" @@ -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== @@ -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" @@ -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== @@ -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" @@ -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" @@ -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" @@ -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" diff --git a/package.json b/package.json index 50d6385..14d88ea 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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)", diff --git a/src/hooks/useFileUpload.ts b/src/hooks/useFileUpload.ts index bf9fbf5..dae9de3 100644 --- a/src/hooks/useFileUpload.ts +++ b/src/hooks/useFileUpload.ts @@ -21,7 +21,7 @@ export default function useFileUpload({ [key: string]: XMLHttpRequest; }>({}); - const startUpload = (item: T): Promise | OnErrorData> => { + const startUpload = (item: T): Promise> => { return new Promise((resolve, reject) => { const formData = new FormData(); formData.append(field, item);