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

ts-fsrs-optimizer

v0.1.2

Published

FSRS parameter optimizer in pure TypeScript. No native module, no WebAssembly, no server — it runs wherever JavaScript does, including React Native.

Readme

ts-fsrs-optimizer

CI npm licence no dependencies

Fits the 21 FSRS parameters to one person's review history, in pure TypeScript.

No native module, no WebAssembly, no server. It runs wherever JavaScript runs — Node, browsers, Cloudflare Workers, and React Native, where the existing optimizers cannot go.

npm install ts-fsrs-optimizer

Repository · Changelog · Porting notes

Why this exists

FSRS ships a scheduler in many languages, but only one optimizer: the Rust implementation in fsrs-rs, distributed as @open-spaced-repetition/binding in napi and WebAssembly flavours. Both are excellent and both are unavailable in some places:

  • React Native cannot load napi modules, and Hermes has no WebAssembly object at all — measured on RN 0.86.3, where typeof WebAssembly is "undefined".
  • Anywhere the review log must not leave the device, a server-side optimizer is not an option regardless of how well it runs.

This package is a line-by-line port of the optimizer half of fsrs-rs, kept close enough to the original that the two can be read side by side.

It does not schedule reviews. Feed the parameters it returns to ts-fsrs or any other FSRS scheduler. There is no dependency between the two packages.

Usage

import { computeParameters } from "ts-fsrs-optimizer";

const parameters = computeParameters({ trainSet });
// → 21 numbers, ready to hand to a scheduler

Shaping the input

trainSet is not a list of cards. It is a list of prediction problems: each item is a prefix of one card's history, and the last review in it is the one the model is asked to predict from everything before it.

So a card reviewed four times contributes three items:

import type { FsrsItem, FsrsReview } from "ts-fsrs-optimizer";

// One card. deltaT is days since the previous review; 0 means the same day.
const reviews: FsrsReview[] = [
  { rating: 3, deltaT: 0 },
  { rating: 3, deltaT: 2 },
  { rating: 1, deltaT: 5 },
  { rating: 4, deltaT: 1 },
];

const items: FsrsItem[] = [];

// Start at the first review that reached a later day: see the third rule below.
const firstLongTerm = reviews.findIndex((review) => review.deltaT > 0);

for (let length = firstLongTerm + 1; length <= reviews.length; length += 1) {
  items.push({ reviews: reviews.slice(0, length) });
}

Three rules that are easy to get wrong:

  • Order the whole set by review time. Recent reviews are weighted far more heavily than old ones, and nothing in the library can detect the wrong order.
  • Ratings are 1–4 (again, hard, good, easy). Anything else is rejected.
  • Every item needs one review with deltaT > 0. A card answered twice on the same day says nothing about how long memory lasts, so such an item is rejected rather than trained on. This is not a corner case: any app that repeats a card within the day produces those prefixes constantly. Drop them — nothing is lost, because those answers stay in the history of the first prefix that does reach the next day.

Grouping by card

Passing cardIds alongside trainSet lets the optimizer score a whole card in one pass instead of replaying each prefix separately. Same answer, less work:

computeParameters({ trainSet: items, cardIds });

Options

| Option | Default | Meaning | | -------------------- | ------- | ---------------------------------------------------------------------- | | cardIds | null | Card id per item; enables windowed batching | | enableShortTerm | true | Fit same-day parameters. false also freezes initial stability | | numRelearningSteps | 1 | Relearning steps, which caps w17 · w18 | | trainingConfig | — | numEpochs, batchSize, seed, learningRate, maxSeqLen, gamma | | skipOutlierFilter | false | Keep anomalous intervals instead of dropping them |

When it declines to train

Too little history is a result, not an error:

  • fewer than 8 items — the defaults come back untouched;
  • fewer than 64, or nothing beyond the initialisation set — only initial stability is fitted.

InvalidInput is thrown only for input that cannot mean anything: an empty review list, a rating outside 1–4, an item with no review past the first day, cardIds that do not line up, or a training config that cannot run.

How much to trust it

A numerical port is easy to get subtly, invisibly wrong: gradient descent that converges to the wrong place looks exactly like gradient descent that works.

  • Golden values from fsrs-rs. Initial stability, the LR schedule, the parameter clipper and two Adam steps are checked against the reference implementation's own test expectations, bit for bit where Rust asserts equality.
  • Finite differences. Every one of the 21 analytic gradients is checked against a numerical derivative of the loss it claims to differentiate. A mistranscribed derivative fails without needing any reference number.
  • Mutation-checked. Those tests were themselves verified by deliberately flipping gradient signs to confirm they fail. Two mutations survived the upstream fixture — the port ships an extra batch that catches them (see PORTING.md).

Known divergences from fsrs-rs

Results are not bit-identical to fsrs-rs, by one deliberate choice: batch shuffling uses a small PRNG rather than a port of rand's ChaCha12. The same seed and input give the same parameters here; they will not equal the Rust output. Everything else is transcribed faithfully.

PORTING.md records every divergence, the assumptions that have not been proven, and the f32/f64 trap that dominates a port like this.

Performance

Node on a desktop, default config, synthetic data:

| Cards | Prefix items | Time | | ----- | ------------ | ------ | | 300 | 2 100 | 47 ms | | 1 000 | 9 000 | 146 ms | | 3 000 | 33 000 | 639 ms |

Roughly linear. Not a device measurement.

Contributing

Bug reports and pull requests are welcome. Because this is a port, there are a few rules that are not obvious — chiefly that the code deliberately mirrors the Rust it came from, and that f32 rounding is load-bearing. CONTRIBUTING.md covers them, and PORTING.md explains why.

Changes are recorded in CHANGELOG.md.

Licence

BSD-3-Clause, inherited from fsrs-rs. Copyright (c) 2023 Open Spaced Repetition and (c) 2026 ihv-forge. See LICENSE.

Thanks to Jarrett Ye and the Open Spaced Repetition contributors, whose work this is a translation of.

This project is not affiliated with, endorsed by, or supported by Open Spaced Repetition. Please report problems here rather than to them.