@open-spaced-repetition/binding
v0.6.1
Published
Node.js bindings for the FSRS Optimizer implemented in Rust and compiled to WASI.
Readme
@open-spaced-repetition/binding
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
>=24
Installation
npm install @open-spaced-repetition/binding
pnpm install @open-spaced-repetition/binding
yarn add @open-spaced-repetition/binding
bun add @open-spaced-repetition/bindingOptimize Parameters
import { readFileSync } from 'node:fs'
import {
computeParameters,
convertCsvToFsrsItems,
} from '@open-spaced-repetition/binding'
const csvBuffer = readFileSync('./revlog.csv')
const modelVersion = 'FSRS-7' // 'FSRS-7' (default) or 'FSRS-6'
const items = convertCsvToFsrsItems(csvBuffer, 4, 'Asia/Shanghai', modelVersion)
const parameters = await computeParameters(items, {
enableShortTerm: true,
modelVersion,
numRelearningSteps: 1,
timeout: 500,
progress: (current, total) => {
console.log(`${current}/${total}`)
},
})
console.log(parameters)Pass the same modelVersion to convertCsvToFsrsItems(data, nextDayStartsAt, timezoneOrOffset, modelVersion) and computeParameters. The default is FSRS7, using fractional study days normalized to each study day’s actual duration between local rollover boundaries (including DST). Select FSRS6 explicitly to use whole study days.
With either WASI Preview 1 target (wasm32-wasip1 or wasm32-wasip1-threads), computeParameters and evaluateWithTimeSeriesSplits run synchronously on the current worker and wrap the completed result in a Promise for API compatibility. In browser applications, call these methods from a dedicated Worker so the main thread stays responsive. Progress callbacks are supported; return false to stop the operation.
CSV Timezone Input
The third convertCsvToFsrsItems argument is timezoneOrOffset.
- Pass an IANA timezone name, such as
'Asia/Shanghai'or'America/New_York', when you want the conversion to account for DST for each review at the time it was reviewed. - Pass a fixed UTC offset in minutes, such as
480for UTC+08:00 or-300for UTC-05:00, when the source data should use the same offset for every review timestamp.
const itemsWithTimezone = convertCsvToFsrsItems(csvBuffer, 4, 'Asia/Shanghai')
const itemsWithFixedOffset = convertCsvToFsrsItems(csvBuffer, 4, 480)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 must be a numeric decay in the range of [0.1, 1]: FSRS-4 uses 1, FSRS-4.5/5 uses 0.5, and FSRS-6 uses weights[20]. Parameter arrays are not accepted. This estimator is not usable with FSRS-3 (exponential curve) or FSRS-7 (dual-trace curve).
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-wasiIn 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-corpExample 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/bindingtoserverExternalPackages - In Vite, add
@open-spaced-repetition/bindingtooptimizeDeps.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-originandCross-Origin-Embedder-Policy: require-corpso worker and WASM resources can load without cross-origin isolation issues. - The Vite example above requires
@open-spaced-repetition/binding-wasm32-wasito be installed explicitly. - If you rely on pnpm
supportedArchitecturesinstead of dynamic loading, exclude@open-spaced-repetition/bindingfrom framework optimization when needed so the WASM asset is not dropped. - For scheduler-only workloads, use
ts-fsrs.
Examples
- Training script: packages/binding-test/src/examples/simple.ts
- Next.js training example: examples/nextjs
Documentation
- Repository overview: github.com/open-spaced-repetition/ts-fsrs
- Full scheduler package README: ts-fsrs
- Contribution guide: CONTRIBUTING.md
- Simplified Chinese README: README_CN.md
- Japanese README: README_JA.md
