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

@lucamattiazzi/task-eta

v0.1.0

Published

Framework-agnostic run-duration prediction (ETA): kNN over historical samples with a conservative quantile baseline, plus an optional Mastra workflow integration.

Readme

@lucamattiazzi/task-eta

Framework-agnostic run-duration prediction (ETA) for long-running work — AI workflows, extraction pipelines, batch jobs. It learns from your own history: each finished run contributes a (features → duration) sample, and future runs get a live estimate as their features become known.

  • Non-parametric. k-nearest-neighbours over historical samples in scaled feature space — no functional form to fit or re-tune.
  • Conservative fallback. Too few similar runs → a quantile baseline over all history for that workflow; no history at all → a prior you provide.
  • A range, not a false promise. Every estimate is a [p50, p90] bracket (configurable), because duration is inherently uncertain.
  • Two seams, zero lock-in. You provide a DurationStore (where samples live) and, optionally, an EmitEstimate (how you surface the live estimate). The engine itself is pure and dependency-free.
  • Optional Mastra layer at @lucamattiazzi/task-eta/mastra.

Install

npm install @lucamattiazzi/task-eta

@mastra/core is an optional peer dependency — only needed if you import the /mastra subpath.

Quick start

import {
  createDurationTracker,
  createInMemoryStore,
} from '@lucamattiazzi/task-eta'

const store = createInMemoryStore() // swap for your own DurationStore

async function runJob(input: { pages: number }) {
  const tracker = createDurationTracker({
    workflow: 'pdf-extraction',
    store,
    prior: { p50Ms: 120_000, p90Ms: 300_000 }, // cold-start guess
    emit: async ({ estimate }) => {
      console.log(`ETA ~${Math.round(estimate.p50Ms / 1000)}s`)
    },
  })

  // Call whenever you learn something — features accumulate across calls.
  await tracker.emitPrediction({ pages: input.pages }, 'starting')

  await doTheWork(input)

  // Record the finished run so future estimates learn from it.
  await tracker.finish()
}

The one-shot estimator is also exported directly:

import { estimateFromSamples } from '@lucamattiazzi/task-eta'

const { p50Ms, p90Ms, tier } = estimateFromSamples(
  candidates,
  { pages: 42 },
  prior,
)

The seams

DurationStore

Where samples live. Implement it against your database. Candidate loads are scoped by workflow and an optional categorical partition (e.g. file type); recordSample persists a finished run.

interface DurationStore {
  loadCandidates(args: {
    workflow: string
    partition?: Record<string, string>
  }): Promise<DurationSample[]>
  recordSample(args: {
    workflow: string
    workflowVersion?: number
    durationS: number
    features: Record<string, number | null | undefined>
    partition?: Record<string, string>
  }): Promise<void>
}

createInMemoryStore() is a ready reference implementation for tests and prototyping. Keep samples minimal and non-identifying — duration, features, and maybe a categorical partition are all the engine uses.

EmitEstimate (optional)

How you surface the live estimate — a websocket broadcast, an SSE frame, a log line. Omit it entirely if you only want to record samples.

type EmitEstimate = (e: {
  estimate: Estimate
  phase: string
  startedAtMs: number
}) => Promise<void> | void

computeProgress({ startedAtMs, p50Ms, p90Ms, nowMs }) turns an estimate into a capped percent + remaining-time, if you want to drive a progress bar.

Mastra integration

Inside a Mastra workflow, stash the tracker in the run's requestContext so one step can start it and another can finish it:

import {
  setDurationTracker,
  getDurationTracker,
} from '@lucamattiazzi/task-eta/mastra'

// in the step that starts the active phase:
const tracker = createDurationTracker({
  workflow: 'my-workflow',
  store,
  prior,
  emit,
})
setDurationTracker(requestContext, tracker)
await tracker.emitPrediction(featuresKnownNow)

// in the completion step:
await getDurationTracker(requestContext)?.finish()

How the estimate is chosen

  1. Tier-2 (kNN) — if enough candidates share the query's numeric features, take the k nearest (range-scaled Euclidean distance) and return their duration quantiles.
  2. Tier-1 (baseline) — otherwise, quantiles over all candidates for the workflow (+partition).
  3. Prior — if history is too thin, the cold-start prior you passed.

Tune via the config/quantiles options (k, thresholds, the bracket).

License

MIT © lucamattiazzi