My App
LoopKitGuides

Custom SRS Algorithm

Implement a custom spaced repetition algorithm

Overview

LoopKit's SM-2 algorithm is the default, but you can replace it with any spaced repetition algorithm by implementing the SRSAlgorithm interface.

The Interface

import type { SRSAlgorithm, SRSState } from '@loopkit/nestjs';
import type { Grade } from '@loopkit/nestjs';
import type { DeckConfig } from '@loopkit/nestjs';

interface SRSAlgorithm {
    calculateNextState(
        card: SRSState,
        grade: Grade,
        config: DeckConfig,
        now?: Date,
    ): SRSState;

    getInitialState(config: DeckConfig, now?: Date): SRSState;
}

SRSState

The state object your algorithm must produce:

interface SRSState {
    state: 'new' | 'learning' | 'review' | 'relearning';
    easeFactor: number;    //1.3
    interval: number;      // Days until next review
    dueDate: Date;         // Absolute next review date
    currentStep: number;   // Learning step index
    lapseCount: number;    // Total lapse count
    reviewCount: number;   // Total review count
}

Example: Simple Fixed-Interval Algorithm

import type { SRSAlgorithm, SRSState } from '@loopkit/nestjs';
import type { Grade } from '@loopkit/nestjs';
import type { DeckConfig } from '@loopkit/nestjs';

export class FixedIntervalAlgorithm implements SRSAlgorithm {
    private intervals = {
        again: 0.01,  // ~15 minutes
        hard: 1,      // 1 day
        good: 3,      // 3 days
        easy: 7,      // 7 days
    };

    calculateNextState(
        card: SRSState,
        grade: Grade,
        config: DeckConfig,
        now: Date = new Date(),
    ): SRSState {
        const interval = this.intervals[grade];
        const dueDate = new Date(now.getTime() + interval * 86400000);

        return {
            state: grade === 'again' ? 'relearning' : 'review',
            easeFactor: card.easeFactor,
            interval,
            dueDate,
            currentStep: 0,
            lapseCount: card.lapseCount + (grade === 'again' ? 1 : 0),
            reviewCount: card.reviewCount + 1,
        };
    }

    getInitialState(config: DeckConfig, now: Date = new Date()): SRSState {
        return {
            state: 'new',
            easeFactor: config.startingEaseFactor,
            interval: 0,
            dueDate: now,
            currentStep: 0,
            lapseCount: 0,
            reviewCount: 0,
        };
    }
}

Register Your Algorithm

import { LoopKitModule, PrismaLoopKitAdapter } from '@loopkit/nestjs';
import { FixedIntervalAlgorithm } from './fixed-interval-algorithm';

@Module({
    imports: [
        LoopKitModule.register({
            storage: new PrismaLoopKitAdapter(prismaClient),
            algorithm: new FixedIntervalAlgorithm(),
        }),
    ],
})
export class AppModule {}

Access via Injection Token

You can also inject the algorithm for direct use:

import { Inject, Injectable } from '@nestjs/common';
import { SRS_ALGORITHM } from '@loopkit/nestjs';
import type { SRSAlgorithm } from '@loopkit/nestjs';

@Injectable()
export class AnalyticsService {
    constructor(
        @Inject(SRS_ALGORITHM) private readonly algorithm: SRSAlgorithm,
    ) {}
}

Guidelines

  • Always respect config.maxInterval — clamp your intervals
  • The easeFactor should never go below 1.3 (use clampEase())
  • Set dueDate as an absolute Date, not a relative offset
  • Increment reviewCount on every grade
  • Increment lapseCount only on again during review state
  • Use config.learningSteps and config.relearningSteps for learning phases
  • The currentStep field tracks progress through learning/relearning steps

On this page