Compare commits
10 Commits
d746e8d5e1
...
64f7839528
Author | SHA1 | Date |
---|---|---|
Josh Guyette | 64f7839528 | |
Josh Guyette | f4a2615e4f | |
Josh Guyette | 6a7a656fa0 | |
Josh Guyette | d9517c319a | |
Josh Guyette | 48440fccb0 | |
Josh Guyette | 1f46191482 | |
Josh Guyette | 617fdebee3 | |
Josh Guyette | 57239561f7 | |
Josh Guyette | f6a78b99fe | |
Josh Guyette | 98e9a6ed68 |
|
@ -15,6 +15,9 @@ node_modules/
|
|||
# Build folder
|
||||
dist
|
||||
|
||||
# Jest coverage report
|
||||
coverage
|
||||
|
||||
# Lock files
|
||||
yarn.lock
|
||||
package-lock.json
|
||||
|
|
28
README.md
28
README.md
|
@ -1,6 +1,6 @@
|
|||
# Schedule Later
|
||||
|
||||
schedule-later is a static class that provides methods for managing date-time based tasks, such as starting timeouts and intervals at a specific time of day. Under-the-hood, it uses `setTimeout` and `setInterval`.
|
||||
schedule-later provides functions for managing date-time based tasks, such as starting timeouts and intervals at a specific time of day. Under-the-hood, it uses `setTimeout` and `setInterval`.
|
||||
|
||||
## Install
|
||||
|
||||
|
@ -17,7 +17,7 @@ yarn add schedule-later
|
|||
## Import
|
||||
|
||||
```typescript
|
||||
import Scheduler from 'schedule-later'
|
||||
import { startTimeout, startInterval, TimeInMS } from 'schedule-later'
|
||||
```
|
||||
|
||||
## Key Concepts
|
||||
|
@ -48,34 +48,30 @@ export type TimeUntil = {
|
|||
|
||||
## Usage
|
||||
|
||||
The `Scheduler` class provides two main static methods: `startTimeout` and `startInterval`.
|
||||
|
||||
### startTimeout
|
||||
|
||||
The `startTimeout` method starts a timeout that calls a given function after a specific delay. The delay is calculated based on the `TimeUntil` object passed to it. The method returns a `StopFunction` (see below).
|
||||
The `startTimeout` function starts a timeout that calls a given function after a specific delay. The delay is calculated based on the `TimeUntil` object passed to it. The function returns a `StopFunction` (see below).
|
||||
|
||||
```typescript
|
||||
public static startTimeout(
|
||||
timerFunc: Function,
|
||||
start: TimeUntil
|
||||
): StopFunction;
|
||||
function startTimeout(timerFunc: Function, start: TimeUntil): StopFunction
|
||||
```
|
||||
|
||||
### startInterval
|
||||
|
||||
The `startInterval` method starts an interval that calls a given function repeatedly with a fixed time delay between each call. Like `startTimeout`, the initial delay is calculated based on a `TimeUntil` object. The method returns a `StopFunction` (see below).
|
||||
The `startInterval` function starts an interval that calls a given function repeatedly with a fixed time delay between each call. Like `startTimeout`, the initial delay is calculated based on a `TimeUntil` object. If called with `callbackAfterTimeout` set to `true`, it will call `intervalFunc` after the timeout has finished running (right when starting the interval). The function returns a `StopFunction` (see below).
|
||||
|
||||
```typescript
|
||||
public static startInterval(
|
||||
function startInterval(
|
||||
intervalFunc: Function,
|
||||
intervalMS: number,
|
||||
start?: TimeUntil
|
||||
): StopFunction;
|
||||
callbackAfterTimeout: boolean = false
|
||||
): StopFunction
|
||||
```
|
||||
|
||||
## Stop Functions
|
||||
|
||||
Both the `startTimeout` and `startInterval` methods return a `StopFunction`. This function can be called to cancel a timeout or interval.
|
||||
Both the `startTimeout` and `startInterval` functions return a `StopFunction`. This function can be called to cancel a timeout or interval.
|
||||
|
||||
When called with no arguments, the `StopFunction` stops the timeout or interval immediately. If called with a `TimeUntil` argument, it schedules a stop at the specified time.
|
||||
|
||||
|
@ -87,7 +83,7 @@ type StopFunction = (stopTime?: TimeUntil) => StopCancelFunction | null
|
|||
|
||||
## Stop Cancel Functions
|
||||
|
||||
The `StopFunction` returns a `StopCancelFunction` when called. This function can be called to cancel a scheduled stop.
|
||||
The `StopFunction` will return a `StopCancelFunction` when called with a stopTime. This function can be called to cancel a scheduled stop.
|
||||
|
||||
```typescript
|
||||
type StopCancelFunction = (stopRunning: boolean = false) => void
|
||||
|
@ -122,11 +118,11 @@ In the `StopCancelFunction`, if the `stopRunning` parameter is `true`, it stops
|
|||
In this example, the goodMorning function will be called at 7:00 AM. If you want to cancel the morning greeting (for example, the user chose to sleep in), you can call the stopTimeout function.
|
||||
|
||||
|
||||
3. Using startInterval with a specific interval, basically a regular setInterval
|
||||
3. Using startInterval with a specific interval, basically a regular setInterval. Uses the TimeInMS enum to clearly specify the interval.
|
||||
|
||||
```typescript
|
||||
const sayHello = () => console.log('Hello, world!')
|
||||
let stopInterval = Scheduler.startInterval(sayHello, 1000)
|
||||
let stopInterval = Scheduler.startInterval(sayHello, TimeInMS.SECOND * 5)
|
||||
|
||||
// Later, if you want to stop the interval
|
||||
stopInterval()
|
||||
|
|
15
package.json
15
package.json
|
@ -1,18 +1,25 @@
|
|||
{
|
||||
"name": "schedule-later",
|
||||
"version": "1.0.1",
|
||||
"version": "1.1.2",
|
||||
"types": "dist/Scheduler.d.ts",
|
||||
"main": "dist/Scheduler.js",
|
||||
"author": "Nightness",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/nightness/schedule-later.git"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"pretest": "tsc",
|
||||
"test": "./node_modules/.bin/jest --verbose",
|
||||
"watch": "./node_modules/.bin/jest --watch",
|
||||
"deploy": "npm run build && npm publish"
|
||||
"coverage": "./node_modules/.bin/jest --coverage",
|
||||
"prepublish": "tsc && npm run test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.3",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/rewire": "^2.5.28",
|
||||
"jest": "^29.6.1",
|
||||
"typescript": "^4.6.4"
|
||||
"typescript": "^5.1.6"
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import Scheduler from './Scheduler'
|
||||
// @ts-ignore - TS doesn't know this is allowed
|
||||
import { startTimeout, startInterval, TimeInMS } from './Scheduler'
|
||||
|
||||
describe('Scheduler', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers()
|
||||
})
|
||||
|
@ -9,13 +9,13 @@ describe('Scheduler', () => {
|
|||
jest.clearAllTimers()
|
||||
})
|
||||
|
||||
test('startTimeout should correctly start and stop timeout', () => {
|
||||
describe('startTimeout Tests', () => {
|
||||
test('should start and stop properly', () => {
|
||||
const callback = jest.fn()
|
||||
|
||||
const stop = Scheduler.startTimeout(callback, { ms: 5000 })
|
||||
const stop = startTimeout(callback, { ms: 5000 })
|
||||
|
||||
// Advance timers by less than the delay and check if callback has not been called
|
||||
jest.advanceTimersByTime(2000)
|
||||
jest.advanceTimersByTime(TimeInMS.SECOND * 2)
|
||||
expect(callback).not.toBeCalled()
|
||||
|
||||
// Advance timers to exactly the delay and check if callback has been called
|
||||
|
@ -26,13 +26,61 @@ describe('Scheduler', () => {
|
|||
stop()
|
||||
})
|
||||
|
||||
test('startInterval should correctly start and stop interval', () => {
|
||||
test('should not call immediately', () => {
|
||||
const callback = jest.fn()
|
||||
const stop = startTimeout(callback, { ms: 5000 })
|
||||
|
||||
const stop = Scheduler.startInterval(callback, 2000, { ms: 5000 })
|
||||
// Should not be called immediately
|
||||
expect(callback).not.toBeCalled()
|
||||
|
||||
// Cleanup
|
||||
stop()
|
||||
})
|
||||
|
||||
test('should not call the function if stop function is called', () => {
|
||||
const callback = jest.fn()
|
||||
const stop = startTimeout(callback, { ms: TimeInMS.SECOND })
|
||||
|
||||
// Stop the timeout
|
||||
stop()
|
||||
|
||||
// Advance timers by less than the delay and check if callback has not been called
|
||||
jest.advanceTimersByTime(TimeInMS.SECOND)
|
||||
expect(callback).not.toBeCalled()
|
||||
|
||||
// Advance timers by less than the delay and check if callback has not been called
|
||||
jest.advanceTimersByTime(TimeInMS.SECOND)
|
||||
expect(callback).not.toBeCalled()
|
||||
})
|
||||
|
||||
test('should still call the function if stop function is called with a delay then canceled', () => {
|
||||
const callback = jest.fn()
|
||||
const stop = startTimeout(callback, { ms: TimeInMS.SECOND * 5 })
|
||||
|
||||
// Cancel the timeout, in 5 seconds
|
||||
const stopCancel = stop({
|
||||
ms: TimeInMS.SECOND * 5,
|
||||
})!
|
||||
|
||||
// Abort that cancel
|
||||
stopCancel()
|
||||
|
||||
// Finish timer
|
||||
jest.advanceTimersByTime(TimeInMS.SECOND * 5)
|
||||
expect(callback).toBeCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('startInterval Tests', () => {
|
||||
test('should correctly start and repeat interval', () => {
|
||||
const callback = jest.fn()
|
||||
const stop = startInterval(callback, TimeInMS.SECOND * 2, { ms: 5000 })
|
||||
|
||||
// Doesn't call the function right away
|
||||
expect(callback).not.toBeCalled()
|
||||
|
||||
// Advance timers by less than the initial delay and check if callback has not been called
|
||||
jest.advanceTimersByTime(2000)
|
||||
jest.advanceTimersByTime(TimeInMS.SECOND * 2)
|
||||
expect(callback).not.toBeCalled()
|
||||
|
||||
// Advance timers to exactly the delay and check if callback has been called
|
||||
|
@ -40,10 +88,43 @@ describe('Scheduler', () => {
|
|||
expect(callback).toBeCalledTimes(1)
|
||||
|
||||
// Advance timers by the interval and check if callback has been called again
|
||||
jest.advanceTimersByTime(2000)
|
||||
jest.advanceTimersByTime(TimeInMS.SECOND * 2)
|
||||
expect(callback).toBeCalledTimes(2)
|
||||
|
||||
// Cleanup
|
||||
stop()
|
||||
})
|
||||
|
||||
test('should not call the function if stop function is called', () => {
|
||||
const callback = jest.fn()
|
||||
const stop = startInterval(callback, TimeInMS.SECOND * 2, {
|
||||
ms: TimeInMS.SECOND,
|
||||
})
|
||||
|
||||
expect(callback).not.toBeCalled()
|
||||
|
||||
stop()
|
||||
|
||||
jest.advanceTimersByTime(TimeInMS.SECOND)
|
||||
expect(callback).not.toBeCalled()
|
||||
})
|
||||
|
||||
test('should still call the function if the stop function is called (with a delay), and the stopCancel function is called before the delay expires', () => {
|
||||
const callback = jest.fn()
|
||||
const stop = startInterval(callback, TimeInMS.SECOND * 2)
|
||||
|
||||
jest.advanceTimersByTime(TimeInMS.SECOND * 2)
|
||||
expect(callback).toBeCalled()
|
||||
|
||||
const stopCancel = stop({
|
||||
ms: TimeInMS.SECOND * 5,
|
||||
})
|
||||
stopCancel?.()
|
||||
|
||||
jest.advanceTimersByTime(TimeInMS.SECOND * 2)
|
||||
expect(callback).toBeCalledTimes(2)
|
||||
|
||||
// cleanup
|
||||
stop()
|
||||
})
|
||||
})
|
||||
|
|
136
src/Scheduler.ts
136
src/Scheduler.ts
|
@ -1,18 +1,37 @@
|
|||
export enum TimeInMS {
|
||||
SECOND = 1000,
|
||||
MINUTE = 60000,
|
||||
HALF_HOUR = 1800000,
|
||||
HOUR = 3600000,
|
||||
HALF_DAY = 43200000,
|
||||
DAY = 86400000,
|
||||
WEEK = 604800000,
|
||||
FOUR_SCORE = 1209600000, // 14 days
|
||||
MONTH = 2592000000,
|
||||
}
|
||||
|
||||
export interface TimeOfDay {
|
||||
hour: number
|
||||
minute?: number
|
||||
seconds?: number
|
||||
}
|
||||
|
||||
export type TimeUntil = {
|
||||
timeOfDay?: TimeOfDay
|
||||
date?: Date
|
||||
ms?: number
|
||||
}
|
||||
type StopCancelFunction = (stopRunning: boolean) => void
|
||||
type StopFunction = (stopTime?: TimeUntil) => StopCancelFunction | null
|
||||
|
||||
export default class Scheduler {
|
||||
private static timeUntil(start: TimeUntil) {
|
||||
export type StopCancelFunction = (stopRunning?: boolean) => void
|
||||
|
||||
export type StopFunction = (stopTime?: TimeUntil) => StopCancelFunction | null
|
||||
|
||||
/**
|
||||
* Used to convert a TimeUntil object into a number of milliseconds
|
||||
* @param {TimeUntil} start
|
||||
* @return {number} delay
|
||||
*/
|
||||
function timeUntil(start: TimeUntil): number {
|
||||
if (start.ms) {
|
||||
return start.ms
|
||||
}
|
||||
|
@ -21,9 +40,8 @@ export default class Scheduler {
|
|||
let now = new Date()
|
||||
|
||||
// set the target time
|
||||
let targetTime = !start.timeOfDay
|
||||
? start.date
|
||||
: new Date(
|
||||
const targetTime = start.timeOfDay
|
||||
? new Date(
|
||||
now.getFullYear(),
|
||||
now.getMonth(),
|
||||
now.getDate(),
|
||||
|
@ -32,6 +50,7 @@ export default class Scheduler {
|
|||
start.timeOfDay.seconds ?? 0,
|
||||
0
|
||||
)
|
||||
: start.date ?? now
|
||||
|
||||
// if the target time has already passed today, set it for tomorrow
|
||||
if (start.timeOfDay && now > targetTime) {
|
||||
|
@ -51,47 +70,49 @@ export default class Scheduler {
|
|||
* @param {TimeUntil} start
|
||||
* @return {StopFunction}
|
||||
*/
|
||||
public static startTimeout(
|
||||
export function startTimeout(
|
||||
timerFunc: Function,
|
||||
start: TimeUntil
|
||||
): StopFunction {
|
||||
let delay = this.timeUntil(start)
|
||||
let delay = timeUntil(start)
|
||||
|
||||
// set a timeout to start the interval at the target time
|
||||
let timeout: NodeJS.Timeout = null
|
||||
let timeout: NodeJS.Timeout | null = null
|
||||
timeout = setTimeout(function () {
|
||||
// Clear the timeout variable
|
||||
timeout = null
|
||||
|
||||
// call the function immediately
|
||||
timerFunc()
|
||||
}, delay)
|
||||
|
||||
/**
|
||||
* Cleanup function
|
||||
* @return {void}
|
||||
*/
|
||||
const stopNow = () => {
|
||||
if (timeout) clearTimeout(timeout)
|
||||
}
|
||||
|
||||
// stop() function to stop the interval and timeout
|
||||
// stop(stopInMS) will stop the interval and timeout in stopInMS milliseconds
|
||||
// stop(stopHour, stopMinute) will stop the interval and timeout at the next stopHour:stopMinute
|
||||
const stop = (stopTime?: TimeUntil): StopCancelFunction => {
|
||||
if (stopTime === undefined) {
|
||||
/**
|
||||
* Stops a timeout
|
||||
* @param {TimeUntil} stopTime - optional
|
||||
* @return {StopCancelFunction}
|
||||
*/
|
||||
const stop = (stopTime?: TimeUntil): StopCancelFunction | null => {
|
||||
if (!stopTime) {
|
||||
stopNow()
|
||||
return null
|
||||
}
|
||||
|
||||
if ((stopTime as any) instanceof Date) {
|
||||
// @ts-ignore - TS doesn't know this is allowed
|
||||
const timeFromNow = stopTime - new Date()
|
||||
const stopTimeout = setTimeout(stopNow, timeFromNow)
|
||||
// stopRunning is a boolean that will either cancel the stop (false), or stop the interval now (true)
|
||||
return (stopRunning: boolean = false) => {
|
||||
clearTimeout(stopTimeout)
|
||||
if (stopRunning) stopNow()
|
||||
}
|
||||
}
|
||||
// Set a timeout to stop the timeout at the target time
|
||||
const stopTimeout = setTimeout(stopNow, timeUntil(stopTime))
|
||||
|
||||
const stopTimeout = setTimeout(stopNow, this.timeUntil(stopTime))
|
||||
// stopRunning is a boolean that will either cancel the stop (false), or stop the interval now (true)
|
||||
/**
|
||||
* Cancels a delayed stop
|
||||
* @param {boolean} stopRunning - optional
|
||||
* @return void
|
||||
*/
|
||||
return (stopRunning: boolean = false) => {
|
||||
clearTimeout(stopTimeout)
|
||||
if (stopRunning) stopNow()
|
||||
|
@ -106,54 +127,64 @@ export default class Scheduler {
|
|||
* Start an interval
|
||||
* @param {Function} intervalFunc
|
||||
* @param {number} intervalMS
|
||||
* @param {TimeUntil} start
|
||||
* @param {TimeUntil} start - optional
|
||||
* @param {boolean} callbackAfterTimeout - optional
|
||||
* @return {StopFunction}
|
||||
*/
|
||||
public static startInterval(
|
||||
export function startInterval(
|
||||
intervalFunc: Function,
|
||||
intervalMS: number,
|
||||
start?: TimeUntil
|
||||
start?: TimeUntil,
|
||||
callbackAfterTimeout: boolean = false
|
||||
): StopFunction {
|
||||
let delay = this.timeUntil(start)
|
||||
let delay = start ? timeUntil(start) : 0
|
||||
|
||||
// set a timeout to start the interval at the target time
|
||||
let interval: number = null
|
||||
let timeout = setTimeout(function () {
|
||||
let interval: number | null =
|
||||
delay === 0 ? setInterval(intervalFunc, intervalMS) : null
|
||||
let timeout: NodeJS.Timeout | null =
|
||||
delay > 0
|
||||
? setTimeout(function () {
|
||||
// Clear the timeout variable
|
||||
timeout = null
|
||||
|
||||
// start the interval
|
||||
interval = setInterval(intervalFunc, intervalMS)
|
||||
// call the function immediately
|
||||
intervalFunc()
|
||||
}, delay)
|
||||
|
||||
// Invoke the callback, just because the timer expired (so at the beginning of the interval)
|
||||
if (callbackAfterTimeout) {
|
||||
intervalFunc()
|
||||
}
|
||||
}, delay)
|
||||
: null
|
||||
|
||||
/**
|
||||
* Cleanup function
|
||||
* @return {void}
|
||||
*/
|
||||
const stopNow = () => {
|
||||
if (timeout) clearTimeout(timeout)
|
||||
if (interval) clearInterval(interval)
|
||||
}
|
||||
|
||||
// stop() function to stop the interval and timeout
|
||||
// stop(stopInMS) will stop the interval and timeout in stopInMS milliseconds
|
||||
// stop(stopHour, stopMinute) will stop the interval and timeout at the next stopHour:stopMinute
|
||||
const stop = (stopTime?: TimeUntil): StopCancelFunction => {
|
||||
/**
|
||||
* Stops an interval
|
||||
* @param {TimeUntil} stopTime - optional
|
||||
* @return {StopCancelFunction}
|
||||
*/
|
||||
const stop = (stopTime?: TimeUntil): StopCancelFunction | null => {
|
||||
if (stopTime === undefined) {
|
||||
stopNow()
|
||||
return null
|
||||
}
|
||||
|
||||
if ((stopTime as any) instanceof Date) {
|
||||
// @ts-ignore - TS doesn't know this is allowed
|
||||
const timeFromNow = stopTime - new Date()
|
||||
const stopTimeout = setTimeout(stopNow, timeFromNow)
|
||||
// stopRunning is a boolean that will either cancel the stop (false), or stop the interval now (true)
|
||||
return (stopRunning: boolean = false) => {
|
||||
clearTimeout(stopTimeout)
|
||||
if (stopRunning) stopNow()
|
||||
}
|
||||
}
|
||||
const stopTimeout = setTimeout(stopNow, timeUntil(stopTime))
|
||||
|
||||
const stopTimeout = setTimeout(stopNow, this.timeUntil(stopTime))
|
||||
// stopRunning is a boolean that will either cancel the stop (false), or stop the interval now (true)
|
||||
/**
|
||||
* Cancels a delayed stop
|
||||
* @param {boolean} stopRunning - optional
|
||||
* @return void
|
||||
*/
|
||||
return (stopRunning: boolean = false) => {
|
||||
clearTimeout(stopTimeout)
|
||||
if (stopRunning) stopNow()
|
||||
|
@ -163,4 +194,3 @@ export default class Scheduler {
|
|||
// return a cleanup function
|
||||
return stop
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,12 +3,11 @@
|
|||
"declaration": true,
|
||||
"declarationDir": "dist",
|
||||
"target": "es2022" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
|
||||
"module": "nodenext" /* Specify what module code is generated. */,
|
||||
"module": "CommonJS" /* Specify what module code is generated. */,
|
||||
"rootDirs": ["./src"],
|
||||
"outDir": "./dist" /* Specify an output folder for all emitted files. */,
|
||||
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */,
|
||||
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
|
||||
"strict": false /* Enable all strict type-checking options. */,
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||
"strict": true /* Enable all strict type-checking options. */
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue