#狀態轉換
State 表示卡片目前所處的 FSRS 學習階段。透過 scheduler.newCard() 建立的卡片初始狀態為 New。每次複習時,DefaultScheduler 會結合 FSRS 模型結果與學習步驟策略,決定下一個狀態和到期時間。
圖表按顯式步驟和間隔中介軟體處理後的最終排程間隔分類。評分會影響間隔,但不能單獨決定下一個狀態。
graduatingInterval(預設 1,單位為天)是卡片處於 Review 時的最短間隔。最終間隔短於 graduatingInterval 時進入或維持 Learning/Relearning;達到 graduatingInterval 時進入 Review。步驟被停用或為空、由模型提供間隔時,也適用這項規則。
有一個例外:Again 使用模型間隔覆蓋顯式步驟時,即使最終間隔達到 graduatingInterval,卡片仍維持 Learning/Relearning。流程圖呈現的是一般的間隔對應規則,此例外優先於圖中的規則。
零時長步驟(如 0m,或按秒四捨五入後為零的時長)視為沒有步驟,不支援負數時長。
#四種狀態
| 狀態 | 含義 | 預設佇列 |
|---|---|---|
New | 卡片是新卡,或已透過 scheduler.forget() 重設。 | new |
Learning | 卡片處於初始學習階段,需要再次複習,間隔通常短於 graduatingInterval。 | learning |
Review | 卡片不處於短期學習階段,間隔至少為 graduatingInterval。 | review |
Relearning | 已複習過的卡片正在重新學習,需要再次複習,間隔通常短於 graduatingInterval。 | learning |
#評分如何改變狀態
評分首先決定是否使用顯式步驟間隔。下表假設 enableShortTerm: true,且目前狀態使用的步驟列表非空:New/Learning 使用 learningSteps,Review/Relearning 使用 relearningSteps。如果該列表為空,即使另一列表設定了步驟,排程器也使用模型間隔而不套用畢業下限。
下文的畢業行為假設 maximumInterval >= graduatingInterval,且沒有其他中介軟體將間隔縮短到 graduatingInterval 以下。
| 評分 | New / Learning / Relearning | Review |
|---|---|---|
Again | 回到第一個步驟,learningStep 重設為 0。 | 使用第一個重新學習步驟,learningStep 重設為 0。 |
Hard | 保持目前 learningStep,使用 Hard 步驟間隔。 | 使用模型間隔,但不短於 graduatingInterval;維持 Review。 |
Good | 存在下一步時推進;否則畢業進入 Review,使用模型間隔,但不短於 graduatingInterval。 | 使用模型間隔,但不短於 graduatingInterval;維持 Review。 |
Easy | 跳過顯式步驟,畢業進入 Review,使用模型間隔,但不短於 graduatingInterval。 | 使用模型間隔,但不短於 graduatingInterval;維持 Review。 |
Hard 步驟間隔為前兩個步驟時長的平均值;只有一個步驟時,為該步驟時長的 1.5 倍。步驟為空或 enableShortTerm: false 時,所有評分都使用模型間隔,只適用下方的狀態映射。learningStep 只在卡片留在 Learning/Relearning 時保留下一步驟,卡片進入 Review 時重設為 0。
開啟 allowModelOverride(DefaultScheduler 對 FSRS-7 預設開啟)時,如果模型間隔比目前步驟長,排程器改用模型間隔。Again 的模型間隔不超過 graduatingInterval,使用模型間隔的 Again 總是進入 Learning/Relearning。Hard 和 Good 的間隔達到 graduatingInterval 時,卡片不必走完剩餘步驟即畢業進入 Review。模型間隔仍可能被其他間隔中介軟體調整。
如果 maximumInterval < graduatingInterval,上限可能阻止畢業。例如,設定 graduatingInterval: 2 和 maximumInterval: 1 時,走完學習步驟後的模型間隔被限制為 1 天,因此卡片仍處於 Learning。
間隔確定後,schedulerLearningStepsMiddleware 按下表決定狀態。上文的 Again 模型覆蓋例外優先於此表。下表描述狀態轉換,不代表各評分使用相同的間隔選擇規則:
| 目前狀態 | 最終間隔 < graduatingInterval | 最終間隔 ≥ graduatingInterval |
|---|---|---|
New | Learning | Review |
Learning | Learning | Review |
Review | Relearning | Review |
Relearning | Relearning | Review |
有適用的顯式步驟時,排程器保留該步驟設定的時長,除非 allowModelOverride 選用了較長的模型間隔。達到 graduatingInterval 的步驟仍會讓卡片進入 Review。
DefaultScheduler 自動註冊 schedulerLearningStepsMiddleware,且 enableShortTerm 預設為 true,FSRS-7 也一樣。對於 FSRS-7,此開關控制顯式 learningSteps 和 relearningSteps,不關閉模型內在的短期記憶,也不控制依間隔進行的狀態映射。即使 enableShortTerm: false,模型間隔小於 graduatingInterval 時仍會進入 Learning 或 Relearning。使用 defineScheduler 時,需註冊 .use(schedulerLearningStepsMiddleware) 才會套用此狀態映射;只組合 FSRS-7 模型而未註冊此中介軟體時,即使排程間隔小於一天,state 仍為 Review,scheduleStatus 仍為 review。模型負責記憶狀態和間隔計算,學習狀態轉換由中介軟體負責。如果要讓 FSRS-7 排程後顯示 Learning/Relearning,請掛載 schedulerLearningStepsMiddleware,或自行實作負責狀態轉換的 middleware。
Rating.Manual 不是複習評分。呼叫 review() 時應傳入 Again、Hard、Good 或 Easy。
#執行狀態轉換
下方的可執行範例使用預設 FSRS-7 模型和以下設定:
const scheduler = await DefaultScheduler({
learningSteps: ['1m', '10m'],
relearningSteps: ['10m'],
enableFuzz: false,
allowModelOverride: false,
})graduatingInterval 使用預設值 1 天。卡片初始狀態為 New,learningStep: 0。learningStep 是從零開始的步驟索引,不是複習次數;學習和重新學習都使用同一個欄位記錄步驟進度。
設定 allowModelOverride: false 時,範例產生以下轉換:
| 目前狀態 | 目前 learningStep | 評分 | 轉換後狀態 | 轉換後 learningStep | 間隔 |
|---|---|---|---|---|---|
| New | 0 | Again | Learning | 0 | 1 分鐘 |
| Learning | 0 | Good | Learning | 1 | 10 分鐘 |
| Learning | 1 | Hard | Learning | 1 | 5.5 分鐘 |
| Learning | 1 | Again | Learning | 0 | 1 分鐘 |
| Learning | 1 | Good | Review | 0 | 1 天 |
| Review | 0 | Again | Relearning | 0 | 10 分鐘 |
| Relearning | 0 | Good | Review | 0 | 1 天 |
learningStep: 1 的 Hard、Again 和 Good 行是對同一張卡片的不同評分分支,並非連續複習。重新學習分支從另一張透過 Easy 進入 Review 的卡片開始。
關閉 allowModelOverride 會保留設定的步驟間隔。開啟後,較長的模型間隔可以取代顯式步驟。例如,假設模型間隔為 30 分鐘,原本 10 分鐘的 Good 步驟就會延長到 30 分鐘;如果模型間隔較短,則該步驟維持不變。即使設定 allowModelOverride: false,排程器在步驟完成後或沒有適用步驟時仍會使用模型間隔。
下方完整範例使用公開的 DefaultSchedulerCard、Grade 和 State 型別,輸出間隔以分鐘顯示,保留兩位小數。
dueAt 表示卡片應從何時起複習,now 是實際複習時間,通常應滿足 now >= dueAt。為了讓輸出可以重複驗證且不混淆兩者,本例會在每張卡片的 dueAt 後 30 秒進行複習。
import {
function DefaultScheduler<Version extends DefaultSchedulerVersion = "FSRS-7">(options?: DefaultSchedulerOptions<Version>): Promise<DefaultScheduler<Version>>
type DefaultScheduler<Version extends DefaultSchedulerVersion = "FSRS-7"> = (Version extends "FSRS-7" ? Omit<SchedulerCore<SchedulerCoreEnv<SchedulerEnvFor<FSRS7DefaultSchedulerModel, Chrono<{
readonly time: SRSSchema<{
input: Date;
output: Date;
}>;
readonly config: SRSSchema<{
input: {
readonly fractionalDays?: boolean | undefined;
};
output: {
readonly fractionalDays: boolean;
};
}>;
readonly fields: {
readonly card: SRSSchema<{
input: {
dueAt: Date;
lastReviewAt?: (Date | null) | undefined;
};
output: {
dueAt: Date;
lastReviewAt: Date | null;
};
}>;
readonly revlog: SRSSchema<{
input: {
dueAt: Date;
lastReviewAt?: (Date | null) | undefined;
reviewTime: Date;
};
output: {
dueAt: Date;
lastReviewAt: Date | null;
reviewTime: Date;
};
}>;
};
}>, readonly [...]>>, ModelCore<...>, ChronoCore<...>>, "definition"> : Omit<...> & {
...;
}) & {
...;
}
DefaultScheduler ,
type type DefaultSchedulerCard<Version extends DefaultSchedulerVersion = "FSRS-7"> = Version extends "FSRS-7" ? {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
} : {
cardId: string | number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}
DefaultSchedulerCard ,
type type Grade = 1 | 2 | 3 | 4 Grade ,
type Rating = 0 | 1 | 2 | 3 | 4
const Rating: Readonly<{
readonly Manual: 0;
readonly Again: 1;
readonly Hard: 2;
readonly Good: 3;
readonly Easy: 4;
}>
Rating ,
type State = 0 | 1 | 2 | 3
const State: Readonly<{
readonly New: 0;
readonly Learning: 1;
readonly Review: 2;
readonly Relearning: 3;
}>
State ,
} from 'ts-fsrs'
const const scheduler: DefaultScheduler<"FSRS-7"> scheduler = await DefaultScheduler<"FSRS-7">(options?: DefaultSchedulerOptions<"FSRS-7"> | undefined): Promise<DefaultScheduler<"FSRS-7">> DefaultScheduler ({
DefaultSchedulerOptions<"FSRS-7">.learningSteps?: readonly (`${number}d` | `${number}m` | `${number}h`)[] | undefinedDefaults to ['1m', '10m'].
learningSteps : ['1m', '10m'],
DefaultSchedulerOptions<"FSRS-7">.relearningSteps?: readonly (`${number}d` | `${number}m` | `${number}h`)[] | undefinedDefaults to ['10m'].
relearningSteps : ['10m'],
DefaultSchedulerOptions<Version extends DefaultSchedulerVersion = DefaultSchedulerVersion>.enableFuzz?: boolean | undefined enableFuzz : false,
DefaultSchedulerOptions<Version extends DefaultSchedulerVersion = DefaultSchedulerVersion>.allowModelOverride?: boolean | undefinedLet longer model intervals override learning steps; defaults to true for FSRS-7 only.
allowModelOverride : false,
})
const stateNames: Record<type State = 0 | 1 | 2 | 3 State , string> = {
[const State: Readonly<{
readonly New: 0;
readonly Learning: 1;
readonly Review: 2;
readonly Relearning: 3;
}>
State .New]: 'New',
[const State: Readonly<{
readonly New: 0;
readonly Learning: 1;
readonly Review: 2;
readonly Relearning: 3;
}>
State .Learning]: 'Learning',
[const State: Readonly<{
readonly New: 0;
readonly Learning: 1;
readonly Review: 2;
readonly Relearning: 3;
}>
State .Review]: 'Review',
[const State: Readonly<{
readonly New: 0;
readonly Learning: 1;
readonly Review: 2;
readonly Relearning: 3;
}>
State .Relearning]: 'Relearning',
}
const const reviewAfterDue: (card: DefaultSchedulerCard, grade: Grade) => {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}
reviewAfterDue = (card: {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}
card : type DefaultSchedulerCard<Version extends DefaultSchedulerVersion = "FSRS-7"> = Version extends "FSRS-7" ? {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
} : {
cardId: string | number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}
DefaultSchedulerCard , grade: type Grade = 1 | 2 | 3 | 4 Grade ) => {
const reviewedAt = new Date(card: {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}
card .dueAt.getTime() + 30_000)
return const scheduler: DefaultScheduler<"FSRS-7"> scheduler .review: (input: {
readonly card: Readonly<Omit<Readonly<{
stabilityFast: number;
stability: number;
difficulty: number;
} & {
dueAt: Date;
lastReviewAt: Date | null;
} & {
readonly cardId: string | number;
readonly reps: number;
readonly lapses: number;
readonly scheduledDays: number;
readonly learningStep?: number | undefined;
} & SchedulerCoreFields$1<"review" | "new" | "learning">>, keyof SchedulerCoreFields$1<"review" | "new" | "learning">> & SchedulerCoreFields$1<"review" | "new" | "learning">>;
readonly grade: Grade;
readonly now?: Date | undefined;
}) => {
...;
}
review ({ card, grade, now: reviewedAt }).card: {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}
card
}
const const newCard: {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}
newCard = const scheduler: DefaultScheduler<"FSRS-7"> scheduler .newCard: (options?: ({
readonly cardId?: string | number | undefined;
} & {
readonly now?: Date | undefined;
}) | undefined) => {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}
newCard ({
cardId: 'state-transitions',
now: new Date('2026-01-01T00:00:00.000Z'),
})
const const learning: {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}
learning = const reviewAfterDue: (card: DefaultSchedulerCard, grade: Grade) => {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}
reviewAfterDue (const newCard: {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}
newCard , const Rating: Readonly<{
readonly Manual: 0;
readonly Again: 1;
readonly Hard: 2;
readonly Good: 3;
readonly Easy: 4;
}>
Rating .Again)
const const nextLearningStep: {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}
nextLearningStep = const reviewAfterDue: (card: DefaultSchedulerCard, grade: Grade) => {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}
reviewAfterDue (const learning: {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}
learning , const Rating: Readonly<{
readonly Manual: 0;
readonly Again: 1;
readonly Hard: 2;
readonly Good: 3;
readonly Easy: 4;
}>
Rating .Good)
const const afterLearningSteps: {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}
afterLearningSteps = const reviewAfterDue: (card: DefaultSchedulerCard, grade: Grade) => {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}
reviewAfterDue (const nextLearningStep: {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}
nextLearningStep , const Rating: Readonly<{
readonly Manual: 0;
readonly Again: 1;
readonly Hard: 2;
readonly Good: 3;
readonly Easy: 4;
}>
Rating .Good)
const const repeated: {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}
repeated = const reviewAfterDue: (card: DefaultSchedulerCard, grade: Grade) => {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}
reviewAfterDue (const nextLearningStep: {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}
nextLearningStep , const Rating: Readonly<{
readonly Manual: 0;
readonly Again: 1;
readonly Hard: 2;
readonly Good: 3;
readonly Easy: 4;
}>
Rating .Hard)
const const restarted: {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}
restarted = const reviewAfterDue: (card: DefaultSchedulerCard, grade: Grade) => {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}
reviewAfterDue (const nextLearningStep: {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}
nextLearningStep , const Rating: Readonly<{
readonly Manual: 0;
readonly Again: 1;
readonly Hard: 2;
readonly Good: 3;
readonly Easy: 4;
}>
Rating .Again)
const const reviewed: {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}
reviewed = const reviewAfterDue: (card: DefaultSchedulerCard, grade: Grade) => {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}
reviewAfterDue (const newCard: {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}
newCard , const Rating: Readonly<{
readonly Manual: 0;
readonly Again: 1;
readonly Hard: 2;
readonly Good: 3;
readonly Easy: 4;
}>
Rating .Easy)
const const relearning: {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}
relearning = const reviewAfterDue: (card: DefaultSchedulerCard, grade: Grade) => {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}
reviewAfterDue (const reviewed: {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}
reviewed , const Rating: Readonly<{
readonly Manual: 0;
readonly Again: 1;
readonly Hard: 2;
readonly Good: 3;
readonly Easy: 4;
}>
Rating .Again)
const const afterRelearningSteps: {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}
afterRelearningSteps = const reviewAfterDue: (card: DefaultSchedulerCard, grade: Grade) => {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}
reviewAfterDue (const relearning: {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}
relearning , const Rating: Readonly<{
readonly Manual: 0;
readonly Again: 1;
readonly Hard: 2;
readonly Good: 3;
readonly Easy: 4;
}>
Rating .Good)
for (const [const from: {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
} | {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}
from , rating, const to: {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}
to ] of [
[const newCard: {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}
newCard , 'Again', const learning: {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}
learning ],
[const learning: {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}
learning , 'Good', const nextLearningStep: {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}
nextLearningStep ],
[const nextLearningStep: {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}
nextLearningStep , 'Hard', const repeated: {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}
repeated ],
[const nextLearningStep: {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}
nextLearningStep , 'Again', const restarted: {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}
restarted ],
[const nextLearningStep: {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}
nextLearningStep , 'Good', const afterLearningSteps: {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}
afterLearningSteps ],
[const newCard: {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}
newCard , 'Easy', const reviewed: {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}
reviewed ],
[const reviewed: {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}
reviewed , 'Again', const relearning: {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}
relearning ],
[const relearning: {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}
relearning , 'Good', const afterRelearningSteps: {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}
afterRelearningSteps ],
] as type const = readonly [readonly [{
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}, "Again", {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}], readonly [{
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}, "Good", {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}], ... 5 more ..., readonly [...]]
const ) {
console.log(
`${stateNames[const from: {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
} | {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}
from .state]} --${rating}--> ${stateNames[const to: {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}
to .state]}; ` +
`learningStep: ${const from: {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
} | {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}
from .learningStep} -> ${const to: {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}
to .learningStep}; ` +
`interval: ${(const to: {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}
to .scheduledDays * 1440).toFixed(2)}m`
)
}
#Card、revlog 與記憶狀態
- 輸入
card表示複習前的狀態;review()不會修改它。 result.card包含下一個state、下一組 FSRS 記憶狀態(FSRS-7 的stability、stabilityFast與difficulty)以及新的dueAt。result.revlog會連同評分和複習時間記錄先前的狀態與記憶狀態;rollback 也使用這份快照還原卡片。scheduleStatus決定卡片進入哪個排程佇列。預設情況下,New對應new,兩個學習狀態對應learning,Review對應review;middleware 可以加入suspended等佇列狀態,而不會改變四種 FSRS 狀態。
用 card.state 解釋學習階段,用 card.scheduleStatus 篩選排程佇列。