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

@open-spaced-repetition/binding

v0.3.1

Published

Node.js bindings for the FSRS Optimizer implemented in Rust and compiled to WASI.

Readme

@open-spaced-repetition/binding

npm version monthly downloads total downloads

Introduction | 简体中文 | はじめに

@open-spaced-repetition/binding provides high-performance FSRS tooling powered by fsrs-rs and napi-rs.

Use it when you need:

  • parameter optimization from review history
  • CSV-to-FSRS item conversion
  • learning and relearning step recommendation
  • WASI-based execution in Node.js or custom browser pipelines

Public beta notice: this package is in public testing and its API may change between releases.

Requirements

  • Node.js >=20

Installation

npm install @open-spaced-repetition/binding
pnpm install @open-spaced-repetition/binding
yarn add @open-spaced-repetition/binding
bun add @open-spaced-repetition/binding

Optimize Parameters

import { readFileSync } from 'node:fs'
import {
  computeParameters,
  convertCsvToFsrsItems,
} from '@open-spaced-repetition/binding'

const timeZoneFormatterCache = new Map<string, Intl.DateTimeFormat>()

const getTimeZoneFormatter = (timeZone: string) => {
  let formatter = timeZoneFormatterCache.get(timeZone)
  if (!formatter) {
    formatter = new Intl.DateTimeFormat('ia', {
      timeZone,
      timeZoneName: 'shortOffset',
    })
    timeZoneFormatterCache.set(timeZone, formatter)
  }
  return formatter
}

const getTimezoneOffset = (timeZone: string, date: Date | number) => {
  const timeZoneName = getTimeZoneFormatter(timeZone)
    .formatToParts(date)
    .find((part) => part.type === 'timeZoneName')?.value

  if (!timeZoneName || timeZoneName === 'GMT' || timeZoneName === 'UTC') {
    return 0
  }

  const [, sign, hours, minutes = '0'] =
    timeZoneName.match(/GMT([+-])(\d{1,2})(?::(\d{2}))?/) ?? []

  if (!sign || !hours) {
    throw new Error(`Unsupported time zone offset: ${timeZoneName}`)
  }

  const totalMinutes = Number(hours) * 60 + Number(minutes)
  return sign === '+' ? totalMinutes : -totalMinutes
}

// Creating Intl.DateTimeFormat repeatedly can be slow, so prefer hoisting or caching formatters.

const csvBuffer = readFileSync('./revlog.csv')
const items = convertCsvToFsrsItems(
  csvBuffer,
  4,
  'Asia/Shanghai',
  (ms, timeZone) => getTimezoneOffset(timeZone, ms)
)

const parameters = await computeParameters(items, {
  enableShortTerm: true,
  numRelearningSteps: 1,
  timeout: 500,
  progress: (current, total) => {
    console.log(`${current}/${total}`)
  },
})

console.log(parameters)

Recommend Learning Steps

import { readFileSync } from 'node:fs'
import { computeOptimalSteps } from '@open-spaced-repetition/binding'

const csvBuffer = readFileSync('./revlog.csv')
const stepStats = computeOptimalSteps(csvBuffer, 0.9, 0.5)

console.log(stepStats.recommendedLearningSteps)
console.log(stepStats.recommendedRelearningSteps)

The third argument can be either:

  • a decay value between 0.1 and 0.8
  • a full FSRS parameter array

Due to technical limitations, at most two learning steps and one relearning step can be recommended. This does not mean you need that many steps, and it does not mean they are always sufficient. It is also possible that no steps are recommended when the data is not suitable.

If you are already using more steps with FSRS, be careful when changing the number of steps. Reducing the number of steps can significantly lower retention, and FSRS may need a long time to adapt to the new step configuration.

Dynamic WASI Initialization

Before using this setup, install the WASI asset package manually:

pnpm add @open-spaced-repetition/binding-wasm32-wasi

In Vite, you can load the WASM asset with ?url and the worker with ?worker, then initialize the optimizer through the dynamic entry:

import { initOptimizer } from '@open-spaced-repetition/binding/dynamic-wasi'
import wasmUrl from '@open-spaced-repetition/binding-wasm32-wasi/fsrs-binding.wasm32-wasi.wasm?url'
import WasiWorker from '@open-spaced-repetition/binding-wasm32-wasi/wasi-worker-browser.mjs?worker'

const binding = await initOptimizer({
  wasm: wasmUrl,
  worker: () => new WasiWorker(),
})

const item = new binding.FSRSBindingItem([
  new binding.FSRSBindingReview(3, 0),
  new binding.FSRSBindingReview(4, 1),
])

const parameters = await binding.computeParameters([item], {
  enableShortTerm: true,
  numRelearningSteps: 1,
})

console.log(parameters)

For browser deployments, enable cross-origin isolation to avoid worker and WASM loading issues. In practice, serve the page with both of these headers:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

Example plugin using dynamic WASI for in-browser training:

WASI Without Dynamic Loading

If you do not need dynamic WASI loading, you can let pnpm install the WASI package automatically by enabling wasm32 in supportedArchitectures:

{
  "pnpm": {
    "supportedArchitectures": {
      "cpu": ["current", "wasm32"]
    }
  }
}

When using this approach, make sure your framework does not prebundle or tree-shake @open-spaced-repetition/binding in a way that drops the WASM asset.

For example:

  • In Next.js, add @open-spaced-repetition/binding to serverExternalPackages
  • In Vite, add @open-spaced-repetition/binding to optimizeDeps.exclude
// vite.config.ts
import { defineConfig } from 'vite'

export default defineConfig({
  optimizeDeps: {
    exclude: ['@open-spaced-repetition/binding'],
  },
})

Notes

  • Edge runtimes are not supported by the default WASI path.
  • For browser usage, prefer bundling the generated WASM and worker assets explicitly.
  • In browsers, set Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp so worker and WASM resources can load without cross-origin isolation issues.
  • The Vite example above requires @open-spaced-repetition/binding-wasm32-wasi to be installed explicitly.
  • If you rely on pnpm supportedArchitectures instead of dynamic loading, exclude @open-spaced-repetition/binding from framework optimization when needed so the WASM asset is not dropped.
  • For scheduler-only workloads, use ts-fsrs.

Examples

Documentation