Should afterSaveFile behave like afterSave?

Hey guys,

I’m using the fantastic new beforeFileSave and afterSaveFile triggers. However, I was expecting afterSaveFile to behave like afterSave, that is:

You can use an afterSave handler to perform lengthy operations after sending a response back to the client.

Whereas, as far as I can tell, saving a file waits for any afterSaveFile trigger to finish, and throwing an error in afterSaveFile prevents the save.

afterSave rejecting promise:

Parse.Cloud.afterSave('AfterSaveTest2', function() {
      return new Promise((resolve, reject) => {
        setTimeout(function() {
          reject('THIS SHOULD BE IGNORED');
        }, 1000);
      });
    });

    const obj = new Parse.Object('AfterSaveTest2');
    obj.save().then(
      function() {
        done();
      },
      function(error) {
        fail(error);
        done();
      }
    );

afterSaveFile:

Parse.Cloud.afterSaveFile(async () => {
      throw new Parse.Error(400, 'some-error-message');
    });
    const filename = 'donald_duck.pdf';
    const file = new Parse.File(filename, [1, 2, 3], 'text/plain');
    try {
      await file.save({ useMasterKey: true });
    } catch (error) {
      expect(error.message).toBe('some-error-message');
    }

Should afterSaveFile ignore lengthy operations and should the client receive a successful response even if afterSaveFile throws an error?

Any trigger can be used to perform lengthy operations and return before that operation has finished. This really depends on whether the code you run in the trigger is synchronous or asynchronous.

If you take another look at the whole paragraph in the docs, it says:

You can use an afterSave handler to perform lengthy operations after sending a response back to the client. In order to respond to the client before the afterSave handler completes, your handler may not return a promise and your afterSave handler may not use async/await.

The example in the docs above that paragraph is asynchronous and will return immediately because it does not await query.get. It can be synchronous if you instead write await query.get, in that case the trigger will not complete until all chained calls have finished. The same is true for class and file triggers.

For further reading, I suggest to search for async/await JavaScript promises and synchronous / asynchronous JavaScript execution, which is a deep topic that really never gets old :slight_smile:

1 Like

Thank you so much for the detailed response. I didn’t realise that using async in afterSave would delay the .save callback, I figured that would only happen in beforeSave. You’re absolutely correct.

The triggers do behave the same in that regard:

  it('afterSaveFile delay save', async () => {
    await reconfigureServer({ filesAdapter: mockAdapter });
    Parse.Cloud.afterSaveFile(async () => {
      await new Promise( resolve=> {
        setTimeout(()=>{
          resolve();
        }, 3000)
      })
    });
    const filename = 'donald_duck.pdf';
    const file = new Parse.File(filename, [1, 2, 3], 'text/plain');
    const saveStart = new Date();
    await file.save({ useMasterKey: true });
    const dif = new Date().getTime() - saveStart.getTime();
    expect(dif).toBeGreaterThan(3000);
  });
  it('afterSave delay save', async () => {
    Parse.Cloud.afterSave('AfterSaveTest2', async() => {
      await new Promise( resolve=> {
        setTimeout(()=>{
          resolve();
        }, 3000)
      })
    });
    const obj = new Parse.Object('AfterSaveTest2');
    const saveStart = new Date();
    await obj.save({ useMasterKey: true });
    const dif = new Date().getTime() - saveStart.getTime();
    expect(dif).toBeGreaterThan(3000);
  });

However, when it comes to throwing errors, even when I test like-for-like, afterSaveFile still behaves a bit differently.

  it('afterSaveFile throws error', async () => {
    await reconfigureServer({ filesAdapter: mockAdapter });
    Parse.Cloud.afterSaveFile(() => {
      throw new Parse.Error(400, 'some-error-message');
    });
    const filename = 'donald_duck.pdf';
    const file = new Parse.File(filename, [1, 2, 3], 'text/plain');
    let error = new Parse.Error();
    try {
      await file.save({ useMasterKey: true });
    } catch (e) {
      // error.message is some-error-message
      error = e;
    }
    expect(error.message).toBe('some-error-message');
    // or should error be null as thrown from afterSaveFile
  });
  it('afterSave throws error', async () => {
    Parse.Cloud.afterSave('AfterSaveTest2', () => {
      throw new Parse.Error(400, 'some-error-message');
    });
    const obj = new Parse.Object('AfterSaveTest2');
    let error = new Parse.Error();
    try {
      await obj.save({ useMasterKey: true });
    } catch (e) {
      // error is undefined
      error = e;
    }
    expect(error.message).toBe('some-error-message');
    // or should error be null as thrown from afterSave
  });

I will chime in here since i’m the one who added the file hooks (I’m glad you like them!).

The way the afterSaveFile hook is written it allows you to have both options (wait for the task to finish or not wait). If you want to do a long running task and not wait you would do something like this:

Parse.Cloud.afterSaveFile(async () => {
   // Lets wait for an object to be fetched
    const object = new Parse.User();
    object.id = 'abc123';
    await object.fetch({ useMasterKey: true });

    // Lets NOT wait for an object to be fetched
    const otherObject = new Parse.User();
    otherObject.id = 'xyz890';
    otherObject.fetch({ useMasterKey: true });
    // notice above I am not using await. This will give you the functionality that you're looking for I believe.
});

I hope this helps

2 Likes

@mtrezza thanks again for the detail around using async in afterSave. My server is a bit quicker now!

And thank you @stevestencil for the example and again for the triggers. They’ve helped me easily compress files, limit each user to one photo and much more. Thank you both for your continued work on Parse and for making my life easier. I cannot stress how much time your work has saved me.

The issue with throwing out of the afterSaveFile isn’t that much of a problem as I can just use return instead of throw.

1 Like

Glad that you could resolve the issue. If you feel there is something that can be improved in the docs that would have helped you, please feel free to open a PR in the docs repo.

1 Like