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 # Build folder
dist dist
# Jest coverage report
coverage
# Lock files # Lock files
yarn.lock yarn.lock
package-lock.json package-lock.json

View File

@ -1,6 +1,6 @@
# Schedule Later # 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 ## Install
@ -17,7 +17,7 @@ yarn add schedule-later
## Import ## Import
```typescript ```typescript
import { startTimeout, startInterval, TimeInMS } from 'schedule-later' import Scheduler from 'schedule-later'
``` ```
## Key Concepts ## Key Concepts
@ -48,30 +48,34 @@ export type TimeUntil = {
## Usage ## Usage
The `Scheduler` class provides two main static methods: `startTimeout` and `startInterval`.
### startTimeout ### 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 ```typescript
function startTimeout(timerFunc: Function, start: TimeUntil): StopFunction public static startTimeout(
timerFunc: Function,
start: TimeUntil
): StopFunction;
``` ```
### startInterval ### 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 ```typescript
function startInterval( public static startInterval(
intervalFunc: Function, intervalFunc: Function,
intervalMS: number, intervalMS: number,
start?: TimeUntil start?: TimeUntil
callbackAfterTimeout: boolean = false ): StopFunction;
): StopFunction
``` ```
## Stop Functions ## 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. 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 ## 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 ```typescript
type StopCancelFunction = (stopRunning: boolean = false) => void 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. 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 ```typescript
const sayHello = () => console.log('Hello, world!') 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 // Later, if you want to stop the interval
stopInterval() stopInterval()

View File

@ -1,25 +1,18 @@
{ {
"name": "schedule-later", "name": "schedule-later",
"version": "1.1.2", "version": "1.0.1",
"types": "dist/Scheduler.d.ts",
"main": "dist/Scheduler.js", "main": "dist/Scheduler.js",
"author": "Nightness", "author": "Nightness",
"repository": {
"type": "git",
"url": "https://github.com/nightness/schedule-later.git"
},
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
"pretest": "tsc",
"test": "./node_modules/.bin/jest --verbose", "test": "./node_modules/.bin/jest --verbose",
"coverage": "./node_modules/.bin/jest --coverage", "watch": "./node_modules/.bin/jest --watch",
"prepublish": "tsc && npm run test" "deploy": "npm run build && npm publish"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^29.5.3", "@types/jest": "^29.5.3",
"@types/node": "^18.11.18", "@types/node": "^18.11.18",
"@types/rewire": "^2.5.28",
"jest": "^29.6.1", "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 Scheduler from './Scheduler'
import { startTimeout, startInterval, TimeInMS } from './Scheduler'
beforeEach(() => { describe('Scheduler', () => {
beforeEach(() => {
jest.useFakeTimers() jest.useFakeTimers()
}) })
afterEach(() => { afterEach(() => {
jest.clearAllTimers() jest.clearAllTimers()
}) })
describe('startTimeout Tests', () => { test('startTimeout should correctly start and stop timeout', () => {
test('should start and stop properly', () => {
const callback = jest.fn() 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 // 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() expect(callback).not.toBeCalled()
// Advance timers to exactly the delay and check if callback has been called // Advance timers to exactly the delay and check if callback has been called
@ -26,61 +26,13 @@ describe('startTimeout Tests', () => {
stop() stop()
}) })
test('should not call immediately', () => { test('startInterval should correctly start and stop interval', () => {
const callback = jest.fn() const callback = jest.fn()
const stop = startTimeout(callback, { ms: 5000 })
// Should not be called immediately const stop = Scheduler.startInterval(callback, 2000, { ms: 5000 })
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 // 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() expect(callback).not.toBeCalled()
// Advance timers to exactly the delay and check if callback has been called // Advance timers to exactly the delay and check if callback has been called
@ -88,43 +40,10 @@ describe('startInterval Tests', () => {
expect(callback).toBeCalledTimes(1) expect(callback).toBeCalledTimes(1)
// Advance timers by the interval and check if callback has been called again // 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) expect(callback).toBeCalledTimes(2)
// Cleanup // Cleanup
stop() 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 { export interface TimeOfDay {
hour: number hour: number
minute?: number minute?: number
seconds?: number seconds?: number
} }
export type TimeUntil = { export type TimeUntil = {
timeOfDay?: TimeOfDay timeOfDay?: TimeOfDay
date?: Date date?: Date
ms?: number ms?: number
} }
type StopCancelFunction = (stopRunning: boolean) => void
type StopFunction = (stopTime?: TimeUntil) => StopCancelFunction | null
export type StopCancelFunction = (stopRunning?: boolean) => void export default class Scheduler {
private static timeUntil(start: TimeUntil) {
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) { if (start.ms) {
return start.ms return start.ms
} }
@ -40,8 +21,9 @@ function timeUntil(start: TimeUntil): number {
let now = new Date() let now = new Date()
// set the target time // set the target time
const targetTime = start.timeOfDay let targetTime = !start.timeOfDay
? new Date( ? start.date
: new Date(
now.getFullYear(), now.getFullYear(),
now.getMonth(), now.getMonth(),
now.getDate(), now.getDate(),
@ -50,7 +32,6 @@ function timeUntil(start: TimeUntil): number {
start.timeOfDay.seconds ?? 0, start.timeOfDay.seconds ?? 0,
0 0
) )
: start.date ?? now
// if the target time has already passed today, set it for tomorrow // if the target time has already passed today, set it for tomorrow
if (start.timeOfDay && now > targetTime) { if (start.timeOfDay && now > targetTime) {
@ -62,129 +43,55 @@ function timeUntil(start: TimeUntil): number {
let delay = targetTime - now let delay = targetTime - now
return delay return delay
} }
/** /**
* Start a timeout * Start a timeout
* @param {Function} timerFunc * @param {Function} timerFunc
* @param {TimeUntil} start * @param {TimeUntil} start
* @return {StopFunction} * @return {StopFunction}
*/ */
export function startTimeout( public static startTimeout(
timerFunc: Function, timerFunc: Function,
start: TimeUntil start: TimeUntil
): StopFunction { ): StopFunction {
let delay = timeUntil(start) let delay = this.timeUntil(start)
// set a timeout to start the interval at the target time // 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 () { timeout = setTimeout(function () {
// Clear the timeout variable // Clear the timeout variable
timeout = null timeout = null
// call the function immediately // call the function immediately
timerFunc() timerFunc()
}, delay) }, delay)
/**
* Cleanup function
* @return {void}
*/
const stopNow = () => { const stopNow = () => {
if (timeout) clearTimeout(timeout) if (timeout) clearTimeout(timeout)
} }
/** // stop() function to stop the interval and timeout
* Stops a timeout // stop(stopInMS) will stop the interval and timeout in stopInMS milliseconds
* @param {TimeUntil} stopTime - optional // stop(stopHour, stopMinute) will stop the interval and timeout at the next stopHour:stopMinute
* @return {StopCancelFunction} const stop = (stopTime?: TimeUntil): 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 => {
if (stopTime === undefined) { if (stopTime === undefined) {
stopNow() stopNow()
return null 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()
}
}
/** const stopTimeout = setTimeout(stopNow, this.timeUntil(stopTime))
* Cancels a delayed stop // stopRunning is a boolean that will either cancel the stop (false), or stop the interval now (true)
* @param {boolean} stopRunning - optional
* @return void
*/
return (stopRunning: boolean = false) => { return (stopRunning: boolean = false) => {
clearTimeout(stopTimeout) clearTimeout(stopTimeout)
if (stopRunning) stopNow() if (stopRunning) stopNow()
@ -193,4 +100,67 @@ export function startInterval(
// return a cleanup function // return a cleanup function
return stop 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, "declaration": true,
"declarationDir": "dist", "declarationDir": "dist",
"target": "es2022" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, "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"], "rootDirs": ["./src"],
"outDir": "./dist" /* Specify an output folder for all emitted files. */, "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. */, "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. */, "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. */
} }
} }