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
easeFactorshould never go below 1.3 (useclampEase()) - Set
dueDateas an absoluteDate, not a relative offset - Increment
reviewCounton every grade - Increment
lapseCountonly onagainduring review state - Use
config.learningStepsandconfig.relearningStepsfor learning phases - The
currentStepfield tracks progress through learning/relearning steps