A powerful and easy-to-use job queue for Meteor, supporting distributed workers that can run anywhere. Version 2.0 is completely rewritten in TypeScript with full async/await support for Meteor 3.x and Node.js 18+.
Note: This is a modernized TypeScript fork of the original vsivsi:job-collection. All credit for the original design and implementation goes to Vaughn Iverson.
- ๐ Full async/await support - Modern promise-based API
- ๐ TypeScript - Complete type definitions included
- ๐ Meteor 3.x compatible - No Fibers dependency (100% removed)
- โก Node.js 22 ready - Supports Node 18, 20, and 22
- ๐ Backward compatible - Callback APIs still supported
- ๐ฏ Better performance - Faster with native promises
Key Changes:
- Meteor 3.0+ and Node.js 18+ now required
- All methods now return Promises - use
async/awaitor callbacks - No Fibers dependency - pure async/await throughout
- Use
setJobAllow/setJobDenyfor permissions (orallow/denyfor backward compat)
// OLD (v1.x with Fibers) const job = myJobs.getJob(id); job.done(); // NEW (v2.x) const job = await myJobs.getJob(id); await job.done();
meteor add strigops:job-collection
Use this in your Meteor application (server and client code).
npm install @strigo/meteor-job-collection
Use this for remote workers that connect to your Meteor app via DDP.
Requirements:
- Meteor 3.0+ (for Meteor apps)
- Node.js 18+ (20+ or 22 recommended)
- MongoDB 5.0+
Which one should I use?
- ๐ Meteor App: Use
meteor add strigo:job-collection - ๐ Remote Workers: Use
npm install @strigo/meteor-job-collection - ๐ Both: Meteor package in your app + npm package for scaled workers
import { JobCollection } from 'meteor/strigops:job-collection'; // Create job collection const myJobs = new JobCollection('myJobQueue'); // Set up permissions myJobs.setJobAllow({ admin: (userId) => !!userId // Authenticated users only }); // Start server on startup Meteor.startup(async () => { await myJobs.startJobServer(); }); // Publish jobs (optional) Meteor.publish('allJobs', function() { return myJobs.find({}); });
import { Job } from 'meteor/strigops:job-collection'; // Create a job with async/await async function scheduleEmail() { const job = new Job(myJobs, 'sendEmail', { to: '[email protected]', subject: 'Hello', body: 'Welcome!' }); const id = await job .priority('normal') .retry({ retries: 5, wait: 15*60*1000 }) // 15 min between retries .save(); return id; }
// Worker with async/await myJobs.processJobs( 'sendEmail', { concurrency: 4 }, async (job, callback) => { try { await sendEmail(job.data); await job.done(); } catch (error) { await job.fail(error.message); } callback(); // Always call callback! } );
waiting โ ready โ running โ completed
โ โ โ
paused ready failed โ waiting (retry)
โ โ
waiting cancelled
const job = new Job(myJobs, 'processImage', data) .priority('high') // low, normal, medium, high, critical .retry({ retries: 5, wait: 5*60*1000, // 5 minutes between retries backoff: 'exponential' // or 'constant' }) .repeat({ repeats: 10, // or Job.forever wait: 60*60*1000 // 1 hour between repeats }) .delay(30*1000) // Delay 30 seconds .depends([job1, job2]) // Wait for dependencies .after(new Date('2024-01-01')) // Run after date .save();
Full type safety out of the box:
import { Job, JobCollection } from 'meteor/strigops:job-collection'; // Define job data types interface EmailJobData { to: string; subject: string; body: string; } // Type-safe job creation const job = new Job<EmailJobData>(myJobs, 'sendEmail', { to: '[email protected]', subject: 'Hello', body: 'Welcome!' }); // Type-safe workers myJobs.processJobs<EmailJobData>( 'sendEmail', async (job, callback) => { const { to, subject, body } = job.data; // Fully typed! await sendEmail(to, subject, body); await job.done(); callback(); } );
All methods support both async/await and callbacks:
// With async/await const id = await job.save(); await job.done(result); await job.fail(error); await job.log('message', { level: 'info' }); await job.progress(50, 100); await job.pause(); await job.resume(); await job.cancel(); await job.restart(); await job.remove(); // With callbacks (backward compatible) job.save((err, id) => { /* ... */ }); job.done(result, (err, success) => { /* ... */ });
Job Creation:
new Job(collection, type, data)- Create new job.priority(level)- Set priority (low/normal/medium/high/critical).retry(options)- Configure retry behavior.repeat(options)- Configure repeat behavior.delay(ms)- Delay before first run.after(date)- Run after specific date.depends(jobs)- Set job dependencies.save([options])- Save to collection
Job Control:
job.refresh()- Reload from serverjob.done([result])- Mark as completedjob.fail([error])- Mark as failedjob.pause()- Pause jobjob.resume()- Resume paused jobjob.cancel()- Cancel jobjob.restart()- Restart failed/cancelled jobjob.rerun()- Clone and rerun completed jobjob.remove()- Remove from collectionjob.ready()- Force to ready state
Job Monitoring:
job.log(message, [options])- Add log entryjob.progress(completed, total)- Update progress
// Get jobs await myJobs.getJob(id) await myJobs.getJobs([id1, id2, id3]) await myJobs.getWork(type, options) // Bulk operations await myJobs.readyJobs([id1, id2]) await myJobs.pauseJobs([id1, id2]) await myJobs.resumeJobs([id1, id2]) await myJobs.cancelJobs([id1, id2]) await myJobs.restartJobs([id1, id2]) await myJobs.removeJobs([id1, id2]) // Server control await myJobs.startJobServer() await myJobs.shutdownJobServer({ timeout: 60000 }) // Worker queue const workers = myJobs.processJobs(type, options, worker)
const workers = myJobs.processJobs('type', options, worker); workers.pause() // Pause processing workers.resume() // Resume processing workers.trigger() // Manually trigger work check workers.length() // Jobs waiting in queue workers.running() // Jobs currently running workers.idle() // True if no jobs workers.full() // True if at max concurrency // Graceful shutdown workers.shutdown({ level: 'soft' }, () => { console.log('Shutdown complete'); });
Fine-grained permission control:
// Use setJobAllow/setJobDeny (or allow/deny for backward compatibility) myJobs.setJobAllow({ // Admin: full control admin: (userId, method, params) => { return Roles.userIsInRole(userId, 'admin'); }, // Manager: can manage existing jobs manager: (userId, method, params) => { return Roles.userIsInRole(userId, 'manager'); }, // Creator: can create new jobs creator: (userId, method, params) => { return userId !== null; }, // Worker: can get work and update status worker: (userId, method, params) => { return Roles.userIsInRole(userId, 'worker'); } }); // Deny rules (override allow) myJobs.setJobDeny({ admin: (userId) => userId === 'bannedUserId' }); // Note: You can also use allow/deny for backward compatibility // myJobs.allow({ ... }) and myJobs.deny({ ... }) work the same way
Run workers outside Meteor:
// worker.js import DDP from 'ddp'; import DDPLogin from 'ddp-login'; import { Job } from '@strigo/meteor-job-collection'; const ddp = new DDP({ host: 'localhost', port: 3000, use_ejson: true }); Job.setDDP(ddp); ddp.connect(async (err) => { if (err) throw err; await DDPLogin(ddp, { /* credentials */ }); const workers = Job.processJobs( 'myQueue', 'sendEmail', { concurrency: 10 }, async (job, callback) => { await sendEmail(job.data); await job.done(); callback(); } ); });
// Job 2 waits for Job 1 const job1 = await new Job(myJobs, 'download', data).save(); const job2 = await new Job(myJobs, 'process', data) .depends([job1]) .save();
const job = new Job(myJobs, 'dailyReport', {}) .repeat({ schedule: myJobs.later.parse.text('at 9:00 am every weekday') }) .save();
myJobs.processJobs('process', async (job, callback) => { const total = 100; for (let i = 0; i < total; i++) { await processItem(i); await job.progress(i + 1, total); } await job.done(); callback(); });
// Set up file logging import fs from 'fs'; const logStream = fs.createWriteStream('jobs.log', { flags: 'a' }); myJobs.setLogStream(logStream); // Or use event-based logging myJobs.events.on('call', (msg) => { console.log(`${msg.method}:`, msg.returnVal); });
// Periodic cleanup async function cleanupJobs() { const weekAgo = new Date(Date.now() - 7*24*60*60*1000); const oldJobs = await myJobs .find({ status: { $in: ['completed', 'cancelled'] }, updated: { $lt: weekAgo } }) .fetchAsync(); const ids = oldJobs.map(j => j._id); await myJobs.removeJobs(ids); } // Schedule cleanup job new Job(myJobs, 'cleanup', {}) .repeat({ schedule: myJobs.later.parse.text('at 3:00 am') }) .save(); myJobs.processJobs('cleanup', async (job, callback) => { await cleanupJobs(); await job.done(); callback(); });
// Count jobs by status const stats = await Promise.all([ myJobs.find({ status: 'waiting' }).countAsync(), myJobs.find({ status: 'running' }).countAsync(), myJobs.find({ status: 'completed' }).countAsync(), myJobs.find({ status: 'failed' }).countAsync() ]); console.log('Jobs:', { waiting: stats[0], running: stats[1], completed: stats[2], failed: stats[3] });
- Use appropriate concurrency
myJobs.processJobs('type', { concurrency: 10 }, worker);
- Batch operations
await myJobs.pauseJobs(manyIds); // Better than loop
- Add indexes for custom queries
await myJobs.createIndexAsync({ type: 1, created: -1 });
-
Clean up old jobs regularly
-
Use prefetch to reduce latency
myJobs.processJobs('type', { prefetch: 5 }, worker);
// Check job status const ready = await myJobs.find({ status: 'ready' }).countAsync(); console.log('Ready jobs:', ready); // Check worker status console.log('Running:', workers.running()); console.log('Queued:', workers.length()); // Monitor errors myJobs.events.on('error', (msg) => { console.error('Error:', msg.error); });
- Check
workTimeoutconfiguration - Look for worker crashes
- Auto-fail for expired jobs is built-in
- Add database indexes
- Increase concurrency
- Use prefetch option
- Clean up old jobs
# Clone repository git clone https://github.com/strigo/meteor-job-collection.git cd meteor-job-collection # Install dependencies npm install # Build TypeScript npm run build # Test meteor test-packages ./
{ _id: JobId, runId: JobId | null, type: string, status: 'waiting' | 'paused' | 'ready' | 'running' | 'failed' | 'cancelled' | 'completed', data: object, result?: object, failures?: object[], priority: number, depends: JobId[], resolved: JobId[], after: Date, updated: Date, created: Date, workTimeout?: number, expiresAfter?: Date, log?: LogEntry[], progress: { completed: number, total: number, percent: number }, retries: number, retried: number, retryUntil: Date, retryWait: number, retryBackoff: 'constant' | 'exponential', repeats: number, repeated: number, repeatUntil: Date, repeatWait: number | LaterJSSchedule }
All DDP methods are prefixed with the collection name (e.g., myQueue_getWork):
startJobServer(options)- Start servershutdownJobServer(options)- Stop servergetJob(ids, options)- Get job(s) by IDgetWork(type, options)- Get ready jobsjobSave(doc, options)- Save jobjobRemove(ids)- Remove jobsjobPause(ids)- Pause jobsjobResume(ids)- Resume jobsjobReady(ids, options)- Ready jobsjobCancel(ids, options)- Cancel jobsjobRestart(ids, options)- Restart jobsjobRerun(id, options)- Rerun jobjobLog(id, runId, message, options)- Add logjobProgress(id, runId, completed, total)- Update progressjobDone(id, runId, result, options)- Mark completejobFail(id, runId, error, options)- Mark failed
MIT License - Copyright (C) 2014-2024 by Vaughn Iverson
This is a modernized fork with TypeScript and async/await support. Original project: vsivsi/meteor-job-collection
Contributions welcome! Please:
- Write tests in TypeScript
- Use async/await patterns
- Follow existing code style
- Add documentation
- Update changelog
git clone YOUR_REPO_URL cd meteor-job-collection npm install npm run watch # Auto-compile on changes
meteor test-packages ./
- Changelog
- Test Guide
- Publishing Guide
- Original Package: vsivsi:job-collection
Made with โค๏ธ for the Meteor community