Compare commits

..

No commits in common. "64f78395284d33b2c1a04672f911a764e852c50c" and "d746e8d5e1a54988e46c70948489c77c23b64f8b" have entirely different histories.

6 changed files with 184 additions and 300 deletions

3
.gitignore vendored
View File

@ -15,9 +15,6 @@ node_modules/
# Build folder
dist
# Jest coverage report
coverage
# Lock files
yarn.lock
package-lock.json

View File

@ -1,6 +1,6 @@
# Schedule Later
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`.
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`.
## Install
@ -17,7 +17,7 @@ yarn add schedule-later
## Import
```typescript
import { startTimeout, startInterval, TimeInMS } from 'schedule-later'
import Scheduler from 'schedule-later'
```
## Key Concepts
@ -48,30 +48,34 @@ export type TimeUntil = {
## Usage
The `Scheduler` class provides two main static methods: `startTimeout` and `startInterval`.
### startTimeout
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).
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).
```typescript
function startTimeout(timerFunc: Function, start: TimeUntil): StopFunction
public static startTimeout(
timerFunc: Function,
start: TimeUntil
): StopFunction;
```
### startInterval
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).
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).
```typescript
function startInterval(
public static startInterval(
intervalFunc: Function,
intervalMS: number,
start?: TimeUntil
callbackAfterTimeout: boolean = false
): StopFunction
): StopFunction;
```
## Stop Functions
Both the `startTimeout` and `startInterval` functions return a `StopFunction`. This function can be called to cancel a timeout or interval.
Both the `startTimeout` and `startInterval` methods 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.
@ -83,7 +87,7 @@ type StopFunction = (stopTime?: TimeUntil) => StopCancelFunction | null
## Stop Cancel Functions
The `StopFunction` will return a `StopCancelFunction` when called with a stopTime. This function can be called to cancel a scheduled stop.
The `StopFunction` returns a `StopCancelFunction` when called. This function can be called to cancel a scheduled stop.
```typescript
type StopCancelFunction = (stopRunning: boolean = false) => void
@ -118,11 +122,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. Uses the TimeInMS enum to clearly specify the interval.
3. Using startInterval with a specific interval, basically a regular setInterval
```typescript
const sayHello = () => console.log('Hello, world!')
let stopInterval = Scheduler.startInterval(sayHello, TimeInMS.SECOND * 5)
let stopInterval = Scheduler.startInterval(sayHello, 1000)
// Later, if you want to stop the interval
stopInterval()

View File

@ -1,25 +1,18 @@
{
"name": "schedule-later",
"version": "1.1.2",
"types": "dist/Scheduler.d.ts",
"version": "1.0.1",
"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",
"coverage": "./node_modules/.bin/jest --coverage",
"prepublish": "tsc && npm run test"
"watch": "./node_modules/.bin/jest --watch",
"deploy": "npm run build && npm publish"
},
"devDependencies": {
"@types/jest": "^29.5.3",
"@types/node": "^18.11.18",
"@types/rewire": "^2.5.28",
"jest": "^29.6.1",
"typescript": "^5.1.6"
"typescript": "^4.6.4"
}
}

View File

@ -1,21 +1,21 @@
// @ts-ignore - TS doesn't know this is allowed
import { startTimeout, startInterval, TimeInMS } from './Scheduler'
import Scheduler from './Scheduler'
beforeEach(() => {
describe('Scheduler', () => {
beforeEach(() => {
jest.useFakeTimers()
})
})
afterEach(() => {
afterEach(() => {
jest.clearAllTimers()
})
})
describe('startTimeout Tests', () => {
test('should start and stop properly', () => {
test('startTimeout should correctly start and stop timeout', () => {
const callback = jest.fn()
const stop = startTimeout(callback, { ms: 5000 })
const stop = Scheduler.startTimeout(callback, { ms: 5000 })
// Advance timers by less than the delay and check if callback has not been called
jest.advanceTimersByTime(TimeInMS.SECOND * 2)
jest.advanceTimersByTime(2000)
expect(callback).not.toBeCalled()
// Advance timers to exactly the delay and check if callback has been called
@ -26,61 +26,13 @@ describe('startTimeout Tests', () => {
stop()
})
test('should not call immediately', () => {
test('startInterval should correctly start and stop interval', () => {
const callback = jest.fn()
const stop = startTimeout(callback, { 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()
const stop = Scheduler.startInterval(callback, 2000, { ms: 5000 })
// Advance timers by less than the initial delay and check if callback has not been called
jest.advanceTimersByTime(TimeInMS.SECOND * 2)
jest.advanceTimersByTime(2000)
expect(callback).not.toBeCalled()
// Advance timers to exactly the delay and check if callback has been called
@ -88,43 +40,10 @@ describe('startInterval Tests', () => {
expect(callback).toBeCalledTimes(1)
// Advance timers by the interval and check if callback has been called again
jest.advanceTimersByTime(TimeInMS.SECOND * 2)
jest.advanceTimersByTime(2000)
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()
})
})

View File

@ -1,37 +1,18 @@
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 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 {
export default class Scheduler {
private static timeUntil(start: TimeUntil) {
if (start.ms) {
return start.ms
}
@ -40,8 +21,9 @@ function timeUntil(start: TimeUntil): number {
let now = new Date()
// set the target time
const targetTime = start.timeOfDay
? new Date(
let targetTime = !start.timeOfDay
? start.date
: new Date(
now.getFullYear(),
now.getMonth(),
now.getDate(),
@ -50,7 +32,6 @@ function timeUntil(start: TimeUntil): number {
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) {
@ -62,129 +43,55 @@ function timeUntil(start: TimeUntil): number {
let delay = targetTime - now
return delay
}
}
/**
/**
* Start a timeout
* @param {Function} timerFunc
* @param {TimeUntil} start
* @return {StopFunction}
*/
export function startTimeout(
public static startTimeout(
timerFunc: Function,
start: TimeUntil
): StopFunction {
let delay = timeUntil(start)
): StopFunction {
let delay = this.timeUntil(start)
// set a timeout to start the interval at the target time
let timeout: NodeJS.Timeout | null = null
let timeout: NodeJS.Timeout = 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)
}
/**
* Stops a timeout
* @param {TimeUntil} stopTime - optional
* @return {StopCancelFunction}
*/
const stop = (stopTime?: TimeUntil): StopCancelFunction | null => {
if (!stopTime) {
stopNow()
return null
}
// Set a timeout to stop the timeout at the target time
const stopTimeout = setTimeout(stopNow, timeUntil(stopTime))
/**
* Cancels a delayed stop
* @param {boolean} stopRunning - optional
* @return void
*/
return (stopRunning: boolean = false) => {
clearTimeout(stopTimeout)
if (stopRunning) stopNow()
}
}
// return a cleanup function
return stop
}
/**
* Start an interval
* @param {Function} intervalFunc
* @param {number} intervalMS
* @param {TimeUntil} start - optional
* @param {boolean} callbackAfterTimeout - optional
* @return {StopFunction}
*/
export function startInterval(
intervalFunc: Function,
intervalMS: number,
start?: TimeUntil,
callbackAfterTimeout: boolean = false
): StopFunction {
let delay = start ? timeUntil(start) : 0
// set a timeout to start the interval at the target time
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)
// 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)
}
/**
* Stops an interval
* @param {TimeUntil} stopTime - optional
* @return {StopCancelFunction}
*/
const stop = (stopTime?: TimeUntil): StopCancelFunction | null => {
// 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) {
stopNow()
return null
}
const stopTimeout = setTimeout(stopNow, timeUntil(stopTime))
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()
}
}
/**
* Cancels a delayed stop
* @param {boolean} stopRunning - optional
* @return void
*/
const stopTimeout = setTimeout(stopNow, this.timeUntil(stopTime))
// 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()
@ -193,4 +100,67 @@ export function startInterval(
// return a cleanup function
return stop
}
/**
* Start an interval
* @param {Function} intervalFunc
* @param {number} intervalMS
* @param {TimeUntil} start
* @return {StopFunction}
*/
public static startInterval(
intervalFunc: Function,
intervalMS: number,
start?: TimeUntil
): StopFunction {
let delay = this.timeUntil(start)
// set a timeout to start the interval at the target time
let interval: number = null
let timeout = setTimeout(function () {
// Clear the timeout variable
timeout = null
// start the interval
interval = setInterval(intervalFunc, intervalMS)
// call the function immediately
intervalFunc()
}, delay)
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 => {
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, this.timeUntil(stopTime))
// 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()
}
}
// return a cleanup function
return stop
}
}

View File

@ -3,11 +3,12 @@
"declaration": true,
"declarationDir": "dist",
"target": "es2022" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
"module": "CommonJS" /* Specify what module code is generated. */,
"module": "nodenext" /* 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": true /* Enable all strict type-checking options. */
"strict": false /* Enable all strict type-checking options. */,
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}