manniru
11/24/2018 - 10:26 AM

[Async Firebase Functions]

[Async Firebase Functions]

const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);

// `/asyncRequests` should be configured so that it has a security rule of `{".read": false, ".write": false}`.
// `/asyncTaskState` should be configured so that it has a security rule of `{".read": "!data.hasChild('uid') || (auth != null && auth.uid === data.child('uid').val())", ".write": false}`.

/**
The main thing about long-running HTTPS functions is to get a response back to the client as fast as possible and tell it to wait for more updates.

To do this follow these steps:
1. Fire the HTTPS onRequest trigger
2. Check the request for validity
3. Act on the data:
 • If OK, PUSH the request data to `/asyncRequests` in your Firebase Database and send a `HTTP 202 Accepted` to the client with a key to listen to for updates. (under say `/asyncTaskState`)
 • If erroneous, send the appropriate error back to the client.
4. On the client, add an onWrite listener on the returned reference.
5. On the server, a database `onCreate` Cloud Function will be fired which can do your long running work and then update the task state when complete.
6. On the client, it will get the updated result and then you can deregister the listener.

The `/asyncRequests` key in your database will contain various records that look like `{taskname: 'compile', requestedBy: req.ip, data: {...}}`. These records are listened to by a database `onCreate` trigger which can then be used to run and wait for your long-running promises.
*/

exports.startSomeNamedTask = functions.https.onRequest((req, res) => {
  let xForwardedFor = req.get('x-forwarded-for') || '';
  let requestor = xForwardedFor.split(',')[0];
  
  // perform assertions on data (req.body).
  
  if (hasMissingData) { // psuedo code
    // HTTP 400 Bad Request - "Missing critical information"
    return res.status(400).json({error: 'missing fields', fields: ['array', 'of', 'fields', 'missing']});
  }
  if (notAuthorized) { // psuedo code
    // HTTP 401 Unauthorized
    return res.status(401).json({error: 'invalid uid'}); // example
  }
  
  
  let pushTaskRef = admin.database().ref('/asyncRequests')
    .push({
      taskname: 'someNamedTask',
      requestedBy: requestor,
      data: req.body,
      // Add a 'uid' field here to restrict access to only that user.
    });
    
  pushTaskRef.then(() => {
    // HTTP 202 Accepted - "I have more work to do, wait for updates"
    res.status(202).json({ref: '/asyncTaskState/' + pushTaskRef.key}); // ref is the path for the client to listen to
  }, (err) => {
    console.log('Error whilst storing async request');
    res.sendStatus(500);
  })
  .catch((err) => console.log('Unexpected error sending response: ', err)); // good habit
});

exports.asyncRequestHandler = functions.database.ref('/asyncRequests/{taskId}').onCreate((event) => {
  let taskId = event.params.taskId;
  let taskMetaData = event.data.val();
  let start = Date.now();
  let taskStateRef = admin.database().ref('/asyncTaskState/' + taskId);
  console.log('Handling task #' + taskId + ':' + taskMetaData.taskname + (taskMetaData.uid ? ' for User ' + taskMetaData.uid : '') '...');
  return taskStateRef.update({
      state: 'started',
      startedAt: start,
      timeoutAt: start + 60000, // 1 minute
      expires: start + 3600000, // 1 hour - this is for clearing out /asyncRequests with a cron job
    })
    .then(() => {
      // Homework: Rate limit by IP address.
      // let requestor = taskMetaData.requestedBy;
      // check requestor
      // return Promise.reject('Rejected:TooManyRequests');
    })
    .then(() => {
      // Do the work.
      // original data is in `taskMetaData.data`
      // return a Promise
    })
    .then((results) => {
      console.log('Async Task #' + taskId + ' completed successfully.');
      return taskStateRef.update({
        state: 'complete',
        finishedAt: Date.now(),
        timeoutAt: null,
        result: results
      });
    }, (error) => {
      console.log('Async Task #' + taskId + ' failed: ', error);
      return taskStateRef.update({
        state: 'failure',
        finishedAt: Date.now(),
        error: error.message || (typeof error === 'string' && error) || 'unexpected'
      });
    })
    .catch((err) => console.log('Unexpected error saving task result: ', err)); // good habit
});