Repository: parse-community/Parse-SDK-JS
Pull request #948 (2019-10-09 21:45:53)

Support ParseFile upload cancel
Fixes: https://github.com/parse-community/Parse-SDK-JS/issues/395

WIP and can be built upon. 

codecov[bot]: # [Codecov](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/948?src=pr&el=h1) Report
> Merging [#948](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/948?src=pr&el=desc) into [master](https://codecov.io/gh/parse-community/Parse-SDK-JS/commit/18fd502e86d4fcf13d8d48185c1d5b7fdaed2034?src=pr&el=desc) will **decrease** coverage by `0.05%`.
> The diff coverage is `76.47%`.

[![Impacted file tree graph](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/948/graphs/tree.svg?width=650&token=8nCENM0QHj&height=150&src=pr)](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/948?src=pr&el=tree)

```diff
@@            Coverage Diff            @@
##           master    #948      +/-   ##
=========================================
- Coverage   91.95%   91.9%   -0.06%     
=========================================
  Files          54      54              
  Lines        5045    5062      +17     
  Branches     1133    1137       +4     
=========================================
+ Hits         4639    4652      +13     
- Misses        406     410       +4
```


| [Impacted Files](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/948?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/ParseFile.js](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/948/diff?src=pr&el=tree#diff-c3JjL1BhcnNlRmlsZS5qcw==) | `97.07% <100%> (+0.14%)` | :arrow_up: |
| [src/RESTController.js](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/948/diff?src=pr&el=tree#diff-c3JjL1JFU1RDb250cm9sbGVyLmpz) | `79.74% <55.55%> (-1.47%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/948?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/948?src=pr&el=footer). Last update [18fd502...a76a138](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/948?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/src/ParseFile.js b/src/ParseFile.js
index 97d3929..d8619da 100644
--- a/src/ParseFile.js
+++ b/src/ParseFile.js
@@ -70,6 +70,7 @@ class ParseFile {
   _source: FileSource;
   _previousSave: ?Promise<ParseFile>;
   _data: ?string;
+  _requestTask: ?any;
 
   /**
    * @param name {String} The file's name. This will be prefixed by a unique
@@ -210,6 +211,8 @@ class ParseFile {
    */
   save(options?: FullOptions) {
     options = options || {};
+    options.requestTask = (task) => this._requestTask = task;
+
     const controller = CoreManager.getFileController();
     if (!this._previousSave) {
       if (this._source.format === 'file') {
@@ -217,6 +220,7 @@ class ParseFile {
           this._name = res.name;
           this._url = res.url;
           this._data = null;
+          this._requestTask = null;
           return this;
         });
       } else if (this._source.format === 'uri') {
@@ -231,12 +235,14 @@ class ParseFile {
         }).then((res) => {
           this._name = res.name;
           this._url = res.url;
+          this._requestTask = null;
           return this;
         });
       } else {
         this._previousSave = controller.saveBase64(this._name, this._source, options).then((res) => {
           this._name = res.name;
           this._url = res.url;
+          this._requestTask = null;
           return this;
         });
       }
@@ -246,6 +252,13 @@ class ParseFile {
     }
   }
 
+  cancel() {
+    if (this._requestTask && typeof this._requestTask.abort === 'function') {
+      this._requestTask.abort();
+    }
+    this._requestTask = null;
+  }
+
   toJSON(): { name: ?string, url: ?string } {
     return {
       __type: 'File',
diff --git a/src/RESTController.js b/src/RESTController.js
index 01af56e..4ebf31f 100644
--- a/src/RESTController.js
+++ b/src/RESTController.js
@@ -51,6 +51,9 @@ if (typeof XDomainRequest !== 'undefined' &&
 function ajaxIE9(method: string, url: string, data: any, options?: FullOptions) {
   return new Promise((resolve, reject) => {
     const xdr = new XDomainRequest();
+    if (options && typeof options.requestTask === 'function') {
+      options.requestTask(xdr);
+    }
     xdr.onload = function() {
       let response;
       try {
@@ -101,7 +104,12 @@ const RESTController = {
         );
       }
       let handled = false;
+      let aborted = false;
+
       const xhr = new XHR();
+      if (options && typeof options.requestTask === 'function') {
+        options.requestTask(xhr);
+      }
       xhr.onreadystatechange = function() {
         if (xhr.readyState !== 4 || handled) {
           return;
@@ -124,6 +132,8 @@ const RESTController = {
           if (response) {
             promise.resolve({ response, status: xhr.status, xhr });
           }
+        } else if (aborted && xhr.status === 0) {
+          promise.resolve({ response: {}, status: 0, xhr });
         } else if (xhr.status >= 500 || xhr.status === 0) { // retry on 5XX or node-xmlhttprequest error
           if (++attempts < CoreManager.get('REQUEST_ATTEMPT_LIMIT')) {
             // Exponentially-growing random delay
@@ -173,7 +183,9 @@ const RESTController = {
           });
         }
       }
-
+      xhr.onabort = () => {
+        aborted = true;
+      };
       xhr.open(method, url, true);
 
       for (const h in headers) {
diff --git a/src/Xhr.weapp.js b/src/Xhr.weapp.js
index b742a96..b1888e0 100644
--- a/src/Xhr.weapp.js
+++ b/src/Xhr.weapp.js
@@ -9,8 +9,10 @@ module.exports = class XhrWeapp {
     this.responseHeader = {};
     this.method = '';
     this.url = '';
-    this.onerror = () => {}
-    this.onreadystatechange = () => {}
+    this.onabort = () => {};
+    this.onerror = () => {};
+    this.onreadystatechange = () => {};
+    this.requestTask = null;
   }
 
   getAllResponseHeaders() {
@@ -34,8 +36,18 @@ module.exports = class XhrWeapp {
     this.url = url;
   }
 
+  abort() {
+    if (!this.requestTask) {
+      return;
+    }
+    this.requestTask.abort();
+    this.status = 0;
+    this.onabort();
+    this.onreadystatechange();
+  }
+
   send(data) {
-    wx.request({
+    this.requestTask = wx.request({
       url: this.url,
       method: this.method,
       data: data,
@@ -46,10 +58,11 @@ module.exports = class XhrWeapp {
         this.response = res.data;
         this.responseHeader = res.header;
         this.responseText = JSON.stringify(res.data);
-
+        this.requestTask = null;
         this.onreadystatechange();
       },
       fail: (err) => {
+        this.requestTask = null;
         this.onerror(err);
       }
     })


Test patch:
diff --git a/src/__tests__/ParseFile-test.js b/src/__tests__/ParseFile-test.js
index dff3f0a..1e9a053 100644
--- a/src/__tests__/ParseFile-test.js
+++ b/src/__tests__/ParseFile-test.js
@@ -233,6 +233,31 @@ describe('ParseFile', () => {
       expect(f.url()).toBe('http://files.parsetfss.com/a/progress.txt');
     });
   });
+
+  it('can cancel file upload', () => {
+    const mockRequestTask = {
+      abort: () => {},
+    };
+    CoreManager.setFileController({
+      saveFile: function(name, payload, options) {
+        options.requestTask(mockRequestTask);
+        return Promise.resolve({});
+      },
+      saveBase64: () => {},
+      download: () => {},
+    });
+    const file = new ParseFile('progress.txt', new File(["Parse"], "progress.txt"));
+
+    jest.spyOn(mockRequestTask, 'abort');
+    file.cancel();
+    expect(mockRequestTask.abort).toHaveBeenCalledTimes(0);
+
+    file.save();
+
+    expect(file._requestTask).toEqual(mockRequestTask);
+    file.cancel();
+    expect(mockRequestTask.abort).toHaveBeenCalledTimes(1);
+  });
 });
 
 describe('FileController', () => {
@@ -294,11 +319,10 @@ describe('FileController', () => {
     await file.save();
     expect(defaultController.download).toHaveBeenCalledTimes(1);
     expect(defaultController.saveBase64).toHaveBeenCalledTimes(1);
-    expect(defaultController.saveBase64.mock.calls[0]).toEqual([
-      'parse.png',
-      { format: 'base64', base64: 'ParseA==', type: 'image/png' },
-      {}
-    ]);
+    expect(defaultController.saveBase64.mock.calls[0][0]).toEqual('parse.png');
+    expect(defaultController.saveBase64.mock.calls[0][1]).toEqual({
+      format: 'base64', base64: 'ParseA==', type: 'image/png'
+    });
     spy.mockRestore();
   });
 
diff --git a/src/__tests__/RESTController-test.js b/src/__tests__/RESTController-test.js
index 3d48b35..b4b8d71 100644
--- a/src/__tests__/RESTController-test.js
+++ b/src/__tests__/RESTController-test.js
@@ -252,6 +252,26 @@ describe('RESTController', () => {
     expect(response.result).toBe('hello');
   });
 
+  it('handles aborted requests', (done) => {
+    const XHR = function() { };
+    XHR.prototype = {
+      open: function() { },
+      setRequestHeader: function() { },
+      send: function() {
+        this.status = 0;
+        this.responseText = '{"foo":"bar"}';
+        this.readyState = 4;
+        this.onabort();
+        this.onreadystatechange();
+      }
+    };
+    RESTController._setXHR(XHR);
+    RESTController.request('GET', 'classes/MyObject', {}, {})
+      .then(() => {
+        done();
+      });
+  });
+
   it('attaches the session token of the current user', async () => {
     CoreManager.setUserController({
       currentUserAsync() {