aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/client/atoms/study.ts
blob: 115bbcd2362d620d10e8ee862d195203e183e65c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import { atomFamily } from "jotai-family";
import { atomWithSuspenseQuery } from "jotai-tanstack-query";
import { getStartOfStudyDayBoundary } from "../../shared/date";
import { localDeckRepository } from "../db/repositories";
import { buildStudyCards, type StudyCardView } from "../db/study-builder";
import { createSeededRandom, shuffle } from "../utils/random";
import { ensureBootstrap } from "./sync";

export type StudyCard = StudyCardView;

export interface StudyDeck {
	id: string;
	name: string;
}

export interface StudyData {
	deck: StudyDeck;
	cards: StudyCard[];
}

async function loadStudyData(deckId: string): Promise<StudyData | null> {
	const deck = await localDeckRepository.findById(deckId);
	if (!deck || deck.deletedAt !== null) return null;
	const cards = await buildStudyCards(deckId);
	const seed = getStartOfStudyDayBoundary().getTime();
	return {
		deck: { id: deck.id, name: deck.name },
		cards: shuffle(cards, createSeededRandom(seed)),
	};
}

// =====================
// Study Session - Suspense-compatible, IndexedDB-first
// =====================

export const studyDataAtomFamily = atomFamily((deckId: string) =>
	atomWithSuspenseQuery(() => ({
		queryKey: ["decks", deckId, "study"],
		queryFn: async (): Promise<StudyData> => {
			let data = await loadStudyData(deckId);
			if (data) {
				ensureBootstrap();
				return data;
			}
			await ensureBootstrap();
			data = await loadStudyData(deckId);
			if (!data) {
				throw new Error(`Deck not found: ${deckId}`);
			}
			return data;
		},
	})),
);