@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
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/bindingOptimize 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.1and0.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-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
