npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@enclosurejs/scheduler

v1.1.0

Published

Coordinated work scheduling (micro/frame/idle queues) for Enclosure apps

Readme

@enclosurejs/scheduler — Coordinated work scheduling for Enclosure apps

[!IMPORTANT] Three queues mapped to browser timing primitives: micro (queueMicrotask), frame (requestAnimationFrame), idle (requestIdleCallback). Pause/resume buffers jobs without losing them. Every schedule() call returns a cancellable Disposable handle.

The Problem

Desktop apps juggle rendering, background computation, and housekeeping. Dumping everything into setTimeout gives no priority control. Using requestAnimationFrame directly couples scheduling to individual components. When the app needs to pause all work (e.g. during a modal, resize, or plugin reload), there's no central switch.

@enclosurejs/scheduler provides a single Scheduler that coordinates WHO runs WHEN across three priority lanes, with pause/resume and per-job cancellation. It works for DOM, Canvas, and WebGPU — it coordinates timing, not rendering.

Architecture

                        createScheduler()
                              │
                 ┌────────────┼────────────┐
                 ▼            ▼            ▼
             micro         frame         idle
        queueMicrotask   rAF / 16ms   rIC / 1ms
                 │            │            │
                 └──── flush(pending) ─────┘
                              │
                      stats tracking
                   (scheduled / executed / cancelled)

Fallbacks (Node / environments without rAF/rIC):

  • framesetTimeout(fn, 16) (approximate 60fps)
  • idlesetTimeout(fn, 1) (low-priority background)

Quick Start

import { createScheduler } from '@enclosurejs/scheduler';

const scheduler = createScheduler();

// Schedule on different priority lanes
scheduler.schedule('micro', () => updateState());
scheduler.schedule('frame', () => drawScene());
scheduler.schedule('idle', () => sendAnalytics());

// Cancel before execution
const handle = scheduler.schedule('frame', () => expensiveWork());
handle.dispose(); // cancelled — job will not run

// Pause all scheduling
scheduler.pause();
scheduler.schedule('frame', () => buffered()); // buffered, not dispatched
scheduler.resume(); // buffered jobs now dispatched

// Inspect stats
console.log(scheduler.stats); // { scheduled: 4, executed: 2, cancelled: 1 }

// Cleanup
scheduler.dispose(); // cancels all pending, clears platform timers

As a Module

import { createApp } from '@enclosurejs/core';
import { createSchedulerModule, SchedulerToken } from '@enclosurejs/scheduler';

const app = createApp({
    modules: [createSchedulerModule()],
});

await app.start();

const scheduler = app.context.use(SchedulerToken);
scheduler.schedule('frame', () => render());

How It Works

Queue Dispatch

schedule(queue, job) wraps the job in a PendingJob with a cancelled flag, then dispatches it to the appropriate platform API:

  • microqueueMicrotask() — runs after the current task, before the next render. Lowest latency, highest priority.
  • framerequestAnimationFrame() — runs before the next repaint (~16ms cadence). Ideal for UI updates, canvas draws, WebGPU frame prep.
  • idlerequestIdleCallback() — runs when the browser has no other work. Best for analytics, cache cleanup, non-urgent persistence.

The flush() function executes a job only if it hasn't been cancelled and the scheduler hasn't been disposed. After execution, the cancelled flag is set to prevent double-execution from stale platform callbacks.

Fallback Detection

Platform API availability is checked once at module load via typeof globalThis.requestAnimationFrame === 'function'. In environments without rAF/rIC (Node.js, test runners), fallbacks fire:

  • framesetTimeout(fn, 16) — approximates 60fps timing
  • idlesetTimeout(fn, 1) — low-priority background

Micro queue always uses queueMicrotask() (available in all modern runtimes).

Pause Buffering

When pause() is called, new schedule() calls push jobs into per-queue buffers (pausedMicro, pausedFrame, pausedIdle) instead of dispatching to platform APIs. resume() iterates all buffers, dispatches non-cancelled jobs, then clears the buffers. Jobs cancelled during pause are silently skipped on resume.

Timer Tracking

Frame and idle platform timer IDs are tracked in Set<number>. On dispose(), all pending timers are cancelled via cancelAnimationFrame / cancelIdleCallback / clearTimeout, and all paused jobs are marked cancelled. This prevents orphaned callbacks after teardown.

API

| Export | Kind | Purpose | | ----------------------- | --------- | ---------------------------------------------------- | | createScheduler() | factory | Creates a standalone Scheduler instance | | Scheduler | interface | Schedule, pause/resume, stats, dispose | | SchedulerQueue | type | 'micro' \| 'frame' \| 'idle' | | SchedulerStats | type | { scheduled, executed, cancelled } counters | | SchedulerToken | token | Resolve Scheduler from DI | | createSchedulerModule | factory | Creates a Module that wires scheduler into the app |

Configuration

No configuration — createScheduler() and createSchedulerModule() take no options. The scheduler auto-detects requestAnimationFrame and requestIdleCallback availability and falls back to setTimeout.

Types Exported

| Type | Used by | | ---------------- | ------------------------------------- | | Scheduler | Any code scheduling work | | SchedulerQueue | Queue selection in schedule() calls | | SchedulerStats | Monitoring and debugging |

Safety

Cancellation

  • handle.dispose() is idempotent — double cancel does not double-count.
  • Cancelling after execution is a no-op (the cancelled flag is already set by flush).
  • schedule() on a disposed scheduler returns a no-op handle, never throws.

Dispose

  • dispose() is idempotent — second call does nothing.
  • Cancels all pending platform timers (cancelAnimationFrame, cancelIdleCallback, clearTimeout).
  • Cancels all paused-but-not-yet-dispatched jobs.
  • Already-queued microtasks are guarded by the disposed flag in flush().

Pause/Resume

  • resume() when not paused is a no-op.
  • Jobs cancelled during pause are skipped on resume.
  • Pause does not cancel already-dispatched platform callbacks — they are guarded by the cancelled flag.

Benchmarks

No benchmarks — scheduler overhead is negligible (one closure + one Set.add per job). The real cost is in the user's job callbacks.

Bundle Size

| Output | File | Size | | ------------ | ------------ | ------- | | Runtime (JS) | index.js | 3.66 KB | | Types (DTS) | index.d.ts | 1.35 KB | | Total | | 5.01 KB |

External dependency (@enclosurejs/core) is not bundled — it is a peer dependency.

Quality

| Metric | Value | | --------------------- | ------------------------------------------------------------------ | | Unit tests | 26 (all pass) | | Test files | 2 (scheduler.test.ts, module.test.ts) | | Source files | 3 (scheduler.ts, module.ts, index.ts) | | External dependencies | 0 | | Peer dependencies | @enclosurejs/core | | Coverage thresholds | statements >= 90%, branches >= 85%, functions >= 95%, lines >= 90% |

Quality Layers

Layer 1: STATIC ANALYSIS (every commit)
  tsc --noEmit        strict mode, zero errors
  eslint              ESLint 9 flat config, zero warnings
  prettier --check    formatting

Layer 2: UNIT TESTS (every commit)
  26 tests            micro/frame/idle queues, pause/resume, stats, dispose,
                      cancel idempotency, disposed scheduler no-op, platform fallbacks
  v8 coverage         statements >= 90%, branches >= 85%, functions >= 95%, lines >= 90%

Layer 3: BENCHMARKS
  N/A                 scheduling overhead is one closure + one Set.add per job

Layer 4: PACKAGE HEALTH
  0 external deps     pure TypeScript + @enclosurejs/core
  tsup build          ESM + DTS output, single entrypoint

File Structure

packages/scheduler/
├── src/
│   ├── index.ts              Barrel: all public exports
│   ├── scheduler.ts          createScheduler, Scheduler, SchedulerQueue, SchedulerStats
│   ├── module.ts             createSchedulerModule, SchedulerToken
│   └── __tests__/
│       ├── scheduler.test.ts 22 tests — micro/frame/idle queues, pause/resume, stats, dispose
│       └── module.test.ts     4 tests — module wiring, dispose cleanup
├── package.json
├── tsconfig.json
└── tsup.config.ts

License

MIT