version 1.0.0

This commit is contained in:
Josh Guyette 2023-07-17 15:54:30 -05:00
parent a9a155be9f
commit 46f9f1400f
8 changed files with 502 additions and 0 deletions

25
.gitignore vendored Normal file
View File

@ -0,0 +1,25 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
.DS_Store
# Dependency directories
node_modules/
# Build folder
dist
# Lock files
yarn.lock
package-lock.json
# Compressed files
*.tgz
*.tar.gz
*.zip

8
.prettierrc.json Normal file
View File

@ -0,0 +1,8 @@
{
"arrowParens": "always",
"endOfLine": "auto",
"jsxSingleQuote": true,
"semi": false,
"singleQuote": true,
"trailingComma": "es5"
}

129
README.md Normal file
View File

@ -0,0 +1,129 @@
# Scheduler
Scheduler is a TypeScript class that provides methods for managing time-based tasks, such as starting and stopping timeouts and intervals at specific times.
## Key Concepts
### TimeOfDay
A `TimeOfDay` object is used to represent a specific time of day. It is an object containing the `hour`, `minute`, and `seconds`.
```typescript
export interface TimeOfDay {
hour: number
minute?: number
seconds?: number
}
```
### TimeUntil
A `TimeUntil` object is used to represent a specific time until a certain event. It can represent time until a certain date, milliseconds from now, or a specific time of day.
```typescript
export type TimeUntil = {
timeOfDay?: TimeOfDay
date?: Date
ms?: number
}
```
## 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).
```typescript
public static 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).
```typescript
public static startInterval(
intervalFunc: Function,
intervalMS: number,
start?: TimeUntil
): StopFunction;
```
## Stop Functions
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.
Here is the type definition of a `StopFunction`:
```typescript
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.
```typescript
type StopCancelFunction = (stopRunning: boolean = false) => void
```
In the `StopCancelFunction`, if the `stopRunning` parameter is `true`, it stops the timeout or interval immediately. If `stopRunning` is `false`, it cancels the scheduled stop.
## Examples
1. Start a timeout that says "Hello, world!" after 10 seconds, then stop it after 5 seconds.
```typescript
const sayHello = () => console.log('Hello, world!')
// Start a timeout that says "Hello, world!" after 10 seconds, and stop it after 5 seconds.
let stopTimeout = Scheduler.startTimeout(sayHello, { ms: 10000 })
stopTimeout({ ms: 5000 })
```
2. Using startTimeout with a specific time of day
```typescript
const goodMorning = () => console.log('Good morning!')
let stopTimeout = Scheduler.startTimeout(goodMorning, {
timeOfDay: { hour: 7, minute: 0 },
})
// Later, if you want to cancel the morning greeting
stopTimeout()
```
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
```typescript
const sayHello = () => console.log('Hello, world!')
let stopInterval = Scheduler.startInterval(sayHello, 1000)
// Later, if you want to stop the interval
stopInterval()
```
4. Using startInterval with a specific time of day
```typescript
const sayHello = () => console.log('Hello, world!')
let stopInterval = Scheduler.startInterval(sayHello, 1000, {
timeOfDay: { hour: 7, minute: 0 },
})
// Later, if you want to stop the interval
stopInterval()
```
In this example, the sayHello function will be called every 1000 milliseconds starting at 7:00 AM. If you want to cancel the morning greeting (for example, the user chose to sleep in), you can call the stopInterval function.

3
jest.config.js Normal file
View File

@ -0,0 +1,3 @@
module.exports = {
moduleFileExtensions: ['js', 'mjs'],
}

18
package.json Normal file
View File

@ -0,0 +1,18 @@
{
"name": "schedule-later",
"version": "1.0.0",
"main": "dist/Scheduler.js",
"author": "Nightness",
"scripts": {
"build": "tsc",
"test": "./node_modules/.bin/jest --verbose",
"watch": "./node_modules/.bin/jest --watch",
"deploy": "npm run build && npm publish"
},
"devDependencies": {
"@types/jest": "^29.5.3",
"@types/node": "^18.11.18",
"jest": "^29.6.1",
"typescript": "^4.6.4"
}
}

49
src/Scheduler.test.ts Normal file
View File

@ -0,0 +1,49 @@
import Scheduler from './Scheduler'
describe('Scheduler', () => {
beforeEach(() => {
jest.useFakeTimers()
})
afterEach(() => {
jest.clearAllTimers()
})
test('startTimeout should correctly start and stop timeout', () => {
const callback = jest.fn()
const stop = Scheduler.startTimeout(callback, { ms: 5000 })
// Advance timers by less than the delay and check if callback has not been called
jest.advanceTimersByTime(2000)
expect(callback).not.toBeCalled()
// Advance timers to exactly the delay and check if callback has been called
jest.advanceTimersByTime(3000)
expect(callback).toBeCalled()
// Cleanup
stop()
})
test('startInterval should correctly start and stop interval', () => {
const callback = jest.fn()
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(2000)
expect(callback).not.toBeCalled()
// Advance timers to exactly the delay and check if callback has been called
jest.advanceTimersByTime(3000)
expect(callback).toBeCalledTimes(1)
// Advance timers by the interval and check if callback has been called again
jest.advanceTimersByTime(2000)
expect(callback).toBeCalledTimes(2)
// Cleanup
stop()
})
})

166
src/Scheduler.ts Normal file
View File

@ -0,0 +1,166 @@
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) {
if (start.ms) {
return start.ms
}
// get the current date
let now = new Date()
// set the target time
let targetTime = !start.timeOfDay
? start.date
: new Date(
now.getFullYear(),
now.getMonth(),
now.getDate(),
start.timeOfDay.hour,
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
}
/**
* Start a timeout
* @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
let timeout: NodeJS.Timeout = null
timeout = setTimeout(function () {
// Clear the timeout variable
timeout = null
// call the function immediately
timerFunc()
}, delay)
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) {
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
}
/**
* 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
}
}

104
tsconfig.json Normal file
View File

@ -0,0 +1,104 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Projects */
// "incremental": true, /* Enable incremental compilation */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es2022" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
"experimentalDecorators": true /* Enable experimental support for TC39 stage 2 draft decorators. */,
"emitDecoratorMetadata": true /* Emit design-type metadata for decorated declarations in source files. */,
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */
// "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
/* Modules */
"module": "nodenext" /* Specify what module code is generated. */,
// "rootDir": "./src" /* Specify the root folder within your source files. */,
// "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
"rootDirs": [
"./prisma",
"./src"
] /* Allow multiple folders to be treated as one when resolving modules. */,
// "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "resolveJsonModule": true, /* Enable importing .json files */
// "noResolve": true, /* Disallow `import`s, `require`s or `<reference>`s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */
"outDir": "./dist" /* Specify an output folder for all emitted files. */,
// "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */,
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
/* Type Checking */
"strict": false /* Enable all strict type-checking options. */,
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */
// "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */
// "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}