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,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(() => { beforeEach(() => {
jest.useFakeTimers() jest.useFakeTimers()
}) })
@ -9,13 +9,13 @@ describe('Scheduler', () => {
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,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 { 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) {
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
} }
@ -21,9 +40,8 @@ export default class Scheduler {
let now = new Date() let now = new Date()
// set the target time // set the target time
let targetTime = !start.timeOfDay const targetTime = start.timeOfDay
? start.date ? new Date(
: new Date(
now.getFullYear(), now.getFullYear(),
now.getMonth(), now.getMonth(),
now.getDate(), now.getDate(),
@ -32,6 +50,7 @@ export default class Scheduler {
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) {
@ -51,47 +70,49 @@ export default class Scheduler {
* @param {TimeUntil} start * @param {TimeUntil} start
* @return {StopFunction} * @return {StopFunction}
*/ */
public static startTimeout( export function startTimeout(
timerFunc: Function, timerFunc: Function,
start: TimeUntil start: TimeUntil
): StopFunction { ): StopFunction {
let delay = this.timeUntil(start) let delay = 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 let timeout: NodeJS.Timeout | null = 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 /**
// stop(stopInMS) will stop the interval and timeout in stopInMS milliseconds * Stops a timeout
// stop(stopHour, stopMinute) will stop the interval and timeout at the next stopHour:stopMinute * @param {TimeUntil} stopTime - optional
const stop = (stopTime?: TimeUntil): StopCancelFunction => { * @return {StopCancelFunction}
if (stopTime === undefined) { */
const stop = (stopTime?: TimeUntil): StopCancelFunction | null => {
if (!stopTime) {
stopNow() stopNow()
return null return null
} }
if ((stopTime as any) instanceof Date) { // Set a timeout to stop the timeout at the target time
// @ts-ignore - TS doesn't know this is allowed const stopTimeout = setTimeout(stopNow, timeUntil(stopTime))
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) * Cancels a delayed stop
* @param {boolean} stopRunning - optional
* @return void
*/
return (stopRunning: boolean = false) => { return (stopRunning: boolean = false) => {
clearTimeout(stopTimeout) clearTimeout(stopTimeout)
if (stopRunning) stopNow() if (stopRunning) stopNow()
@ -106,54 +127,64 @@ export default class Scheduler {
* Start an interval * Start an interval
* @param {Function} intervalFunc * @param {Function} intervalFunc
* @param {number} intervalMS * @param {number} intervalMS
* @param {TimeUntil} start * @param {TimeUntil} start - optional
* @param {boolean} callbackAfterTimeout - optional
* @return {StopFunction} * @return {StopFunction}
*/ */
public static startInterval( export function startInterval(
intervalFunc: Function, intervalFunc: Function,
intervalMS: number, intervalMS: number,
start?: TimeUntil start?: TimeUntil,
callbackAfterTimeout: boolean = false
): StopFunction { ): StopFunction {
let delay = this.timeUntil(start) let delay = start ? timeUntil(start) : 0
// set a timeout to start the interval at the target time // set a timeout to start the interval at the target time
let interval: number = null let interval: number | null =
let timeout = setTimeout(function () { delay === 0 ? setInterval(intervalFunc, intervalMS) : null
let timeout: NodeJS.Timeout | null =
delay > 0
? setTimeout(function () {
// Clear the timeout variable // Clear the timeout variable
timeout = null timeout = null
// start the interval // start the interval
interval = setInterval(intervalFunc, intervalMS) 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 = () => { const stopNow = () => {
if (timeout) clearTimeout(timeout) if (timeout) clearTimeout(timeout)
if (interval) clearInterval(interval) if (interval) clearInterval(interval)
} }
// stop() function to stop the interval and timeout /**
// stop(stopInMS) will stop the interval and timeout in stopInMS milliseconds * Stops an interval
// stop(stopHour, stopMinute) will stop the interval and timeout at the next stopHour:stopMinute * @param {TimeUntil} stopTime - optional
const stop = (stopTime?: TimeUntil): StopCancelFunction => { * @return {StopCancelFunction}
*/
const stop = (stopTime?: TimeUntil): StopCancelFunction | null => {
if (stopTime === undefined) { if (stopTime === undefined) {
stopNow() stopNow()
return null return null
} }
if ((stopTime as any) instanceof Date) { const stopTimeout = setTimeout(stopNow, timeUntil(stopTime))
// @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) * Cancels a delayed stop
* @param {boolean} stopRunning - optional
* @return void
*/
return (stopRunning: boolean = false) => { return (stopRunning: boolean = false) => {
clearTimeout(stopTimeout) clearTimeout(stopTimeout)
if (stopRunning) stopNow() if (stopRunning) stopNow()
@ -163,4 +194,3 @@ export default class Scheduler {
// return a cleanup function // return a cleanup function
return stop 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. */
} }
} }