Repository: kibertoad/toad-scheduler (ts)

Issue:
Check if job exists or not without throw error
Hi,

Would it be possible to have jobExists or that getById not throwing errors?
 

Pull request description:
Add existsById
fixes #155

Patch:
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 97ed31c..6b94927 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -14,3 +14,4 @@ updates:
       - dependency-name: "@typescript-eslint/eslint-plugin"
       - dependency-name: "@typescript-eslint/parser"
       - dependency-name: "@types/node"
+      - dependency-name: "@types/jest"
diff --git a/README.md b/README.md
index 487f7ab..fc94050 100644
--- a/README.md
+++ b/README.md
@@ -136,6 +136,85 @@ console.log(scheduler.getById('id_2').getStatus()); // returns "stopped" and can
 `toad-scheduler` does not persist its state by design, and has no out-of-the-box concurrency management features. In case it is necessary
 to prevent parallel execution of jobs in clustered environment, it is highly recommended to use [redis-semaphore](https://github.com/swarthy/redis-semaphore) in your tasks.
 
+Here is an example:
+
+```ts
+import { randomUUID } from 'crypto'
+
+import type Redis from 'ioredis'
+import { Mutex } from 'redis-semaphore'
+import type { LockOptions } from 'redis-semaphore'
+import { AsyncTask } from 'toad-scheduler';
+
+export type BackgroundJobConfiguration = {
+    jobId: string
+}
+
+export type LockConfiguration = {
+    lockName?: string
+    refreshInterval?: number
+    lockTimeout: number
+}
+
+export abstract class AbstractBackgroundJob {
+    public readonly jobId: string
+    protected readonly redis: Redis
+
+    protected constructor(
+        options: BackgroundJobConfiguration,
+        redis: Redis,
+    ) {
+        this.jobId = options.jobId
+        this.redis = redis
+    }
+
+    protected abstract processInternal(executionUuid: string): Promise<void>
+
+    async process() {
+        const uuid = randomUUID()
+
+        try {
+            await this.processInternal(uuid)
+        } catch (err) {
+            console.error(logObject)
+        }
+    }
+
+    protected getJobMutex(key: string, options: LockOptions) {
+        return new Mutex(this.redis, this.getJobLockName(key), options)
+    }
+
+    protected async tryAcquireExclusiveLock(lockConfiguration: LockConfiguration) {
+        const mutex = this.getJobMutex(lockConfiguration.lockName ?? 'exclusive', {
+            acquireAttemptsLimit: 1,
+            refreshInterval: lockConfiguration.refreshInterval,
+            lockTimeout: lockConfiguration.lockTimeout,
+        })
+
+        const lock = await mutex.tryAcquire()
+        // If someone else already has this lock, skip
+        if (!lock) {
+            return
+        }
+
+        return mutex
+    }
+
+    protected getJobLockName(key: string) {
+        return `${this.jobId}:locks:${key}`
+    }
+}
+
+function createTask(job: AbstractBackgroundJob): AsyncTask {
+    return new AsyncTask(
+        job.jobId,
+        () => {
+            return job.process()
+        },
+    )
+}
+```
+
 ## API for schedule
 
 * `days?: number` - how many days to wait before executing the job for the next time;
@@ -158,6 +237,7 @@ to prevent parallel execution of jobs in clustered environment, it is highly rec
 * `addIntervalJob(job: SimpleIntervalJob | LongIntervalJob): void` - registers and starts new interval-based job;
 * `stop(): void` - stops all jobs, registered in the scheduler;
 * `getById(id: string): Job` - returns the job with a given id.
+* `existsById(id: string): boolean` - returns true if job with given id exists, false otherwise.
 * `stopById(id: string): void` - stops the job with a given id.
 * `removeById(id: string): Job | undefined` - stops the job with a given id and removes it from the scheduler. If no such job exists, returns `undefined`, otherwise returns the job.
 * `startById(id: string): void` - starts, or restarts (if it's already running) the job with a given id.
diff --git a/lib/toadScheduler.ts b/lib/toadScheduler.ts
index 2be96f2..0a71993 100644
--- a/lib/toadScheduler.ts
+++ b/lib/toadScheduler.ts
@@ -53,6 +53,14 @@ export class ToadScheduler {
     return job
   }
 
+  existsById(id: string): boolean {
+    const job = this.jobRegistry[id]
+    if (!job) {
+      return false
+    }
+    return true
+  }
+
   removeById(id: string): Job | undefined {
     const job = this.jobRegistry[id]
     if (!job) {
diff --git a/package.json b/package.json
index 85c0f4e..803189d 100644
--- a/package.json
+++ b/package.json
@@ -21,20 +21,20 @@
     "prepublishOnly": "npm run build"
   },
   "devDependencies": {
-    "@types/jest": "^29.1.2",
-    "@types/node": "^18.8.4",
-    "@typescript-eslint/eslint-plugin": "^5.40.0",
-    "@typescript-eslint/parser": "^5.40.0",
+    "@types/jest": "^28.1.8",
+    "@types/node": "^18.11.11",
+    "@typescript-eslint/eslint-plugin": "^5.45.1",
+    "@typescript-eslint/parser": "^5.45.1",
     "eslint": "^8.25.0",
     "eslint-config-prettier": "^8.5.0",
     "eslint-plugin-prettier": "^4.2.1",
-    "jasmine-core": "^4.4.0",
+    "jasmine-core": "^4.5.0",
     "jest": "^28.1.3",
     "karma": "^6.4.1",
     "karma-chrome-launcher": "^3.1.1",
     "karma-jasmine": "^5.1.0",
     "karma-typescript": "^5.5.3",
-    "prettier": "^2.7.1",
+    "prettier": "^2.8.0",
     "ts-jest": "^28.0.8",
     "typescript": "4.9.3"
   },


Test patch:
diff --git a/test/LongIntervalJob.spec.ts b/test/LongIntervalJob.spec.ts
index f49edc9..1f50946 100644
--- a/test/LongIntervalJob.spec.ts
+++ b/test/LongIntervalJob.spec.ts
@@ -3,7 +3,6 @@ import { LongIntervalJob } from '../lib/engines/simple-interval/LongIntervalJob'
 import { Task } from '../lib/common/Task'
 import { NoopTask } from './utils/testTasks'
 import { advanceTimersByTime, mockTimers, unMockTimers } from './utils/timerUtils'
-import { SimpleIntervalJob } from '../lib/engines/simple-interval/SimpleIntervalJob'
 
 const isJest = process.env.JEST_WORKER_ID !== undefined
 const isJasmine = !isJest
diff --git a/test/toadScheduler.spec.ts b/test/toadScheduler.spec.ts
index 6ea01e1..9d2ffed 100644
--- a/test/toadScheduler.spec.ts
+++ b/test/toadScheduler.spec.ts
@@ -31,6 +31,38 @@ describe('ToadScheduler', () => {
     })
   })
 
+  describe('existsById', () => {
+    it('returns true for existing job', () => {
+      const scheduler = new ToadScheduler()
+      const task = new NoopTask()
+      const job = new SimpleIntervalJob(
+        {
+          seconds: 20,
+        },
+        task,
+        { id: 'id' }
+      )
+      scheduler.addSimpleIntervalJob(job)
+      const result = scheduler.existsById('id')
+      expect(result).toBe(true)
+    })
+
+    it('returns false for non-existing job', () => {
+      const scheduler = new ToadScheduler()
+      const task = new NoopTask()
+      const job = new SimpleIntervalJob(
+        {
+          seconds: 20,
+        },
+        task,
+        { id: 'id' }
+      )
+      scheduler.addSimpleIntervalJob(job)
+      const result = scheduler.existsById('id2')
+      expect(result).toBe(false)
+    })
+  })
+
   describe('removeById', () => {
     it('correctly removes job by id', () => {
       let counter = 0