Repository: kibertoad/toad-scheduler
Pull request #50 (2021-10-25 07:31:41)

added ID example to the documentation
see if you like it! just wanted to highlight a couple of more usecases


Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 8f54f42..1f8416f 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -14,7 +14,7 @@ jobs:
 
     strategy:
       matrix:
-        node-version: [10.x, 12.x, 14.x, 16.x]
+        node-version: [10.x, 12.x, 14.x, 16.x, 17.x]
 
     steps:
       - uses: actions/checkout@v2
diff --git a/README.md b/README.md
index fcb2022..5dd6926 100644
--- a/README.md
+++ b/README.md
@@ -31,6 +31,7 @@ scheduler.addSimpleIntervalJob(job)
 scheduler.stop()
 ```
 
+
 ## Usage with async tasks
 
 In order to avoid unhandled rejections, make sure to use AsyncTask if your task is asynchronous:
@@ -55,6 +56,50 @@ scheduler.stop()
 
 Note that in order to avoid memory leaks, it is recommended to use promise chains instead of async/await inside task definition. See [talk on common Promise mistakes](https://www.youtube.com/watch?v=XV-u_Ow47s0) for more details.
 
+## Using IDs and ES6-style imports
+
+You can attach IDs to tasks to identify them later. This is helpful in projects that run a lot of tasks and especially if you want to target some of the tasks specifically (e. g. in order to stop or restart them, or to check their status).
+
+```js
+import { ToadScheduler, SimpleIntervalJob, Task } from 'toad-scheduler';
+
+const scheduler = new ToadScheduler();
+
+const task = new Task('simple task', () => {
+	console.log('Task triggered');
+});
+
+const job1 = new SimpleIntervalJob(
+	{ seconds: 20, runImmediately: true },
+	task,
+	'id_1'
+);
+
+const job2 = new SimpleIntervalJob(
+	{ seconds: 15, runImmediately: true },
+	task,
+	'id_2'
+);
+
+//create and start jobs
+scheduler.addSimpleIntervalJob(job1);
+scheduler.addSimpleIntervalJob(job2);
+
+// stop job with ID: id_2
+scheduler.stopById('id_2');
+
+// remove job with ID: id_1
+scheduler.removeById('id_1');
+
+// check status of jobs
+console.log(scheduler.getById('id_1').getStatus()); // returns Error (job not found)
+
+console.log(scheduler.getById('id_2').getStatus()); // returns "stopped" and can be started again
+
+```
+
+
+
 ## API for schedule
 
 * `days?: number` - how many days to wait before executing the job for the next time;
@@ -73,6 +118,8 @@ Note that in order to avoid memory leaks, it is recommended to use promise chain
 ## API for scheduler
 
 * `addSimpleIntervalJob(job: SimpleIntervalJob): void` - registers and starts a new job;
+* `addLongIntervalJob(job: SimpleIntervalJob): void` - registers and starts a new job with support for intervals longer than 24.85 days;
+* `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.
 * `stopById(id: string): void` - stops the job with a given id.
diff --git a/index.ts b/index.ts
index 019697b..c6f51d6 100644
--- a/index.ts
+++ b/index.ts
@@ -3,6 +3,6 @@ export { AsyncTask } from './lib/common/AsyncTask'
 export { Task } from './lib/common/Task'
 export { Job } from './lib/common/Job'
 export type { JobStatus } from './lib/common/Job'
-
 export { SimpleIntervalJob } from './lib/engines/simple-interval/SimpleIntervalJob'
+export { LongIntervalJob } from './lib/engines/simple-interval/LongIntervalJob'
 export type { SimpleIntervalSchedule } from './lib/engines/simple-interval/SimpleIntervalSchedule'
diff --git a/lib/common/Task.ts b/lib/common/Task.ts
index 3d2d5e2..7f7448b 100644
--- a/lib/common/Task.ts
+++ b/lib/common/Task.ts
@@ -18,7 +18,7 @@ export class Task {
   execute(): void {
     try {
       this.handler()
-    } catch (err) {
+    } catch (err: any) {
       this.errorHandler(err)
     }
   }
diff --git a/lib/engines/simple-interval/LongIntervalJob.ts b/lib/engines/simple-interval/LongIntervalJob.ts
new file mode 100644
index 0000000..a0c8510
--- /dev/null
+++ b/lib/engines/simple-interval/LongIntervalJob.ts
@@ -0,0 +1,112 @@
+import Timeout = NodeJS.Timeout
+import { Job, JobStatus } from '../../common/Job'
+import { SimpleIntervalSchedule, toMsecs } from './SimpleIntervalSchedule'
+import { Task } from '../../common/Task'
+import { AsyncTask } from '../../common/AsyncTask'
+import { SimpleIntervalJob } from './SimpleIntervalJob'
+
+const MAX_TIMEOUT_DURATION_MS = 2147483647
+
+export class LongIntervalJob extends Job {
+  private childJob?: SimpleIntervalJob
+  private timer?: Timeout
+  private readonly schedule: SimpleIntervalSchedule
+  private readonly task: Task | AsyncTask
+  constructor(schedule: SimpleIntervalSchedule, task: Task | AsyncTask, id?: string) {
+    super(id)
+    this.schedule = schedule
+    this.task = task
+
+    // Create a future date for this task and get the number of ms
+    const taskPeriod = toMsecs(schedule)
+    // Initiate time-eating logic
+    if (taskPeriod >= MAX_TIMEOUT_DURATION_MS) {
+      this.setTimeEatingJob(taskPeriod)
+    }
+  }
+
+  private setTimeEatingJob(taskPeriod: number): void {
+    const mainTaskExecutionDate = new Date()
+    mainTaskExecutionDate.setTime(Date.now() + taskPeriod)
+    const mainTaskExecutionTime = mainTaskExecutionDate.getTime()
+    const startingRemainingMs = mainTaskExecutionTime - Date.now()
+
+    const timeEater = new Task('time eating task', () => {
+      const remainingMs = mainTaskExecutionTime - Date.now()
+      if (remainingMs >= MAX_TIMEOUT_DURATION_MS) {
+        /* istanbul ignore next */
+        this.childJob?.stop()
+        this.childJob = new SimpleIntervalJob(
+          {
+            milliseconds: Math.min(MAX_TIMEOUT_DURATION_MS - 1, remainingMs),
+          },
+          timeEater
+        )
+        this.childJob.start()
+      } else {
+        /* istanbul ignore next */
+        this.childJob?.stop()
+        this.childJob = new SimpleIntervalJob(
+          {
+            milliseconds: Math.min(MAX_TIMEOUT_DURATION_MS - 1, remainingMs),
+          },
+          new Task('Final mile task', () => {
+            this.setTimeEatingJob(toMsecs(this.schedule))
+            return this.task.execute()
+          })
+        )
+        this.childJob.start()
+      }
+    })
+
+    this.childJob?.stop()
+    this.childJob = new SimpleIntervalJob(
+      {
+        milliseconds: Math.min(MAX_TIMEOUT_DURATION_MS - 1, startingRemainingMs),
+      },
+      timeEater
+    )
+    this.childJob.start()
+  }
+
+  start(): void {
+    if (this.childJob) {
+      return this.childJob.start()
+    }
+
+    const time = toMsecs(this.schedule)
+    // Avoid starting duplicates and leaking previous timers
+    if (this.timer) {
+      this.stop()
+    }
+
+    if (this.schedule.runImmediately) {
+      this.task.execute()
+    }
+
+    this.timer = setInterval(() => {
+      this.task.execute()
+    }, time)
+  }
+
+  stop(): void {
+    if (this.childJob) {
+      return this.childJob.stop()
+    }
+    if (!this.timer) {
+      return
+    }
+    clearInterval(this.timer)
+    this.timer = undefined
+  }
+
+  getStatus(): JobStatus {
+    if (this.childJob) {
+      return this.childJob.getStatus()
+    }
+    if (this.timer) {
+      return JobStatus.RUNNING
+    }
+    return JobStatus.STOPPED
+  }
+}
diff --git a/lib/engines/simple-interval/SimpleIntervalEngine.ts b/lib/engines/simple-interval/SimpleIntervalEngine.ts
index 7f80964..2a73824 100644
--- a/lib/engines/simple-interval/SimpleIntervalEngine.ts
+++ b/lib/engines/simple-interval/SimpleIntervalEngine.ts
@@ -1,15 +1,17 @@
 import { SchedulerEngine } from '../../common/SchedulerEngine'
 import { SimpleIntervalJob } from './SimpleIntervalJob'
+import { LongIntervalJob } from './LongIntervalJob'
+import { Job } from '../../common/Job'
 
-export class SimpleIntervalEngine extends SchedulerEngine<SimpleIntervalJob> {
-  private readonly jobs: SimpleIntervalJob[]
+export class SimpleIntervalEngine extends SchedulerEngine<SimpleIntervalJob | LongIntervalJob> {
+  private readonly jobs: Job[]
 
   constructor() {
     super()
     this.jobs = []
   }
 
-  add(job: SimpleIntervalJob): void {
+  add(job: Job): void {
     this.jobs.push(job)
     job.start()
   }
diff --git a/lib/engines/simple-interval/SimpleIntervalJob.ts b/lib/engines/simple-interval/SimpleIntervalJob.ts
index 55df945..c42eab8 100644
--- a/lib/engines/simple-interval/SimpleIntervalJob.ts
+++ b/lib/engines/simple-interval/SimpleIntervalJob.ts
@@ -20,7 +20,7 @@ export class SimpleIntervalJob extends Job {
     // See https://github.com/kibertoad/toad-scheduler/issues/24
     if (time >= 2147483647) {
       throw new Error(
-        'Due to setInterval limitations, no intervals longer than 24.85 days can be scheduled correctly. toad-scheduler will eventually include a workaround for this, but for now your schedule is likely to break.'
+        'Due to setInterval limitations, no intervals longer than 24.85 days can be scheduled correctly. Please create LongIntervalJob instead.'
       )
     }
 
diff --git a/lib/toadScheduler.ts b/lib/toadScheduler.ts
index 67b7877..2be96f2 100644
--- a/lib/toadScheduler.ts
+++ b/lib/toadScheduler.ts
@@ -1,6 +1,7 @@
 import { SimpleIntervalEngine } from './engines/simple-interval/SimpleIntervalEngine'
 import { SimpleIntervalJob } from './engines/simple-interval/SimpleIntervalJob'
 import { Job } from './common/Job'
+import { LongIntervalJob } from './engines/simple-interval/LongIntervalJob'
 
 type EngineRegistry = {
   simpleIntervalEngine?: SimpleIntervalEngine
@@ -15,7 +16,7 @@ export class ToadScheduler {
     this.jobRegistry = {}
   }
 
-  addSimpleIntervalJob(job: SimpleIntervalJob): void {
+  addIntervalJob(job: SimpleIntervalJob | LongIntervalJob) {
     if (!this.engines.simpleIntervalEngine) {
       this.engines.simpleIntervalEngine = new SimpleIntervalEngine()
     }
@@ -30,6 +31,14 @@ export class ToadScheduler {
     this.engines.simpleIntervalEngine.add(job)
   }
 
+  addLongIntervalJob(job: LongIntervalJob): void {
+    return this.addIntervalJob(job)
+  }
+
+  addSimpleIntervalJob(job: SimpleIntervalJob): void {
+    return this.addIntervalJob(job)
+  }
+
   stop(): void {
     for (const engine of Object.values(this.engines)) {
       engine?.stop()
diff --git a/package.json b/package.json
index 43efd3b..28a53bd 100644
--- a/package.json
+++ b/package.json
@@ -15,21 +15,21 @@
     "build": "tsc",
     "test": "jest --config=jest.config.json",
     "test:coverage": "jest --config=jest.config.json --coverage",
-    "lint": "eslint --format codeframe \"lib/**/*.ts\" \"test/**/*.ts\"",
+    "lint": "eslint \"lib/**/*.ts\" \"test/**/*.ts\"",
     "prettier": "prettier --write \"{lib,test}/**/*.{js,ts}\" index.ts",
     "prepublishOnly": "npm run build"
   },
   "devDependencies": {
     "@types/jest": "^27.0.2",
-    "@types/node": "^16.10.2",
-    "@typescript-eslint/eslint-plugin": "^4.32.0",
-    "@typescript-eslint/parser": "^4.32.0",
-    "eslint": "^7.32.0",
+    "@types/node": "^16.11.6",
+    "@typescript-eslint/eslint-plugin": "^5.2.0",
+    "@typescript-eslint/parser": "^5.2.0",
+    "eslint": "^8.1.0",
     "eslint-config-prettier": "^8.3.0",
     "eslint-plugin-prettier": "^4.0.0",
-    "jest": "^27.2.4",
+    "jest": "^27.3.1",
     "prettier": "^2.4.1",
-    "ts-jest": "^27.0.5",
+    "ts-jest": "^27.0.7",
     "typescript": "4.4.4"
   },
   "homepage": "https://github.com/kibertoad/toad-scheduler",


Test patch:
diff --git a/test/LongIntervalJob.spec.ts b/test/LongIntervalJob.spec.ts
new file mode 100644
index 0000000..4f70b63
--- /dev/null
+++ b/test/LongIntervalJob.spec.ts
@@ -0,0 +1,372 @@
+import { ToadScheduler } from '../lib/toadScheduler'
+import { LongIntervalJob } from '../lib/engines/simple-interval/LongIntervalJob'
+import { Task } from '../lib/common/Task'
+import { NoopTask } from './utils/testTasks'
+
+describe('ToadScheduler', () => {
+  beforeEach(() => {
+    jest.useFakeTimers()
+  })
+
+  afterEach(() => {
+    jest.useRealTimers()
+  })
+
+  describe('LongIntervalJob', () => {
+    it('correctly returns status for started job', () => {
+      const scheduler = new ToadScheduler()
+      const task = new NoopTask()
+      const job = new LongIntervalJob(
+        {
+          seconds: 20,
+        },
+        task
+      )
+      scheduler.addLongIntervalJob(job)
+      expect(job.getStatus()).toBe('running')
+    })
+
+    it('correctly returns status for stopped job', () => {
+      const scheduler = new ToadScheduler()
+      const task = new NoopTask()
+      const job = new LongIntervalJob(
+        {
+          seconds: 20,
+        },
+        task
+      )
+      scheduler.addLongIntervalJob(job)
+      scheduler.stop()
+      expect(job.getStatus()).toBe('stopped')
+    })
+
+    it('correctly processes LongIntervalJob', () => {
+      let counter = 0
+      const scheduler = new ToadScheduler()
+      const task = new Task('simple task', () => {
+        counter++
+      })
+      const job = new LongIntervalJob(
+        {
+          seconds: 20,
+        },
+        task
+      )
+
+      scheduler.addLongIntervalJob(job)
+
+      expect(counter).toBe(0)
+      jest.advanceTimersByTime(19999)
+      expect(counter).toBe(0)
+      jest.advanceTimersByTime(1)
+      expect(counter).toBe(1)
+      jest.advanceTimersByTime(19999)
+      expect(counter).toBe(1)
+      jest.advanceTimersByTime(1)
+      expect(counter).toBe(2)
+
+      scheduler.stop()
+    })
+
+    it('allows executing job immediately', () => {
+      let counter = 0
+      const scheduler = new ToadScheduler()
+      const task = new Task('simple task', () => {
+        counter++
+      })
+      const job = new LongIntervalJob(
+        {
+          hours: 33,
+          runImmediately: true,
+        },
+        task
+      )
+
+      expect(counter).toBe(0)
+      scheduler.addIntervalJob(job)
+      expect(counter).toBe(1)
+      scheduler.stop()
+    })
+
+    it('correctly handles milliseconds', () => {
+      let counter = 0
+      const scheduler = new ToadScheduler()
+      const task = new Task('simple task', () => {
+        counter++
+      })
+      const job = new LongIntervalJob(
+        {
+          milliseconds: 33,
+        },
+        task
+      )
+
+      scheduler.addLongIntervalJob(job)
+
+      expect(counter).toBe(0)
+      jest.advanceTimersByTime(32)
+      expect(counter).toBe(0)
+      jest.advanceTimersByTime(1)
+      expect(counter).toBe(1)
+
+      scheduler.stop()
+    })
+
+    it('correctly handles adding job twice', () => {
+      let counter = 0
+      const scheduler = new ToadScheduler()
+      const task = new Task('simple task', () => {
+        counter++
+      })
+      const job = new LongIntervalJob(
+        {
+          milliseconds: 10,
+        },
+        task
+      )
+
+      scheduler.addLongIntervalJob(job)
+      scheduler.addLongIntervalJob(job)
+
+      expect(counter).toBe(0)
+      jest.advanceTimersByTime(20)
+      expect(counter).toBe(2)
+
+      scheduler.stop()
+    })
+
+    it('correctly stops jobs', () => {
+      let counter = 0
+      const scheduler = new ToadScheduler()
+      const task = new Task('simple task', () => {
+        counter++
+      })
+      const job = new LongIntervalJob(
+        {
+          milliseconds: 10,
+        },
+        task
+      )
+
+      scheduler.addLongIntervalJob(job)
+
+      expect(counter).toBe(0)
+      jest.advanceTimersByTime(20)
+      expect(counter).toBe(2)
+
+      scheduler.stop()
+      scheduler.stop()
+
+      jest.advanceTimersByTime(20)
+      expect(counter).toBe(2)
+    })
+
+    it('correctly handles minutes', () => {
+      let counter = 0
+      const scheduler = new ToadScheduler()
+      const task = new Task('simple task', () => {
+        counter++
+      })
+      const job = new LongIntervalJob(
+        {
+          minutes: 5,
+        },
+        task
+      )
+
+      scheduler.addLongIntervalJob(job)
+
+      expect(counter).toBe(0)
+      jest.advanceTimersByTime(299999)
+      expect(counter).toBe(0)
+      jest.advanceTimersByTime(1)
+      expect(counter).toBe(1)
+
+      scheduler.stop()
+    })
+
+    it('correctly handles hours', () => {
+      let counter = 0
+      const scheduler = new ToadScheduler()
+      const task = new Task('simple task', () => {
+        counter++
+      })
+      const job = new LongIntervalJob(
+        {
+          hours: 1,
+        },
+        task
+      )
+
+      scheduler.addLongIntervalJob(job)
+
+      expect(counter).toBe(0)
+      jest.advanceTimersByTime(3599999)
+      expect(counter).toBe(0)
+      jest.advanceTimersByTime(1)
+      expect(counter).toBe(1)
+
+      scheduler.stop()
+    })
+
+    it('correctly handles days', () => {
+      let counter = 0
+      const scheduler = new ToadScheduler()
+      const task = new Task('simple task', () => {
+        counter++
+      })
+      const job = new LongIntervalJob(
+        {
+          days: 1,
+        },
+        task
+      )
+
+      scheduler.addLongIntervalJob(job)
+
+      expect(counter).toBe(0)
+      jest.advanceTimersByTime(86399999)
+      expect(counter).toBe(0)
+      jest.advanceTimersByTime(1)
+      expect(counter).toBe(1)
+
+      scheduler.stop()
+    })
+
+    it('correctly handles large intervals', () => {
+      let counter = 0
+      const scheduler = new ToadScheduler()
+      const task = new Task('simple task', () => {
+        counter++
+      })
+      const job = new LongIntervalJob(
+        {
+          days: 25,
+        },
+        task
+      )
+      scheduler.addLongIntervalJob(job)
+
+      expect(counter).toBe(0)
+
+      for (let x = 0; x < 24; x++) {
+        jest.advanceTimersByTime(86_400_000)
+      }
+      jest.advanceTimersByTime(86_400_000 - 1000)
+      expect(counter).toBe(0)
+
+      jest.advanceTimersByTime(1000)
+      expect(counter).toBe(1)
+
+      scheduler.stop()
+    })
+
+    it('correctly handles status for large intervals', () => {
+      const scheduler = new ToadScheduler()
+      const task = new Task('simple task', () => {})
+      const job = new LongIntervalJob(
+        {
+          days: 25,
+        },
+        task
+      )
+      scheduler.addLongIntervalJob(job)
+      expect(job.getStatus()).toEqual('running')
+      job.stop()
+      expect(job.getStatus()).toEqual('stopped')
+      scheduler.stop()
+    })
+
+    it('correctly handles large intervals repeatedly', () => {
+      let counter = 0
+      const scheduler = new ToadScheduler()
+      const task = new Task('simple task', () => {
+        counter++
+      })
+      const job = new LongIntervalJob(
+        {
+          days: 25,
+        },
+        task
+      )
+      scheduler.addLongIntervalJob(job)
+
+      expect(counter).toBe(0)
+
+      for (let x = 0; x < 24; x++) {
+        jest.advanceTimersByTime(86_400_000)
+      }
+      jest.advanceTimersByTime(86_400_000 - 1000)
+      expect(counter).toBe(0)
+
+      jest.advanceTimersByTime(1000)
+      expect(counter).toBe(1)
+
+      for (let x = 0; x < 25; x++) {
+        jest.advanceTimersByTime(86_400_000)
+      }
+      expect(counter).toBe(2)
+      scheduler.stop()
+    })
+
+    it('correctly handles very large intervals', () => {
+      let counter = 0
+      const scheduler = new ToadScheduler()
+      const task = new Task('simple task', () => {
+        counter++
+      })
+      const job = new LongIntervalJob(
+        {
+          days: 55,
+        },
+        task
+      )
+      scheduler.addLongIntervalJob(job)
+
+      expect(counter).toBe(0)
+
+      for (let x = 0; x < 54; x++) {
+        jest.advanceTimersByTime(86_400_000)
+      }
+      jest.advanceTimersByTime(86_400_000 - 1000)
+      expect(counter).toBe(0)
+
+      jest.advanceTimersByTime(1000)
+      expect(counter).toBe(1)
+
+      scheduler.stop()
+    })
+
+    it('correctly handles a combination of time units', () => {
+      let counter = 0
+      const scheduler = new ToadScheduler()
+      const task = new Task('simple task', () => {
+        counter++
+      })
+      const job = new LongIntervalJob(
+        {
+          days: 1,
+          hours: 2,
+          minutes: 3,
+          seconds: 4,
+          milliseconds: 5,
+        },
+        task
+      )
+
+      scheduler.addLongIntervalJob(job)
+
+      expect(counter).toBe(0)
+      jest.advanceTimersByTime(86_400_000)
+      jest.advanceTimersByTime(3_600_000 * 2)
+      jest.advanceTimersByTime(60_000 * 3)
+      jest.advanceTimersByTime(1_000 * 4)
+      jest.advanceTimersByTime(4)
+      expect(counter).toBe(0)
+      jest.advanceTimersByTime(1)
+      expect(counter).toBe(1)
+
+      scheduler.stop()
+    })
+  })
+})
diff --git a/test/SimpleIntervalJob.spec.ts b/test/SimpleIntervalJob.spec.ts
index 176a940..917e7f4 100644
--- a/test/SimpleIntervalJob.spec.ts
+++ b/test/SimpleIntervalJob.spec.ts
@@ -247,6 +247,7 @@ describe('ToadScheduler', () => {
       )
 
       expect(() => scheduler.addSimpleIntervalJob(job)).toThrow(/can be scheduled correctly/)
+      expect(counter).toEqual(0)
       scheduler.stop()
     })