Repository: taskforcesh/bullmq (ts)

Issue:
Create a compatibility class for bull 3.x
In order to simplify migration between 3.x and 4.0 we should have a compatibility class. 
In 4.0 we modularized by dividing responsibilities in different classes such as client, worker, events, scheduler, etc. In 3.0 on the other hand we have a unique class "Queue" that provides the same functionality.
We could create a class that mimics 3.x as close as possible (although probably we cannot require it to be 100% backwards compatible), by wrapping all the other classes. It will not be as efficient in terms of redis client usage but it will make it much easier for 3.x users to upgrade.

Pull request:
Compatibility class implementing bull3 api in bullmq
Work is still in progress, there're a lot of things to improve:

review and port all tests
implement missing features in bullmq
refactor initialization of bullmq worker (there're some drawbacks in current implementation)
do some manual testing as well
Resolves #4

Patch:
diff --git a/package.json b/package.json
index b959eae709..b3e0833065 100644
--- a/package.json
+++ b/package.json
@@ -27,6 +27,7 @@
     "@types/mocha": "^5.2.5",
     "@types/node": "^12.6.8",
     "@types/node-uuid": "^0.0.28",
+    "@types/sinon": "^7.0.13",
     "chai": "^4.2.0",
     "coveralls": "^3.0.2",
     "delay": "^4.3.0",
diff --git a/src/classes/compat.ts b/src/classes/compat.ts
new file mode 100644
index 0000000000..ac6a311f0a
--- /dev/null
+++ b/src/classes/compat.ts
@@ -0,0 +1,890 @@
+// Type definitions for bull 3.10
+// Project: https://github.com/OptimalBits/bull
+// Definitions by: Bruno Grieder <https://github.com/bgrieder>
+//                 Cameron Crothers <https://github.com/JProgrammer>
+//                 Marshall Cottrell <https://github.com/marshall007>
+//                 Weeco <https://github.com/weeco>
+//                 Gabriel Terwesten <https://github.com/blaugold>
+//                 Oleg Repin <https://github.com/iamolegga>
+//                 David Koblas <https://github.com/koblas>
+//                 Bond Akinmade <https://github.com/bondz>
+//                 Wuha Team <https://github.com/wuha-team>
+//                 Alec Brunelle <https://github.com/aleccool213>
+//                 Dan Manastireanu <https://github.com/danmana>
+//                 Kjell-Morten Bratsberg Thorsen <https://github.com/kjellmorten>
+//                 Christian D. <https://github.com/pc-jedi>
+//                 Silas Rech <https://github.com/lenovouser>
+//                 DoYoung Ha <https://github.com/hados99>
+//                 Borys Kupar <https://github.com/borys-kupar>
+// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
+// TypeScript Version: 2.8
+
+import { EventEmitter } from 'events';
+import { QueueEvents, Worker, Queue, QueueScheduler, Job } from '@src/classes';
+import {
+  JobsOpts,
+  QueueOptions,
+  RepeatOpts,
+  QueueEventsOptions,
+  QueueSchedulerOptions,
+  WorkerOptions,
+  Processor,
+} from '@src/interfaces';
+
+type CommonOptions = QueueSchedulerOptions &
+  QueueOptions &
+  WorkerOptions &
+  QueueEventsOptions;
+
+export class Queue3<T = any> extends EventEmitter {
+  /**
+   * The name of the queue
+   */
+  name: string;
+
+  private opts: CommonOptions;
+
+  private readonly queue: Queue;
+  private queueEvents: QueueEvents;
+  private worker: Worker;
+  private queueScheduler: QueueScheduler;
+
+  /**
+   * This is the Queue constructor.
+   * It creates a new Queue that is persisted in Redis.
+   * Everytime the same queue is instantiated it tries to process all the old jobs
+   * that may exist from a previous unfinished session.
+   */
+  constructor(name: string, opts?: CommonOptions) {
+    super();
+
+    this.opts = opts;
+    this.name = name;
+
+    this.queue = new Queue(this.name, this.opts);
+  }
+
+  /**
+   * Returns a promise that resolves when Redis is connected and the queue is ready to accept jobs.
+   * This replaces the `ready` event emitted on Queue in previous verisons.
+   */
+  async isReady(): Promise<this> {
+    await this.queue.waitUntilReady();
+    return this;
+  }
+
+  /**
+   * Defines a processing function for the jobs placed into a given Queue.
+   *
+   * The callback is called everytime a job is placed in the queue.
+   * It is passed an instance of the job as first argument.
+   *
+   * If the callback signature contains the second optional done argument,
+   * the callback will be passed a done callback to be called after the job has been completed.
+   * The done callback can be called with an Error instance, to signal that the job did not complete successfully,
+   * or with a result as second argument (e.g.: done(null, result);) when the job is successful.
+   * Errors will be passed as a second argument to the "failed" event; results,
+   * as a second argument to the "completed" event.
+   *
+   * If, however, the callback signature does not contain the done argument,
+   * a promise must be returned to signal job completion.
+   * If the promise is rejected, the error will be passed as a second argument to the "failed" event.
+   * If it is resolved, its value will be the "completed" event's second argument.
+   */
+  process(processor: string | Processor): Promise<void> {
+    if (this.worker) {
+      throw new Error('Queue3.process() cannot be called twice');
+    }
+
+    this.worker = new Worker(this.name, processor, this.opts);
+    this.queueScheduler = new QueueScheduler(this.name, this.opts);
+    return this.worker.waitUntilReady();
+  }
+
+  add(jobName: string, data: any, opts?: JobsOpts): Promise<Job> {
+    return this.queue.add(jobName, data, opts);
+  }
+
+  /**
+   * Returns a promise that resolves when the queue is paused.
+   *
+   * A paused queue will not process new jobs until resumed, but current jobs being processed will continue until
+   * they are finalized. The pause can be either global or local. If global, all workers in all queue instances
+   * for a given queue will be paused. If local, just this worker will stop processing new jobs after the current
+   * lock expires. This can be useful to stop a worker from taking new jobs prior to shutting down.
+   *
+   * Pausing a queue that is already paused does nothing.
+   */
+  async pause(): Promise<void> {
+    return this.queue.pause();
+  }
+
+  async pauseWorker(doNotWaitActive?: boolean): Promise<void> {
+    if (!this.worker) {
+      throw new Error('Worker is not initialized, call process() first');
+    }
+    return this.worker.pause(doNotWaitActive);
+  }
+
+  /**
+   * Returns a promise that resolves when the queue is resumed after being paused.
+   *
+   * The resume can be either local or global. If global, all workers in all queue instances for a given queue
+   * will be resumed. If local, only this worker will be resumed. Note that resuming a queue globally will not
+   * resume workers that have been paused locally; for those, resume(true) must be called directly on their
+   * instances.
+   *
+   * Resuming a queue that is not paused does nothing.
+   */
+  async resume(): Promise<void> {
+    return this.queue.resume();
+  }
+
+  async resumeWorker(): Promise<void> {
+    if (!this.worker) {
+      throw new Error('Worker is not initialized, call process() first');
+    }
+    return this.worker.resume();
+  }
+
+  isWorkerPaused(): boolean {
+    return this.worker && this.worker.isPaused();
+  }
+
+  /**
+   * Returns a promise that returns the number of jobs in the queue, waiting or paused.
+   * Since there may be other processes adding or processing jobs,
+   * this value may be true only for a very small amount of time.
+   */
+  count(): Promise<number> {
+    return this.queue.count();
+  }
+
+  /**
+   * Empties a queue deleting all the input lists and associated jobs.
+   */
+  empty(): Promise<void> {
+    return this.queue.drain();
+  }
+
+  /**
+   * Closes the underlying redis client. Use this to perform a graceful shutdown.
+   *
+   * `close` can be called from anywhere, with one caveat:
+   * if called from within a job handler the queue won't close until after the job has been processed
+   */
+  close(): Promise<any> {
+    const promises = [];
+
+    if (this.queueScheduler) {
+      promises.push(this.queueScheduler.close());
+    }
+    if (this.queue) {
+      promises.push(this.queue.close());
+    }
+    if (this.queueEvents) {
+      promises.push(this.queueEvents.close());
+    }
+    if (this.worker) {
+      promises.push(this.worker.close());
+    }
+    return Promise.all(promises);
+  }
+
+  /**
+   * Returns a promise that will return the job instance associated with the jobId parameter.
+   * If the specified job cannot be located, the promise callback parameter will be set to null.
+   */
+  getJob(jobId: string): Promise<Job | null> {
+    return this.queue.getJob(jobId);
+  }
+
+  /**
+   * Returns a promise that will return an array with the waiting jobs between start and end.
+   */
+  getWaiting(start = 0, end = -1): Promise<Array<Job>> {
+    return this.queue.getWaiting(start, end);
+  }
+
+  /**
+   * Returns a promise that will return an array with the active jobs between start and end.
+   */
+  getActive(start = 0, end = -1): Promise<Array<Job>> {
+    return this.queue.getActive(start, end);
+  }
+
+  /**
+   * Returns a promise that will return an array with the delayed jobs between start and end.
+   */
+  getDelayed(start = 0, end = -1): Promise<Array<Job>> {
+    return this.queue.getDelayed(start, end);
+  }
+
+  /**
+   * Returns a promise that will return an array with the completed jobs between start and end.
+   */
+  getCompleted(start = 0, end = -1): Promise<Array<Job>> {
+    return this.queue.getCompleted(start, end);
+  }
+
+  /**
+   * Returns a promise that will return an array with the failed jobs between start and end.
+   */
+  async getFailed(start = 0, end = -1): Promise<Array<Job>> {
+    return this.queue.getFailed(start, end);
+  }
+
+  /**
+   * Returns JobInformation of repeatable jobs (ordered descending). Provide a start and/or an end
+   * index to limit the number of results. Start defaults to 0, end to -1 and asc to false.
+   */
+  getRepeatableJobs(
+    start = 0,
+    end = -1,
+    asc = false,
+  ): Promise<JobInformation3[]> {
+    return this.queue.repeat.getRepeatableJobs(start, end, asc);
+  }
+
+  /**
+   * ???
+   */
+  async nextRepeatableJob(
+    name: string,
+    data: any,
+    opts?: JobsOpts,
+    skipCheckExists?: boolean,
+  ): Promise<Job> {
+    return this.queue.repeat.addNextRepeatableJob(
+      name,
+      data,
+      opts,
+      skipCheckExists,
+    );
+  }
+
+  /**
+   * Removes a given repeatable job. The RepeatOptions and JobId needs to be the same as the ones
+   * used for the job when it was added.
+   *
+   * name: The name of the to be removed job
+   */
+  async removeRepeatable(name: string, repeat: RepeatOpts): Promise<void> {
+    return this.queue.repeat.removeRepeatable(name, repeat, repeat.jobId);
+  }
+
+  /**
+   * Removes a given repeatable job by key.
+   */
+  async removeRepeatableByKey(repeatJobKey: string): Promise<void> {
+    const repeat = this.queue.repeat;
+    await repeat.waitUntilReady();
+
+    const tokens = repeatJobKey.split(':');
+    const data = {
+      key: repeatJobKey,
+      name: tokens[0],
+      id: tokens[1] || null,
+      endDate: parseInt(tokens[2]) || null,
+      tz: tokens[3] || null,
+      cron: tokens[4],
+    };
+
+    const queueKey = repeat.toKey('');
+    return (<any>repeat.client).removeRepeatable(
+      repeat.keys.repeat,
+      repeat.keys.delayed,
+      data.id,
+      repeatJobKey,
+      queueKey,
+    );
+  }
+
+  /**
+   * Returns a promise that will return an array of job instances of the given types.
+   * Optional parameters for range and ordering are provided.
+   */
+  getJobs(
+    types: string[] | string,
+    start = 0,
+    end = -1,
+    asc = false,
+  ): Promise<Array<Job>> {
+    return this.queue.getJobs(types, start, end, asc);
+  }
+
+  async getNextJob(): Promise<Job> {
+    throw new Error('Not supported');
+  }
+
+  /**
+   * Returns a object with the logs according to the start and end arguments. The returned count
+   * value is the total amount of logs, useful for implementing pagination.
+   */
+  getJobLogs(
+    jobId: string,
+    start = 0,
+    end = -1,
+  ): Promise<{ logs: string[]; count: number }> {
+    return this.queue.getJobLogs(jobId, start, end);
+  }
+
+  /**
+   * Returns a promise that resolves with the job counts for the given queue.
+   */
+  getJobCounts(...types: string[]): Promise<{ [index: string]: number }> {
+    return this.queue.getJobCounts(...types);
+  }
+
+  /**
+   * Returns a promise that resolves with the job counts for the given queue of the given types.
+   */
+  async getJobCountByTypes(...types: string[]): Promise<number> {
+    return this.queue.getJobCountByTypes(...types);
+  }
+
+  /**
+   * Returns a promise that resolves with the quantity of completed jobs.
+   */
+  getCompletedCount(): Promise<number> {
+    return this.queue.getCompletedCount();
+  }
+
+  /**
+   * Returns a promise that resolves with the quantity of failed jobs.
+   */
+  getFailedCount(): Promise<number> {
+    return this.queue.getFailedCount();
+  }
+
+  /**
+   * Returns a promise that resolves with the quantity of delayed jobs.
+   */
+  getDelayedCount(): Promise<number> {
+    return this.queue.getDelayedCount();
+  }
+
+  /**
+   * Returns a promise that resolves with the quantity of waiting jobs.
+   */
+  getWaitingCount(): Promise<number> {
+    return this.queue.getWaitingCount();
+  }
+
+  /**
+   * Returns a promise that resolves with the quantity of paused jobs.
+   */
+  getPausedCount(): Promise<number> {
+    return this.queue.getJobCountByTypes('paused');
+  }
+
+  /**
+   * Returns a promise that resolves with the quantity of active jobs.
+   */
+  getActiveCount(): Promise<number> {
+    return this.queue.getActiveCount();
+  }
+
+  /**
+   * Returns a promise that resolves to the quantity of repeatable jobs.
+   */
+  getRepeatableCount(): Promise<number> {
+    return this.queue.repeat.getRepeatableCount();
+  }
+
+  /**
+   * Tells the queue remove all jobs created outside of a grace period in milliseconds.
+   * You can clean the jobs with the following states: completed, wait (typo for waiting), active, delayed, and failed.
+   * @param grace Grace period in milliseconds.
+   * @param limit Maximum amount of jobs to clean per call. If not provided will clean all matching jobs.
+   * @param type Status of the job to clean. Values are completed, wait,
+   * active, paused, delayed, and failed. Defaults to completed.
+   */
+  clean(
+    grace: number,
+    limit: number,
+    type:
+      | 'completed'
+      | 'wait'
+      | 'active'
+      | 'paused'
+      | 'delayed'
+      | 'failed' = 'completed',
+  ): Promise<Array<Job>> {
+    return this.queue.clean(grace, limit, type);
+  }
+
+  /**
+   * Listens to queue events
+   */
+  on(event: string, callback: (...args: any[]) => void): this;
+
+  /**
+   * An error occured
+   */
+  on(event: 'error', callback: ErrorEventCallback3): this;
+
+  /**
+   * A Job is waiting to be processed as soon as a worker is idling.
+   */
+  on(event: 'waiting', callback: WaitingEventCallback3): this;
+
+  /**
+   * A job has started. You can use `jobPromise.cancel()` to abort it
+   */
+  on(event: 'active', callback: ActiveEventCallback3<T>): this;
+
+  /**
+   * A job has been marked as stalled.
+   * This is useful for debugging job workers that crash or pause the event loop.
+   */
+  on(event: 'stalled', callback: StalledEventCallback3<T>): this;
+
+  /**
+   * A job's progress was updated
+   */
+  on(event: 'progress', callback: ProgressEventCallback3<T>): this;
+
+  /**
+   * A job successfully completed with a `result`
+   */
+  on(event: 'completed', callback: CompletedEventCallback3<T>): this;
+
+  /**
+   * A job failed with `err` as the reason
+   */
+  on(event: 'failed', callback: FailedEventCallback3<T>): this;
+
+  /**
+   * The queue has been paused
+   */
+  on(event: 'paused', callback: EventCallback3): this;
+
+  /**
+   * The queue has been resumed
+   */
+  on(event: 'resumed', callback: EventCallback3): this; // tslint:disable-line unified-signatures
+
+  /**
+   * A job successfully removed.
+   */
+  on(event: 'removed', callback: RemovedEventCallback3<T>): this;
+
+  /**
+   * Old jobs have been cleaned from the queue.
+   * `jobs` is an array of jobs that were removed, and `type` is the type of those jobs.
+   *
+   * @see Queue#clean() for details
+   */
+  on(event: 'cleaned', callback: CleanedEventCallback3<T>): this;
+
+  /**
+   * Emitted every time the queue has processed all the waiting jobs
+   * (even if there can be some delayed jobs not yet processed)
+   */
+  on(event: 'drained', callback: EventCallback3): this; // tslint:disable-line unified-signatures
+
+  on(event: string | symbol, listener: (...args: any[]) => void): this {
+    return this.attachListener(false, event, listener);
+  }
+
+  once(event: string | symbol, listener: (...args: any[]) => void): this {
+    return this.attachListener(true, event, listener);
+  }
+
+  off(event: string | symbol, listener: (...args: any[]) => void): this {
+    return this.detachListener(event, listener);
+  }
+
+  removeListener(
+    event: string | symbol,
+    listener: (...args: any[]) => void,
+  ): this {
+    if (!listener) {
+      throw new Error('listener is required');
+    }
+    return this.detachListener(event, listener);
+  }
+
+  removeAllListeners(event: string | symbol): this {
+    return this.detachListener(event);
+  }
+
+  /**
+   * Set clientName to Redis.client
+   */
+  setWorkerName(): Promise<any> {
+    throw new Error('Not supported');
+  }
+
+  /**
+   * Returns Redis clients array which belongs to current Queue
+   */
+  getWorkers(): Promise<{ [key: string]: string }[]> {
+    return this.queue.getWorkers();
+  }
+
+  /**
+   * Returns Queue name in base64 encoded format
+   */
+  base64Name(): string {
+    return (this.queue as any).base64Name();
+  }
+
+  /**
+   * Returns Queue name with keyPrefix (default: 'bull')
+   */
+  clientName(): string {
+    return (this.queue as any).clientName();
+  }
+
+  /**
+   * Returns Redis clients array which belongs to current Queue from string with all redis clients
+   *
+   * @param list String with all redis clients
+   */
+  parseClientList(list: string): { [key: string]: string }[] {
+    return (this.queue as any).parseClientList(list);
+  }
+
+  retryJob(job: Job): Promise<void> {
+    return job.retry();
+  }
+
+  private getQueueEvents() {
+    if (!this.queueEvents) {
+      this.queueEvents = new QueueEvents(this.name, this.opts);
+      this.queueEvents.init();
+    }
+    return this.queueEvents;
+  }
+
+  private ensureWorkerCreated() {
+    if (!this.worker) {
+      throw new Error(
+        'You should create internal ' +
+          'worker by calling progress() ' +
+          'prior to attach listeners to worker events',
+      );
+    }
+  }
+
+  private attachListener(
+    once: boolean,
+    event: string | symbol,
+    listener: (...args: any[]) => void,
+  ): this {
+    switch (event) {
+      case 'active':
+        this.ensureWorkerCreated();
+        if (once) {
+          this.worker.once('active', listener);
+        } else {
+          this.worker.on('active', listener);
+        }
+        break;
+      case 'completed':
+        this.ensureWorkerCreated();
+        if (once) {
+          this.worker.once('completed', listener);
+        } else {
+          this.worker.on('completed', listener);
+        }
+        break;
+      case 'drained':
+        this.ensureWorkerCreated();
+        if (once) {
+          this.worker.once('drained', listener);
+        } else {
+          this.worker.on('drained', listener);
+        }
+        break;
+      case 'failed':
+        this.ensureWorkerCreated();
+        if (once) {
+          this.worker.once('failed', listener);
+        } else {
+          this.worker.on('failed', listener);
+        }
+        break;
+      case 'paused':
+        if (once) {
+          this.queue.once('paused', listener);
+        } else {
+          this.queue.on('paused', listener);
+        }
+        break;
+      case 'resumed':
+        if (once) {
+          this.queue.once('resumed', listener);
+        } else {
+          this.queue.on('resumed', listener);
+        }
+        break;
+      case 'progress':
+        if (once) {
+          this.queue.once('progress', listener);
+        } else {
+          this.queue.on('progress', listener);
+        }
+        break;
+      case 'waiting':
+        if (once) {
+          this.queue.once('waiting', listener);
+        } else {
+          this.queue.on('waiting', listener);
+        }
+        break;
+      case 'global:active':
+        if (once) {
+          this.getQueueEvents().once('active', listener);
+        } else {
+          this.getQueueEvents().on('active', listener);
+        }
+        break;
+      case 'global:completed':
+        if (once) {
+          this.getQueueEvents().once('completed', listener);
+        } else {
+          this.getQueueEvents().on('completed', listener);
+        }
+        break;
+      case 'global:drained':
+        if (once) {
+          this.getQueueEvents().once('drained', listener);
+        } else {
+          this.getQueueEvents().on('drained', listener);
+        }
+        break;
+      case 'global:failed':
+        if (once) {
+          this.getQueueEvents().once('failed', listener);
+        } else {
+          this.getQueueEvents().on('failed', listener);
+        }
+        break;
+      case 'global:paused':
+        if (once) {
+          this.getQueueEvents().once('paused', listener);
+        } else {
+          this.getQueueEvents().on('paused', listener);
+        }
+        break;
+      case 'global:resumed':
+        if (once) {
+          this.getQueueEvents().once('resumed', listener);
+        } else {
+          this.getQueueEvents().on('resumed', listener);
+        }
+        break;
+      case 'global:progress':
+        if (once) {
+          this.getQueueEvents().once('progress', listener);
+        } else {
+          this.getQueueEvents().on('progress', listener);
+        }
+        break;
+      case 'global:waiting':
+        if (once) {
+          this.getQueueEvents().once('waiting', listener);
+        } else {
+          this.getQueueEvents().on('waiting', listener);
+        }
+        break;
+      default:
+        throw new Error(
+          `Listening on '${String(event)}' event is not supported`,
+        );
+    }
+    return this;
+  }
+
+  detachListener(
+    event: string | symbol,
+    listener?: (...args: any[]) => void,
+  ): this {
+    switch (event) {
+      case 'active':
+        if (this.worker) {
+          if (listener) {
+            this.worker.removeListener('active', listener);
+          } else {
+            this.worker.removeAllListeners('active');
+          }
+        }
+        break;
+      case 'completed':
+        if (this.worker) {
+          if (listener) {
+            this.worker.removeListener('completed', listener);
+          } else {
+            this.worker.removeAllListeners('completed');
+          }
+        }
+        break;
+      case 'drained':
+        if (this.worker) {
+          if (listener) {
+            this.worker.removeListener('drained', listener);
+          } else {
+            this.worker.removeAllListeners('drained');
+          }
+        }
+        break;
+      case 'failed':
+        if (this.worker) {
+          if (listener) {
+            this.worker.removeListener('failed', listener);
+          } else {
+            this.worker.removeAllListeners('failed');
+          }
+        }
+        break;
+      case 'paused':
+        if (listener) {
+          this.queue.removeListener('paused', listener);
+        } else {
+          this.queue.removeAllListeners('paused');
+        }
+        break;
+      case 'resumed':
+        if (listener) {
+          this.queue.removeListener('resumed', listener);
+        } else {
+          this.queue.removeAllListeners('resumed');
+        }
+        break;
+      case 'progress':
+        if (listener) {
+          this.queue.removeListener('progress', listener);
+        } else {
+          this.queue.removeAllListeners('progress');
+        }
+        break;
+      case 'waiting':
+        if (listener) {
+          this.queue.removeListener('waiting', listener);
+        } else {
+          this.queue.removeAllListeners('waiting');
+        }
+        break;
+      case 'global:active':
+        if (this.queueEvents) {
+          if (listener) {
+            this.queueEvents.removeListener('active', listener);
+          } else {
+            this.queueEvents.removeAllListeners('active');
+          }
+        }
+        break;
+      case 'global:completed':
+        if (this.queueEvents) {
+          if (listener) {
+            this.queueEvents.removeListener('completed', listener);
+          } else {
+            this.queueEvents.removeAllListeners('completed');
+          }
+        }
+        break;
+      case 'global:drained':
+        if (this.queueEvents) {
+          if (listener) {
+            this.queueEvents.removeListener('drained', listener);
+          } else {
+            this.queueEvents.removeAllListeners('drained');
+          }
+        }
+        break;
+      case 'global:failed':
+        if (this.queueEvents) {
+          if (listener) {
+            this.queueEvents.removeListener('failed', listener);
+          } else {
+            this.queueEvents.removeAllListeners('failed');
+          }
+        }
+        break;
+      case 'global:paused':
+        if (this.queueEvents) {
+          if (listener) {
+            this.queueEvents.removeListener('paused', listener);
+          } else {
+            this.queueEvents.removeAllListeners('paused');
+          }
+        }
+        break;
+      case 'global:resumed':
+        if (this.queueEvents) {
+          if (listener) {
+            this.queueEvents.removeListener('resumed', listener);
+          } else {
+            this.queueEvents.removeAllListeners('resumed');
+          }
+        }
+        break;
+      case 'global:waiting':
+        if (this.queueEvents) {
+          if (listener) {
+            this.queueEvents.removeListener('waiting', listener);
+          } else {
+            this.queueEvents.removeAllListeners('waiting');
+          }
+        }
+        break;
+      default:
+        break;
+    }
+    return this;
+  }
+}
+
+export type JobStatusClean3 =
+  | 'completed'
+  | 'wait'
+  | 'active'
+  | 'delayed'
+  | 'paused'
+  | 'failed';
+
+export interface JobInformation3 {
+  key: string;
+  name: string;
+  id?: string;
+  endDate?: number;
+  tz?: string;
+  cron: string;
+  next: number;
+}
+
+export type EventCallback3 = () => void;
+
+export type ErrorEventCallback3 = (error: Error) => void;
+
+export interface JobPromise3 {
+  /**
+   * Abort this job
+   */
+  cancel(): void;
+}
+
+export type ActiveEventCallback3<T = any> = (
+  job: Job,
+  jobPromise?: JobPromise3,
+) => void;
+
+export type StalledEventCallback3<T = any> = (job: Job) => void;
+
+export type ProgressEventCallback3<T = any> = (job: Job, progress: any) => void;
+
+export type CompletedEventCallback3<T = any> = (job: Job, result: any) => void;
+
+export type FailedEventCallback3<T = any> = (job: Job, error: Error) => void;
+
+export type CleanedEventCallback3<T = any> = (
+  jobs: Array<Job>,
+  status: JobStatusClean3,
+) => void;
+
+export type RemovedEventCallback3<T = any> = (job: Job) => void;
+
+export type WaitingEventCallback3 = (jobId: string) => void;
diff --git a/src/classes/queue.ts b/src/classes/queue.ts
index 93a5f3fa6b..ad6ab23f43 100644
--- a/src/classes/queue.ts
+++ b/src/classes/queue.ts
@@ -1,4 +1,4 @@
-import { JobsOpts, RateLimiterOpts, QueueBaseOptions } from '@src/interfaces';
+import { JobsOpts, RateLimiterOpts, QueueOptions } from '@src/interfaces';
 import { v4 } from 'node-uuid';
 import { Job } from './job';
 import { QueueGetters } from './queue-getters';
@@ -12,10 +12,7 @@ export class Queue extends QueueGetters {
   repeat: Repeat;
   jobsOpts: JobsOpts;
 
-  constructor(
-    name: string,
-    opts?: QueueBaseOptions & { defaultJobOptions?: JobsOpts },
-  ) {
+  constructor(name: string, opts?: QueueOptions) {
     super(name, opts);
 
     this.repeat = new Repeat(name, {
diff --git a/src/classes/sandbox.ts b/src/classes/sandbox.ts
index 165c981322..5d6f26ab48 100644
--- a/src/classes/sandbox.ts
+++ b/src/classes/sandbox.ts
@@ -54,17 +54,6 @@ const sandbox = (processFile: any, childPool: any) => {
         childPool.release(child);
       }
     }
-
-    // return done.finally(() => {
-    //   child.removeListener('message', msgHandler);
-    //   child.removeListener('exit', exitHandler);
-    //
-    //   if (child.exitCode !== null) {
-    //     childPool.remove(child);
-    //   } else {
-    //     childPool.release(child);
-    //   }
-    // });
   };
 };
 
diff --git a/src/interfaces/advance-opts.ts b/src/interfaces/advanced-opts.ts
similarity index 100%
rename from src/interfaces/advance-opts.ts
rename to src/interfaces/advanced-opts.ts
diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts
index 68f028df2e..70bcb23a5e 100644
--- a/src/interfaces/index.ts
+++ b/src/interfaces/index.ts
@@ -1,6 +1,9 @@
-export * from './advance-opts';
+export * from './advanced-opts';
+export * from './backoff-opts';
 export * from './jobs-opts';
 export * from './queue-opts';
 export * from './rate-limiter-opts';
 export * from './redis-opts';
+export * from './repeat-opts';
 export * from './queue-scheduler-opts';
+export * from './worker-opts';
diff --git a/src/interfaces/worker-opts.ts b/src/interfaces/worker-opts.ts
index 363d562ee3..99400e8e66 100644
--- a/src/interfaces/worker-opts.ts
+++ b/src/interfaces/worker-opts.ts
@@ -1,7 +1,7 @@
 import { RateLimiterOpts } from './rate-limiter-opts';
 import { Job } from '@src/classes';
 import { QueueBaseOptions } from './queue-opts';
-import { AdvancedOpts } from './advance-opts';
+import { AdvancedOpts } from './advanced-opts';
 
 export type Processor = (job: Job) => Promise<any>;
 


Test patch:
diff --git a/src/test/test_compat.ts b/src/test/test_compat.ts
new file mode 100644
index 0000000000..62ce9e12ac
--- /dev/null
+++ b/src/test/test_compat.ts
@@ -0,0 +1,626 @@
+/*eslint-env node */
+'use strict';
+
+import { Job, Queue, QueueEvents, QueueScheduler, Worker } from '@src/classes';
+import { describe, beforeEach, afterEach, it } from 'mocha';
+import { expect } from 'chai';
+import IORedis from 'ioredis';
+import { v4 } from 'node-uuid';
+import { after } from 'lodash';
+
+import { delay } from 'bluebird';
+import { Queue3 } from '@src/classes/compat';
+
+describe('Compat', function() {
+  describe('jobs getters', function() {
+    this.timeout(4000);
+    let queue: Queue3;
+    let queueName: string;
+    let client: IORedis.Redis;
+
+    beforeEach(function() {
+      client = new IORedis();
+      return client.flushdb();
+    });
+
+    beforeEach(async function() {
+      queueName = 'test-' + v4();
+      queue = new Queue3(queueName);
+    });
+
+    afterEach(async function() {
+      await queue.close();
+      return client.quit();
+    });
+
+    it('should get waiting jobs', async function() {
+      await queue.add('test', { foo: 'bar' });
+      await queue.add('test', { baz: 'qux' });
+
+      const jobs = await queue.getWaiting();
+      expect(jobs).to.be.a('array');
+      expect(jobs.length).to.be.equal(2);
+      expect(jobs[0].data.foo).to.be.equal('bar');
+      expect(jobs[1].data.baz).to.be.equal('qux');
+    });
+
+    it('should get paused jobs', async function() {
+      await queue.pause();
+      await Promise.all([
+        queue.add('test', { foo: 'bar' }),
+        queue.add('test', { baz: 'qux' }),
+      ]);
+      const jobs = await queue.getWaiting();
+      expect(jobs).to.be.a('array');
+      expect(jobs.length).to.be.equal(2);
+      expect(jobs[0].data.foo).to.be.equal('bar');
+      expect(jobs[1].data.baz).to.be.equal('qux');
+    });
+
+    it('should get active jobs', async function() {
+      let processor;
+      const processing = new Promise(resolve => {
+        processor = async (job: Job) => {
+          const jobs = await queue.getActive();
+          expect(jobs).to.be.a('array');
+          expect(jobs.length).to.be.equal(1);
+          expect(jobs[0].data.foo).to.be.equal('bar');
+          resolve();
+        };
+      });
+
+      await queue.add('test', { foo: 'bar' });
+      await queue.process(processor);
+      await processing;
+    });
+
+    it('should get completed jobs', function(done) {
+      queue.process(async job => {});
+
+      let counter = 2;
+
+      queue.on('completed', async function() {
+        counter--;
+
+        if (counter === 0) {
+          const jobs = await queue.getCompleted();
+          expect(jobs).to.be.a('array');
+          done();
+        }
+      });
+
+      queue.add('test', { foo: 'bar' });
+      queue.add('test', { baz: 'qux' });
+    });
+
+    it('should get failed jobs', function(done) {
+      queue.process(async job => {
+        throw new Error('Forced error');
+      });
+
+      let counter = 2;
+
+      queue.on('failed', async function() {
+        counter--;
+
+        if (counter === 0) {
+          const jobs = await queue.getFailed();
+          expect(jobs).to.be.a('array');
+          done();
+        }
+      });
+
+      queue.add('test', { foo: 'bar' });
+      queue.add('test', { baz: 'qux' });
+    });
+
+    it('should return all completed jobs when not setting start/end', function(done) {
+      queue.process(async job => {});
+
+      queue.on(
+        'completed',
+        after(3, async function() {
+          try {
+            const jobs = await queue.getJobs('completed');
+            expect(jobs)
+              .to.be.an('array')
+              .that.have.length(3);
+            expect(jobs[0]).to.have.property('finishedOn');
+            expect(jobs[1]).to.have.property('finishedOn');
+            expect(jobs[2]).to.have.property('finishedOn');
+
+            expect(jobs[0]).to.have.property('processedOn');
+            expect(jobs[1]).to.have.property('processedOn');
+            expect(jobs[2]).to.have.property('processedOn');
+            done();
+          } catch (err) {
+            done(err);
+          }
+        }),
+      );
+
+      queue.add('test', { foo: 1 });
+      queue.add('test', { foo: 2 });
+      queue.add('test', { foo: 3 });
+    });
+
+    it('should return all failed jobs when not setting start/end', function(done) {
+      queue.process(async job => {
+        throw new Error('error');
+      });
+
+      queue.on(
+        'failed',
+        after(3, async function() {
+          try {
+            queue;
+            const jobs = await queue.getJobs('failed');
+            expect(jobs)
+              .to.be.an('array')
+              .that.has.length(3);
+            expect(jobs[0]).to.have.property('finishedOn');
+            expect(jobs[1]).to.have.property('finishedOn');
+            expect(jobs[2]).to.have.property('finishedOn');
+
+            expect(jobs[0]).to.have.property('processedOn');
+            expect(jobs[1]).to.have.property('processedOn');
+            expect(jobs[2]).to.have.property('processedOn');
+            done();
+          } catch (err) {
+            done(err);
+          }
+        }),
+      );
+
+      queue.add('test', { foo: 1 });
+      queue.add('test', { foo: 2 });
+      queue.add('test', { foo: 3 });
+    });
+
+    it('should return subset of jobs when setting positive range', function(done) {
+      queue.process(async job => {});
+
+      queue.on(
+        'completed',
+        after(3, async function() {
+          try {
+            const jobs = await queue.getJobs('completed', 1, 2, true);
+            expect(jobs)
+              .to.be.an('array')
+              .that.has.length(2);
+            expect(jobs[0].data.foo).to.be.eql(2);
+            expect(jobs[1].data.foo).to.be.eql(3);
+            expect(jobs[0]).to.have.property('finishedOn');
+            expect(jobs[1]).to.have.property('finishedOn');
+            expect(jobs[0]).to.have.property('processedOn');
+            expect(jobs[1]).to.have.property('processedOn');
+            done();
+          } catch (err) {
+            done(err);
+          }
+        }),
+      );
+
+      queue.add('test', { foo: 1 });
+      queue.add('test', { foo: 2 });
+      queue.add('test', { foo: 3 });
+    });
+
+    it('should return subset of jobs when setting a negative range', function(done) {
+      queue.process(async job => {});
+
+      queue.on(
+        'completed',
+        after(3, async function() {
+          try {
+            const jobs = await queue.getJobs('completed', -3, -1, true);
+            expect(jobs)
+              .to.be.an('array')
+              .that.has.length(3);
+            expect(jobs[0].data.foo).to.be.equal(1);
+            expect(jobs[1].data.foo).to.be.eql(2);
+            expect(jobs[2].data.foo).to.be.eql(3);
+            done();
+          } catch (err) {
+            done(err);
+          }
+        }),
+      );
+
+      queue.add('test', { foo: 1 });
+      queue.add('test', { foo: 2 });
+      queue.add('test', { foo: 3 });
+    });
+
+    it('should return subset of jobs when range overflows', function(done) {
+      queue.process(async job => {});
+
+      queue.on(
+        'completed',
+        after(3, async function() {
+          try {
+            const jobs = await queue.getJobs('completed', -300, 99999, true);
+            expect(jobs)
+              .to.be.an('array')
+              .that.has.length(3);
+            expect(jobs[0].data.foo).to.be.equal(1);
+            expect(jobs[1].data.foo).to.be.eql(2);
+            expect(jobs[2].data.foo).to.be.eql(3);
+            done();
+          } catch (err) {
+            done(err);
+          }
+        }),
+      );
+
+      queue.add('test', { foo: 1 });
+      queue.add('test', { foo: 2 });
+      queue.add('test', { foo: 3 });
+    });
+
+    it('should return jobs for multiple types', function(done) {
+      let counter = 0;
+
+      queue.process(async job => {
+        counter++;
+        if (counter == 2) {
+          await queue.add('test', { foo: 3 });
+          return queue.pause();
+        }
+      });
+
+      queue.on(
+        'completed',
+        after(2, async function() {
+          try {
+            const jobs = await queue.getJobs(['completed', 'waiting']);
+            expect(jobs).to.be.an('array');
+            expect(jobs).to.have.length(3);
+            done();
+          } catch (err) {
+            done(err);
+          }
+        }),
+      );
+
+      queue.add('test', { foo: 1 });
+      queue.add('test', { foo: 2 });
+    });
+  });
+
+  describe('events', function() {
+    this.timeout(4000);
+    let queue: Queue3;
+    let queueName: string;
+    let client: IORedis.Redis;
+
+    beforeEach(function() {
+      client = new IORedis();
+      return client.flushdb();
+    });
+
+    beforeEach(async function() {
+      queueName = 'test-' + v4();
+      queue = new Queue3(queueName);
+    });
+
+    afterEach(async function() {
+      await queue.close();
+      return client.quit();
+    });
+
+    it('should emit waiting when a job has been added', function(done) {
+      queue.on('waiting', function() {
+        done();
+      });
+
+      queue.add('test', { foo: 'bar' });
+    });
+
+    it('should emit global waiting event when a job has been added', function(done) {
+      queue.on('waiting', function() {
+        done();
+      });
+
+      queue.add('test', { foo: 'bar' });
+    });
+
+    it('emits drained and global:drained event when all jobs have been processed', function(done) {
+      queue.process(async job => {});
+
+      const drainedCallback = after(2, async function() {
+        const jobs = await queue.getJobCountByTypes('completed');
+        expect(jobs).to.be.equal(2);
+        done();
+      });
+
+      queue.once('drained', drainedCallback);
+      queue.once('global:drained', drainedCallback);
+
+      queue.add('test', { foo: 'bar' });
+      queue.add('test', { foo: 'baz' });
+    });
+
+    it('should emit an event when a job becomes active', function(done) {
+      queue.add('test', {});
+
+      queue.process(async () => {});
+
+      queue.once('active', function() {
+        queue.once('completed', async function() {
+          done();
+        });
+      });
+    });
+
+    it('should listen to global events', function(done) {
+      let state: string;
+      queue.on('global:waiting', function() {
+        expect(state).to.be.undefined;
+        state = 'waiting';
+      });
+      queue.once('global:active', function() {
+        expect(state).to.be.equal('waiting');
+        state = 'active';
+      });
+      queue.once('global:completed', async function() {
+        expect(state).to.be.equal('active');
+        done();
+      });
+
+      queue.add('test', {});
+      queue.process(async () => {});
+    });
+  });
+
+  describe('Pause', function() {
+    let queue: Queue3;
+    let queueName: string;
+    let client: IORedis.Redis;
+
+    beforeEach(function() {
+      client = new IORedis();
+      return client.flushdb();
+    });
+
+    beforeEach(async function() {
+      queueName = 'test-' + v4();
+      queue = new Queue3(queueName);
+    });
+
+    afterEach(async function() {
+      await queue.close();
+      return client.quit();
+    });
+
+    // it('should pause a queue until resumed', async () => {
+    //   let process;
+    //   let isPaused = false;
+    //   let counter = 2;
+    //   const processPromise = new Promise(resolve => {
+    //     process = async (job: Job) => {
+    //       expect(isPaused).to.be.eql(false);
+    //       expect(job.data.foo).to.be.equal('paused');
+    //       counter--;
+    //       if (counter === 0) {
+    //         resolve();
+    //       }
+    //     };
+    //   });
+    //
+    //   await queue.process(process);
+    //
+    //   await queue.pause();
+    //   isPaused = true;
+    //   await queue.add('test', { foo: 'paused' });
+    //   await queue.add('test', { foo: 'paused' });
+    //   isPaused = false;
+    //   await queue.resume();
+    //
+    //   await processPromise;
+    // });
+
+    it('should be able to pause a running queue and emit relevant events', async () => {
+      let process;
+
+      let isPaused = false,
+        isResumed = true,
+        first = true;
+
+      const processPromise = new Promise((resolve, reject) => {
+        process = async (job: Job) => {
+          try {
+            expect(isPaused).to.be.eql(false);
+            expect(job.data.foo).to.be.equal('paused');
+
+            if (first) {
+              first = false;
+              isPaused = true;
+              return queue.pause();
+            } else {
+              expect(isResumed).to.be.eql(true);
+              await queue.close();
+              resolve();
+            }
+          } catch (err) {
+            reject(err);
+          }
+        };
+      });
+
+      await queue.process(process);
+
+      queue.add('test', { foo: 'paused' });
+      queue.add('test', { foo: 'paused' });
+
+      queue.on('global:paused', async () => {
+        isPaused = false;
+        await queue.resume();
+      });
+
+      queue.on('global:resumed', () => {
+        isResumed = true;
+      });
+
+      return processPromise;
+    });
+
+    it('should pause the queue locally', async () => {
+      let counter = 2;
+      let process;
+      const processPromise = new Promise(resolve => {
+        process = async (job: Job) => {
+          expect(queue.isWorkerPaused()).to.be.eql(false);
+          counter--;
+          if (counter === 0) {
+            await queue.close();
+            resolve();
+          }
+        };
+      });
+
+      await queue.process(process);
+      await queue.pauseWorker();
+
+      // Add the worker after the queue is in paused mode since the normal behavior is to pause
+      // it after the current lock expires. This way, we can ensure there isn't a lock already
+      // to test that pausing behavior works.
+
+      await queue.add('test', { foo: 'paused' });
+      await queue.add('test', { foo: 'paused' });
+
+      expect(counter).to.be.eql(2);
+      expect(queue.isWorkerPaused()).to.be.eql(true);
+
+      await queue.resumeWorker();
+      return processPromise;
+    });
+
+    // it('should wait until active jobs are finished before resolving pause', async () => {
+    //   let process;
+    //
+    //   const startProcessing = new Promise(resolve => {
+    //     process = async () => {
+    //       resolve();
+    //       return delay(1000);
+    //     };
+    //   });
+    //
+    //   await queue.process(process);
+    //
+    //   const jobs = [];
+    //   for (let i = 0; i < 10; i++) {
+    //     jobs.push(queue.add('test', i));
+    //   }
+    //
+    //   //
+    //   // Add start processing so that we can test that pause waits for this job to be completed.
+    //   //
+    //   jobs.push(startProcessing);
+    //   await Promise.all(jobs);
+    //   await queue.pause(true);
+    //
+    //   let active = await queue.getJobCountByTypes('active');
+    //   expect(active).to.be.eql(0);
+    //   expect(queue.isPaused()).to.be.eql(true);
+    //
+    //   // One job from the 10 posted above will be processed, so we expect 9 jobs pending
+    //   let paused = await queue.getJobCountByTypes('delayed', 'waiting');
+    //   expect(paused).to.be.eql(9);
+    //   await Promise.all([active, paused]);
+    //
+    //   await queue.add('test', {});
+    //
+    //   active = await queue.getJobCountByTypes('active');
+    //   expect(active).to.be.eql(0);
+    //
+    //   paused = await queue.getJobCountByTypes('paused', 'waiting', 'delayed');
+    //   expect(paused).to.be.eql(10);
+    //
+    //   await Promise.all([active, paused]);
+    // });
+
+    it('should pause the queue locally when more than one worker is active', async () => {
+      let process1, process2;
+
+      const startProcessing1 = new Promise(resolve => {
+        process1 = async () => {
+          resolve();
+          return delay(200);
+        };
+      });
+
+      const startProcessing2 = new Promise(resolve => {
+        process2 = async () => {
+          resolve();
+          return delay(200);
+        };
+      });
+
+      const worker1 = new Worker(queueName, process1);
+      await worker1.waitUntilReady();
+
+      const worker2 = new Worker(queueName, process2);
+      await worker2.waitUntilReady();
+
+      queue.add('test', 1);
+      queue.add('test', 2);
+      queue.add('test', 3);
+      queue.add('test', 4);
+
+      await Promise.all([startProcessing1, startProcessing2]);
+      await Promise.all([worker1.pause(), worker2.pause()]);
+
+      const count = await queue.getJobCounts('active', 'waiting', 'completed');
+      expect(count.active).to.be.eql(0);
+      expect(count.waiting).to.be.eql(2);
+      expect(count.completed).to.be.eql(2);
+
+      return Promise.all([worker1.close(), worker2.close()]);
+    });
+
+    // it('should wait for blocking job retrieval to complete before pausing locally', async () => {
+    //   let process;
+    //
+    //   const startProcessing = new Promise(resolve => {
+    //     process = async () => {
+    //       resolve();
+    //       return delay(200);
+    //     };
+    //   });
+    //
+    //   await queue.process(process);
+    //
+    //   await queue.add('test', 1);
+    //   await startProcessing;
+    //   await queue.pause(true);
+    //   await queue.add('test', 2);
+    //
+    //   const count = await queue.getJobCounts('active', 'waiting', 'completed');
+    //   expect(count.active).to.be.eql(0);
+    //   expect(count.waiting).to.be.eql(1);
+    //   expect(count.completed).to.be.eql(1);
+    // });
+
+    it('pauses fast when queue is drained', async function() {
+      await queue.process(async () => {});
+
+      await queue.add('test', {});
+
+      return new Promise((resolve, reject) => {
+        queue.on('global:drained', async () => {
+          try {
+            const start = new Date().getTime();
+            await queue.pause();
+
+            const finish = new Date().getTime();
+            expect(finish - start).to.be.lt(1000);
+            resolve();
+          } catch (err) {
+            reject(err);
+          }
+        });
+      });
+    });
+  });
+});