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

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. This service extends the base Service class from 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) 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

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

Promise-Based Timeout

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

Repeating Interval with Cleanup

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

Interval as Async Iterator

(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

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

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

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, with tests in 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →