Compare commits

...

10 Commits

Author SHA1 Message Date
Josh Guyette 64f7839528 version 1.1.2 2023-07-18 18:10:27 -05:00
Josh Guyette f4a2615e4f more documentation updates 2023-07-18 18:08:42 -05:00
Josh Guyette 6a7a656fa0 added callbackAfterTimeout, comments, and updated documentation 2023-07-18 18:07:34 -05:00
Josh Guyette d9517c319a improved testing 2023-07-18 17:55:45 -05:00
Josh Guyette 48440fccb0 version 1.1.1 2023-07-17 20:37:42 -05:00
Josh Guyette 1f46191482 version 1.1.0 2023-07-17 19:45:31 -05:00
Josh Guyette 617fdebee3 version 1.0.6 2023-07-17 18:56:34 -05:00
Josh Guyette 57239561f7 version 1.0.5 2023-07-17 18:09:45 -05:00
Josh Guyette f6a78b99fe version 1.0.5 2023-07-17 17:57:54 -05:00
Josh Guyette 98e9a6ed68 just "npm publish" directly 2023-07-17 16:25:50 -05:00
6 changed files with 300 additions and 184 deletions

3
.gitignore vendored
View File

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

View File

@ -1,18 +1,25 @@
{ {
"name": "schedule-later", "name": "schedule-later",
"version": "1.0.1", "version": "1.1.2",
"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",
"watch": "./node_modules/.bin/jest --watch", "coverage": "./node_modules/.bin/jest --coverage",
"deploy": "npm run build && npm publish" "prepublish": "tsc && npm run test"
}, },
"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": "^4.6.4" "typescript": "^5.1.6"
} }
} }

View File

@ -1,21 +1,21 @@
import Scheduler from './Scheduler' // @ts-ignore - TS doesn't know this is allowed
import { startTimeout, startInterval, TimeInMS } from './Scheduler'
describe('Scheduler', () => { beforeEach(() => {
beforeEach(() => { jest.useFakeTimers()
jest.useFakeTimers() })
})
afterEach(() => { afterEach(() => {
jest.clearAllTimers() jest.clearAllTimers()
}) })
test('startTimeout should correctly start and stop timeout', () => { describe('startTimeout Tests', () => {
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(2000) jest.advanceTimersByTime(TimeInMS.SECOND * 2)
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,13 +26,61 @@ describe('Scheduler', () => {
stop() stop()
}) })
test('startInterval should correctly start and stop interval', () => { test('should not call immediately', () => {
const callback = jest.fn() 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 // 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() 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
@ -40,10 +88,43 @@ describe('Scheduler', () => {
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(2000) jest.advanceTimersByTime(TimeInMS.SECOND * 2)
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,166 +1,196 @@
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 default class Scheduler { export type StopCancelFunction = (stopRunning?: boolean) => void
private static timeUntil(start: TimeUntil) {
if (start.ms) {
return start.ms
}
// get the current date export type StopFunction = (stopTime?: TimeUntil) => StopCancelFunction | null
let now = new Date()
// set the target time /**
let targetTime = !start.timeOfDay * Used to convert a TimeUntil object into a number of milliseconds
? start.date * @param {TimeUntil} start
: new Date( * @return {number} delay
now.getFullYear(), */
now.getMonth(), function timeUntil(start: TimeUntil): number {
now.getDate(), if (start.ms) {
start.timeOfDay.hour, return start.ms
start.timeOfDay.minute ?? 0,
start.timeOfDay.seconds ?? 0,
0
)
// if the target time has already passed today, set it for tomorrow
if (start.timeOfDay && now > targetTime) {
targetTime.setDate(targetTime.getDate() + 1)
}
// calculate the delay until the next target time
// @ts-ignore - TS doesn't know this is allowed
let delay = targetTime - now
return delay
} }
/** // get the current date
* Start a timeout let now = new Date()
* @param {Function} timerFunc
* @param {TimeUntil} start
* @return {StopFunction}
*/
public static startTimeout(
timerFunc: Function,
start: TimeUntil
): StopFunction {
let delay = this.timeUntil(start)
// set a timeout to start the interval at the target time // set the target time
let timeout: NodeJS.Timeout = null const targetTime = start.timeOfDay
timeout = setTimeout(function () { ? new Date(
// Clear the timeout variable now.getFullYear(),
timeout = null now.getMonth(),
// call the function immediately now.getDate(),
timerFunc() start.timeOfDay.hour,
}, delay) start.timeOfDay.minute ?? 0,
start.timeOfDay.seconds ?? 0,
0
)
: start.date ?? now
const stopNow = () => { // if the target time has already passed today, set it for tomorrow
if (timeout) clearTimeout(timeout) if (start.timeOfDay && now > targetTime) {
} targetTime.setDate(targetTime.getDate() + 1)
// 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
} }
/** // calculate the delay until the next target time
* Start an interval // @ts-ignore - TS doesn't know this is allowed
* @param {Function} intervalFunc let delay = targetTime - now
* @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 return delay
let interval: number = null }
let timeout = setTimeout(function () {
// Clear the timeout variable /**
timeout = null * Start a timeout
// start the interval * @param {Function} timerFunc
interval = setInterval(intervalFunc, intervalMS) * @param {TimeUntil} start
// call the function immediately * @return {StopFunction}
intervalFunc() */
}, delay) export function startTimeout(
timerFunc: Function,
const stopNow = () => { start: TimeUntil
if (timeout) clearTimeout(timeout) ): StopFunction {
if (interval) clearInterval(interval) let delay = timeUntil(start)
}
// set a timeout to start the interval at the target time
// stop() function to stop the interval and timeout let timeout: NodeJS.Timeout | null = null
// stop(stopInMS) will stop the interval and timeout in stopInMS milliseconds timeout = setTimeout(function () {
// stop(stopHour, stopMinute) will stop the interval and timeout at the next stopHour:stopMinute // Clear the timeout variable
const stop = (stopTime?: TimeUntil): StopCancelFunction => { timeout = null
if (stopTime === undefined) {
stopNow() // call the function immediately
return null timerFunc()
} }, delay)
if ((stopTime as any) instanceof Date) { /**
// @ts-ignore - TS doesn't know this is allowed * Cleanup function
const timeFromNow = stopTime - new Date() * @return {void}
const stopTimeout = setTimeout(stopNow, timeFromNow) */
// stopRunning is a boolean that will either cancel the stop (false), or stop the interval now (true) const stopNow = () => {
return (stopRunning: boolean = false) => { if (timeout) clearTimeout(timeout)
clearTimeout(stopTimeout) }
if (stopRunning) stopNow()
} /**
} * Stops a timeout
* @param {TimeUntil} stopTime - optional
const stopTimeout = setTimeout(stopNow, this.timeUntil(stopTime)) * @return {StopCancelFunction}
// stopRunning is a boolean that will either cancel the stop (false), or stop the interval now (true) */
return (stopRunning: boolean = false) => { const stop = (stopTime?: TimeUntil): StopCancelFunction | null => {
clearTimeout(stopTimeout) if (!stopTime) {
if (stopRunning) stopNow() stopNow()
} return null
} }
// return a cleanup function // Set a timeout to stop the timeout at the target time
return stop 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) {
stopNow()
return null
}
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
} }

View File

@ -3,12 +3,11 @@
"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": "nodenext" /* Specify what module code is generated. */, "module": "CommonJS" /* 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": false /* Enable all strict type-checking options. */, "strict": true /* Enable all strict type-checking options. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
} }
} }