job-collection for Meteor.js

repository·master·Indexed 18 days ago

https://github.com/vsivsi/meteor-job-collection

A job management system for Meteor.js that enables scheduling, repeating, and offloading computationally expensive tasks to external Node.js workers. It maintains reactivity and persistence via MongoDB and uses the DDP protocol for secure remote access. The system includes a JobCollection API for managing queues, a Job object for configuring priority, retries, and delays, and the meteor-job npm package for running pure Node.js workers.

Tokens
10.3K
Snippets
30
Records
43
Agent score
14%

What's inside job-collection

  1. Interact with job-collections

    master

    Job collections are backed by Meteor Collections and support standard reactive methods like .find() and .findOne() on the client.

    Important Security Note: Meteor clients are automatically denied permission to directly insert, update, or remove jobs. To modify jobs, you must use the provided JobCollection, Job, and JobQueue APIs. Servers retain access to standard Meteor collection methods but should favor the job collection APIs to ensure the job document model is correctly maintained and enforced.

    For non-Meteor clients, you can use the meteor-job npm package to implement identical functionality via the same interfaces.

  2. Add private server-side data to job documents

    master

    You can add custom, private server-side data to a job document by using a subdocument named _private.

    Important Security Warning: Data inside _private is not accepted via or returned from standard jobCollection method calls. However, if you include _private in a query cursor returned via a Meteor publish function, you will leak this sensitive data to clients. Always ensure you exclude _private from any published cursors.

  3. How job-collection design works

    master

    The job-collection package is built on top of MongoDB and uses Meteor's DDP protocol for persistence, reactivity, and secure remote access.

    Key architectural points:

    • JobCollection as a Collection: A JobCollection is a standard Meteor Collection. While you can use .find() and .findOne(), most interactions happen through the Job object API.
    • Method-based API: Most Job API calls are transformed into Meteor Methods. This allows the Job class to be implemented in pure JavaScript, making it compatible with Meteor servers, Meteor clients, and independent Node.js processes (via the meteor-job package).
  4. DDP Method reference for job-collection

    master

    Each job-collection instance created on a Meteor server defines a set of DDP methods. These methods are prefixed with the name of the job collection (e.g., myJobs_getWork) to prevent collisions between multiple collections on the same server.

    While the JobCollection and Job APIs are the recommended way to interact with the library, you can call these DDP methods directly if you require finer control over allow/deny rules than the predefined admin, manager, creator, and worker access categories provide.

  5. Secure a job-collection using roles

    master

    Securing a job-collection follows standard Meteor patterns (publish/subscribe and allow/deny rules) but includes specialized remote methods.

    To manage permissions efficiently, you can write allow/deny rules for one of the four predefined permission groups:

    • admin
    • manager
    • creator
    • worker

    These roles group related functions to separate security concerns. If these predefined roles do not meet your requirements, you can secure each remote method individually with custom allow/deny rules.

  6. Configure JobCollection permissions with allow() and deny()

    master

    By default, JobCollection is a server-side service with no remote access. Use jc.allow(options) and jc.deny(options) to manage remote access via DDP methods.

    Permission Groups

    Methods are grouped into four predefined levels:

    • admin: Full access to all remote methods.
    • manager: Can manage the collection (e.g., cancelling jobs).
    • creator: Can create new jobs.
    • worker: Can retrieve jobs and update their status.

    Usage Examples

    Allow all remote actions (Insecure - development only):

    jc.allow({
      admin: function (userId, method, params) {
        return true;
      }
    });

    Allow specific admin users:

    // Using an array of userIds
    jc.allow({
      admin: [ adminUserId ]
    });

    Granular method-level permission: Grant permission to create specific job types (e.g., 'email') to a specific user:

    jc.allow({
      jobSave: function (userId, method, params) {
        // params[0] is the new job doc
        if ((userId === emailCreator) && (params[0].type === 'email')) {
          return true;
        }
        return false;
      }
    });

    Deny all remote access:

    jc.deny({
      admin: function (userId, method, params) {
        return true;
      }
    });
    // Allow any remote client (Meteor client or node.js application) to perform any action
    jc.allow({
      // The "admin" below represents
      // the grouping of all remote methods
      admin: function (userId, method, params) {
        return true;
      }
    });
  7. Clean up old jobs from the database

    master

    Completed and canceled jobs accumulate in the database by default. To prevent database bloat, use one of the following strategies:

    1. Add a job cleaning job: Create a recurring job specifically designed to clean up old records based on your custom logic.
    2. Use events: Listen to jc.events to remove jobs automatically once they complete or are removed.
  8. Quick start: Set up a JobCollection on a Meteor server

    master

    To use job-collection, you first initialize a JobCollection on the server. You can use .allow() to define security rules for which users can perform actions on the jobs. Finally, call .startJobServer() within Meteor.startup to begin processing the queue.

    ///////////////////
    // Server
    if (Meteor.isServer) {
    
      var myJobs = JobCollection('myJobQueue');
      myJobs.allow({
        // Grant full permission to any authenticated user
        admin: function (userId, method, params) {
          return (userId ? true : false);
        }
      });
    
      Meteor.startup(function () {
        // Normal Meteor publish call, the server always
        // controls what each client can see
        Meteor.publish('allJobs', function () {
          return myJobs.find({});
        });
    
        // Start the myJobs queue running
        return myJobs.startJobServer();
      });
    }
  9. Create a responsive JobQueue using Meteor observe

    master

    To avoid the latency of time-based polling, you can set a very high pollInterval and use Meteor's observe to trigger the queue whenever a new job is added to the collection.

    Note: For non-Meteor Node.js worker scripts, use the alternative approach documented in the meteor-job npm package.

    // 1. Create a queue that doesn't poll automatically
    const q = jc.processJobs(
      'jobType',
      {
        pollInterval: 1000000000, // Effectively disable polling
      },
      function (job, callback) {
        job.done();
        callback();
      }
    );
    
    // 2. Use Meteor observe to trigger the queue when a job is added
    jc.find({ type: 'jobType', status: 'ready' })
      .observe({
         added: function () { q.trigger(); }
      });
  10. Create and manage jobs from a Meteor client

    master

    On the client, you can subscribe to the job collection and create new Job instances. You can chain methods like .priority(), .retry(), and .delay() before calling .save() to commit the job to the server. Once a job is saved, it becomes a reactive document in the collection. You can also fetch existing jobs by _id and control them using methods like .pause(), .cancel(), or .remove().

    ///////////////////
    // Client
    if (Meteor.isClient) {
    
      var myJobs = JobCollection('myJobQueue');
    
      Meteor.startup(function () {
        Meteor.subscribe('allJobs');
    
        // Create a job:
        var job = new Job(myJobs, 'sendEmail', // type of job
          // Job data that you define
          {
            address: 'bozo@clowns.com',
            subject: 'Critical rainbow hair shortage',
            message: 'LOL; JK, KThxBye.'
          }
        );
    
        // Set some properties of the job and then submit it
        job.priority('normal')
          .retry({ retries: 5,
            wait: 15*60*1000 })  // 15 minutes between attempts
          .delay(60*60*1000)     // Wait an hour before first try
          .save();               // Commit it to the server
    
        // Fetch a job by _id to control it
        myJobs.getJob(_id, function (err, job) {
          if (job) {
            job.pause();
            job.cancel();
            job.remove();
          }
        });
      });
    }
  11. Run a pure Node.js worker for job-collection

    master

    You can run workers outside of the Meteor environment using a pure Node.js process. This requires the meteor-job npm package. You must establish a DDP connection to your Meteor server, authenticate (e.g., via ddp-login), and then use Job.processJobs() to listen for and execute specific job types.

    ///////////////////
    // node.js Worker
    var DDP = require('ddp');
    var DDPlogin = require('ddp-login');
    var Job = require('meteor-job');
    
    // Setup the DDP connection
    var ddp = new DDP({
      host: "meteor.mydomain.com",
      port: 3000,
      use_ejson: true
    });
    
    // Connect Job with this DDP session
    Job.setDDP(ddp);
    
    // Open the DDP connection
    ddp.connect(function (err) {
      if (err) throw err;
      DDPlogin(ddp, function (err, token) {
        if (err) throw err;
    
        // Create a worker to get sendMail jobs from 'myJobQueue'
        var workers = Job.processJobs('myJobQueue', 'sendEmail',
          function (job, cb) {
            var email = job.data;
            sendEmail(email.address, email.subject, email.message,
              function(err) {
                if (err) {
                  job.log("Sending failed with error" + err,
                    {level: 'warning'});
                  job.fail("" + err);
                } else {
                  job.done();
                }
                // Be sure to invoke the callback
                cb();
              }
            );
          }
        );
      });
    });