# Cordis Timer Plugin Scheduling and Delayed Execution: A Complete Guide to TimerService

> Master Cordis timer plugin scheduling and delayed execution with TimerService. Inject lifecycle-aware methods like timeout and interval for leak free timer management.

- Repository: [Cordiverse/cordis](https://github.com/cordiverse/cordis)
- Tags: how-to-guide
- Published: 2026-08-25

---

**The Cordis timer plugin implements `TimerService` to inject lifecycle-aware scheduling methods (`timeout`, `interval`, `throttle`, `debounce`) into every Context, automatically disposing timers when their owning context shuts down to eliminate memory leaks.**

The Cordis framework provides enterprise-grade scheduling primitives through its official timer plugin. This plugin implements the `TimerService` class, extending Cordis' core `Service` abstraction to offer safe, context-bound delayed execution utilities. When you load the plugin via `await ctx.plugin(Timer)`, every `Context` instance gains a `timer` property exposing methods that guarantee scheduled callbacks cannot outlive their parent context.

## How TimerService Extends Cordis Contexts

The timer functionality centers on the **`TimerService`** class defined in [`packages/timer/src/index.ts`](https://github.com/cordiverse/cordis/blob/main/packages/timer/src/index.ts). This service extends the base `Service` class from [`packages/core/src/service.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/service.ts) and registers a mixin during construction. Upon plugin load, every `Context` instance receives a `timer` property granting access to scheduling methods that integrate with Cordis' effect system.

All timer methods rely on **`Context.effect`** to bind the lifecycle of the underlying `setTimeout` or `setInterval` to the owning context. When the context disposes—for example, when a plugin unloads or the application shuts down—the effect's cleanup function fires automatically, clearing pending timers and preventing further callbacks. This architecture guarantees that asynchronous operations respect the hierarchical disposal model of the Cordis framework.

## Core Scheduling Methods

The Cordis timer plugin exposes four primary scheduling utilities, each handling different execution patterns.

### ctx.timeout(callback, delay) and Promise-Based Delays

The **`ctx.timeout()`** method schedules one-off delayed execution with two distinct signatures:

- **Callback style**: Pass a function as the first argument to execute it after the delay. Returns a disposal callback that cancels the timer.
- **Promise style**: Omit the callback to receive a promise that resolves after the specified milliseconds or rejects if the context disposes before completion.

Both variants use `Context.effect` to register automatic cleanup, ensuring the timer clears immediately upon context disposal.

### ctx.interval(callback, delay) and Async Iterators

The **`ctx.interval()`** method supports both traditional polling and modern async iteration:

- **Callback style**: Repeatedly invokes the provided function every *delay* milliseconds using `setInterval`, wrapped in `Context.effect` for automatic cleanup.
- **Async iterator style**: Call without a callback to receive an async iterator that yields on each tick. The iterator automatically disposes when you break, return, or throw, or when the context shuts down.

This dual API allows flexible integration with both callback-heavy legacy code and modern `for await...of` loops.

### ctx.throttle(callback, delay, noTrailing?)

The **`ctx.throttle()`** method guarantees a callback executes at most once per specified delay period. It tracks the `lastCall` timestamp and computes `remaining = delay - now + lastCall` on each invocation:

- If `remaining ≤ 0`, the callback runs immediately.
- If `remaining > 0`, the method schedules a trailing execution via `setTimeout` unless `noTrailing` is set to true.

This method uses the private **`_schedule`** helper (lines 103-115 in [`packages/timer/src/index.ts`](https://github.com/cordiverse/cordis/blob/main/packages/timer/src/index.ts)) to create a wrapper function that stores timer references and exposes a manual `dispose()` method.

### ctx.debounce(callback, delay)

The **`ctx.debounce()`** method delays execution until *delay* milliseconds have passed without another call. On each invocation, it clears any pending timeout and schedules a fresh one. This ensures the callback only fires after the specified quiet period, making it ideal for handling rapid-fire events like user input.

Like throttle, debounce builds on the `_schedule` helper to ensure proper disposal integration and manual cancellation support.

## Automatic Lifecycle Management with Context.effect

All timer-related methods in Cordis rely on **`Context.effect`** from the core service architecture. When you schedule a timer, the plugin creates an effect that registers a cleanup function. This cleanup clears the underlying `setTimeout` or `setInterval` handle immediately when:

- The context is manually disposed
- A parent context in the hierarchy shuts down
- The application performs a graceful exit

The private `_schedule` helper (lines 103-115) encapsulates this pattern by creating a wrapper function that:
1. Stores a reference to the current timer handle
2. Registers a disposal effect that marks the wrapper as disposed and clears the timer
3. Returns a callable that manages timer state and scheduling

This mechanism ensures that even if you forget to manually cancel a throttle or debounce wrapper, the underlying resources release automatically when their context dies.

## Delayed Execution Implementation Details

Understanding the internal mechanics helps optimize usage:

| Method | Delay Logic |
|--------|-------------|
| **timeout** | Direct `setTimeout` that resolves a promise or invokes the user callback; rejects promise if context disposes early |
| **interval** | `setInterval` for callbacks; async iterator uses `setInterval` to resolve a `PromiseWithResolvers` on each tick |
| **throttle** | Computes remaining time against `lastCall` timestamp; runs immediately if expired, otherwise schedules trailing `setTimeout` |
| **debounce** | Clears existing timeout on every call and creates fresh `setTimeout(callback, delay)` to ensure single execution after quiet period |

The deprecated aliases `ctx.setTimeout` and `ctx.setInterval` still exist for backward compatibility but simply forward to `ctx.timeout()` and `ctx.interval()` respectively.

## Practical Code Examples

The following TypeScript examples demonstrate common patterns using the Cordis timer plugin. Each assumes you have installed the plugin via `await ctx.plugin(Timer)`.

### One-Off Timeout with Callback

```typescript
const dispose = ctx.timeout(() => console.log('Fired after 2 seconds'), 2000);
// Cancel before it fires
dispose();

```

### Promise-Based Timeout

```typescript
await ctx.timeout(3000); // pauses for 3 seconds
console.log('3 seconds later');

```

### Repeating Interval with Cleanup

```typescript
const stop = ctx.interval(() => console.log('Tick'), 1000);
// Stop after 5 seconds
setTimeout(stop, 5000);

```

### Interval as Async Iterator

```typescript
(async () => {
  const it = ctx.interval(1500);
  let count = 0;
  for await (const _ of it) {
    console.log('Iterated tick');
    count++;
    if (count >= 3) break; // Automatically disposes iterator
  }
})();

```

### Throttled Function Execution

```typescript
const throttled = ctx.throttle((msg: string) => console.log('Throttled:', msg), 1000);
throttled('first');   // immediate execution
throttled('second');  // ignored (within 1 second)
// After 1.1 seconds:
throttled('third');   // executes after delay

```

### Debounced Input Handler

```typescript
const debounced = ctx.debounce((msg: string) => console.log('Debounced:', msg), 800);
debounced('a');
debounced('b'); // resets timer, 'a' never fires
// After 900ms of silence: logs 'b'

```

### Manual Disposal of Throttle/Debounce

```typescript
throttled.dispose();
debounced.dispose();

```

## Summary

- **TimerService** extends Cordis' `Service` class and mixes scheduling capabilities into every `Context` instance through the `timer` property.
- **Four core methods** handle different timing needs: `timeout()` for one-off delays, `interval()` for recurring execution, `throttle()` for rate limiting, and `debounce()` for quiet-period execution.
- **Context.effect integration** ensures automatic cleanup of all timers when their owning context disposes, preventing memory leaks and zombie callbacks.
- **Implementation location**: Core logic resides in [`packages/timer/src/index.ts`](https://github.com/cordiverse/cordis/blob/main/packages/timer/src/index.ts), with tests in [`packages/timer/tests/index.spec.ts`](https://github.com/cordiverse/cordis/blob/main/packages/timer/tests/index.spec.ts).
- **Backward compatibility**: Deprecated `ctx.setTimeout` and `ctx.setInterval` aliases forward to the modern methods.

## Frequently Asked Questions

### How does Cordis prevent timer memory leaks?

Cordis prevents memory leaks by binding every timer to its owning `Context` through the `Context.effect` API. When a context disposes—whether through plugin unloading or application shutdown—the effect system automatically clears all pending `setTimeout` and `setInterval` handles. This guarantees that timers cannot outlive the components that created them, eliminating the need for manual cleanup in most scenarios.

### What is the difference between throttle and debounce in Cordis?

**Throttle** guarantees a callback executes at most once per specified delay period, running immediately on the first call and scheduling subsequent calls only after the delay expires. **Debounce** delays execution until the specified delay has passed without any new calls, resetting the timer on every invocation. Use throttle for rate-limiting frequent events like scrolling; use debounce for handling final input states after typing stops.

### Can I use intervals as async generators in Cordis?

Yes. When you call `ctx.interval(delay)` without a callback argument, it returns an async iterator that yields on each tick. You can use this with `for await...of` loops, and the iterator automatically handles disposal when you break from the loop, return from the async function, throw an error, or when the context shuts down. This provides a modern, cancellation-safe alternative to traditional `setInterval` callbacks.

### How do I manually cancel a scheduled timer?

Methods that return disposable resources provide explicit cleanup functions. `ctx.timeout()` returns a disposal callback when used with callbacks. `ctx.throttle()` and `ctx.debounce()` return wrapper functions that expose a `dispose()` method. Calling these functions immediately clears the underlying timer and marks the wrapper as disposed, preventing any pending executions from firing.