Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add interval-runner #524

Merged
merged 12 commits into from
Mar 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 159 additions & 0 deletions packages/interval-runner/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
# IntervalRunner

A flexible and powerful utility for running functions at specified intervals with smart execution control.

## Features

- ⏱️ Run functions at specified intervals (in milliseconds)
- 🛑 Smart stopping mechanism - functions can self-terminate by returning `false`
- 🔄 Supports various function types:
- Regular synchronous functions
- Promises / async functions
- Generators
- Async generators
- 🧩 Pass arguments to your interval functions

## Installation

```bash
pnpm install @ts-drp/interval-runner
```

## Usage

### Basic Example

```typescript
import { IntervalRunner } from "@ts-drp/interval-runner";

// Create an interval runner that executes every 5 seconds
const runner = new IntervalRunner({
interval: 5000,
fn: () => {
console.log("Executing task...");
return true; // Return true to continue the interval
}
});

// Start the runner
runner.start();

// Later, stop the runner when needed
runner.stop();
```

### Self-terminating Interval

```typescript
import { IntervalRunner } from "@ts-drp/interval-runner";

let count = 0;
const runner = new IntervalRunner({
interval: 1000,
fn: () => {
console.log(`Execution #${++count}`);

// Automatically stop after 5 executions
return count < 5;
}
});

runner.start();
```

### With Async Functions

```typescript
import { IntervalRunner } from "@ts-drp/interval-runner";

const runner = new IntervalRunner({
interval: 10000,
fn: async () => {
console.log("Starting async operation...");

// Simulate an API call
const result = await fetchSomeData();
console.log("Data fetched:", result);

return true;
}
});

runner.start();
```

### With Generators

```typescript
import { IntervalRunner } from "@ts-drp/interval-runner";

const runner = new IntervalRunner({
interval: 3000,
fn: function* () {
console.log("Starting generator execution");

// You can yield multiple values
yield true; // Continue the interval

// The last yielded value determines whether the interval continues
return false; // Stop the interval
}
});

runner.start();
```

### Passing Arguments

```typescript
import { IntervalRunner } from "@ts-drp/interval-runner";

const runner = new IntervalRunner<[string, number]>({
interval: 2000,
fn: (name, count) => {
console.log(`Hello ${name}, count: ${count}`);
return true;
}
});

// Pass arguments when starting
runner.start(["World", 42]);
```

## API

### Constructor

```typescript
new IntervalRunner(options: IntervalRunnerOptions)
```

#### Options

- `interval`: The interval in milliseconds (must be > 0)
- `fn`: The function to execute at each interval
- `logConfig`: Optional logging configuration

### Methods

#### `start(args?: Args): void`

Starts the interval runner. Optionally accepts arguments to pass to the function.

#### `stop(): void`

Stops the interval runner.

### Properties

#### `interval: number`

The interval in milliseconds.

#### `state: "running" | "stopped"`

The current state of the interval runner.

## License

[MIT](../../LICENSE)
37 changes: 37 additions & 0 deletions packages/interval-runner/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"name": "@ts-drp/interval-runner",
"version": "0.8.5",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/drp-tech/ts-drp.git"
},
"type": "module",
"types": "./dist/src/index.d.ts",
"files": [
"src",
"dist",
"!dist/test",
"!**/*.tsbuildinfo"
],
"exports": {
".": {
"types": "./dist/src/index.d.ts",
"import": "./dist/src/index.js"
}
},
"scripts": {
"build": "tsc -b",
"clean": "rm -rf dist/ node_modules/",
"cli": "tsx ./src/run.ts",
"prebuild": "node -p \"'export const VERSION = ' + JSON.stringify(require('./package.json').version) + ';'\" > src/version.ts",
"prepack": "tsc -b",
"test": "vitest",
"watch": "tsc -b -w"
},
"dependencies": {
"@ts-drp/logger": "0.8.5",
"@ts-drp/types": "0.8.5",
"@ts-drp/utils": "0.8.5"
}
}
125 changes: 125 additions & 0 deletions packages/interval-runner/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { Logger } from "@ts-drp/logger";
import type {
AnyBooleanCallback,
IntervalRunner as IntervalRunnerInterface,
IntervalRunnerOptions,
} from "@ts-drp/types";
import { isAsyncGenerator, isGenerator, isPromise } from "@ts-drp/utils";
import * as crypto from "node:crypto";

export class IntervalRunner<Args extends unknown[] = []> implements IntervalRunnerInterface<Args> {
readonly interval: number;
readonly fn: AnyBooleanCallback<Args>;
readonly id: string;

private _intervalId: NodeJS.Timeout | null = null;
private _state: 0 | 1;
private _logger: Logger;

/**
* @param interval - The interval in milliseconds
* @param fn - The function to run when the interval is up and returns a boolean, true if the interval should continue, false if it should stop
*/
constructor(config: IntervalRunnerOptions) {
if (config.interval <= 0) {
throw new Error("Interval must be greater than 0");
}

this.interval = config.interval;
this.fn = config.fn;
this._logger = new Logger("drp:interval-runner", config.logConfig);
this._state = 0;

this.id =
config.id ??
crypto
.createHash("sha256")
.update(Math.floor(Math.random() * Number.MAX_VALUE).toString())
.digest("hex");
}

private async execute(args?: Args): Promise<boolean> {
const result = args ? this.fn(...args) : this.fn();

if (isAsyncGenerator(result)) {
let lastValue: boolean = false;
for await (const value of result) {
lastValue = value;
}

return lastValue;
}

if (isGenerator(result)) {
let lastValue: boolean = false;
for await (const value of result) {
lastValue = value;
}

return lastValue;
}

if (isPromise(result)) {
return await result;
}

return result;
}

/**
* Start the interval runner
* @param args - The arguments to pass to the function
*/
start(args?: Args): void {
if (this._state === 1) {
throw new Error("Interval runner is already running");
}

this._state = 1;

const scheduleNext = async () => {
if (this._state === 0) {
this._logger.info("Interval runner was already stopped");
return;
}

try {
const result = await this.execute(args);
if (result === false) {
this._logger.info("Interval runner stopped");
this.stop();
return;
}

if (this._state === 1) {
this._intervalId = setTimeout(scheduleNext, this.interval);
}
} catch (error) {
this._logger.error("Error in interval runner:", error);
this.stop();
}
};

// Start the first execution
void scheduleNext();
}

/**
* Stop the interval runner
*/
stop(): void {
if (this._state === 0) {
throw new Error("Interval runner is not running");
}

this._state = 0;
if (this._intervalId) {
clearTimeout(this._intervalId);
this._intervalId = null;
}
}

get state(): "running" | "stopped" {
return this._state === 1 ? "running" : "stopped";
}
}
1 change: 1 addition & 0 deletions packages/interval-runner/src/version.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const VERSION = "0.8.5";
Loading
Loading