Files
remanso/src/modules/card/hooks/useSpacedRepetitionCards.ts
Julien Calixte b003a3e008 perf: move PouchDB/IndexedDB operations to a Web Worker
All database reads and writes now run off the main thread via a
dedicated worker, eliminating IndexedDB overhead from the frame budget.

- Create data.worker.ts exposing the Data class via Comlink
- Refactor data.ts to export a Comlink-wrapped proxy and a standalone
  generateId() pure function (workers can't expose sync methods cleanly)
- Update all 10 call sites to import generateId directly instead of
  calling data.generateId()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 11:27:45 +02:00

158 lines
3.7 KiB
TypeScript

// https://npm.io/package/supermemo
import { useAsyncState } from "@vueuse/core"
import { addDays, isAfter } from "date-fns"
import { computed, nextTick, watch } from "vue"
import { data, generateId } from "@/data/data"
import { DataType } from "@/data/DataType.enum"
import { useFile } from "@/hooks/useFile.hook"
import { useLinks } from "@/hooks/useLinks.hook"
import { markdownBuilder } from "@/hooks/useMarkdown.hook"
import { Card } from "@/modules/card/models/Card"
import { RepetitionCard } from "@/modules/card/models/RepetitionCard"
import { useUserRepoStore } from "@/modules/repo/store/userRepo.store"
import { decodeBase64ToUTF8 } from "@/utils/decodeBase64ToUTF8"
const MAX_LEVEL = 8
export interface Repetition {
repetition: RepetitionCard
card: Card
}
export const useSpacedRepetitionCards = () => {
const { toHTML } = markdownBuilder()
const store = useUserRepoStore()
const { listenToClick } = useLinks("flip-card")
const cardFiles = computed(() =>
store.files.filter(
(file) =>
file.path !== undefined &&
file.path.startsWith("_cards") &&
file.path.endsWith(".md")
)
)
const {
state: cards,
isReady,
execute
} = useAsyncState(
async () => {
const cards: Repetition[] = []
for (const cardFile of cardFiles.value) {
if (!cardFile.sha) {
continue
}
const repetition = await data.getOrCreate<
DataType.RepetitionCard,
RepetitionCard
>(generateId(DataType.RepetitionCard, cardFile.path), {
$type: DataType.RepetitionCard,
level: 1,
repeatDate: new Date(),
needsReview: false
})
if (
isAfter(new Date(repetition.repeatDate), new Date()) ||
repetition.level === MAX_LEVEL ||
repetition.needsReview
) {
continue
}
const { getContent } = useFile(cardFile.sha, false)
const content = (await getContent()) ?? ""
const [front, back, references] =
decodeBase64ToUTF8(content).split("___") ?? []
cards.push({
repetition,
card: {
front: toHTML(front),
back: toHTML(back),
references: toHTML(references)
}
})
}
return cards
},
[],
{ immediate: false }
)
const successRepetition = async (cardId: string) => {
const repetition = await data.get<DataType.RepetitionCard, RepetitionCard>(
cardId
)
if (!repetition) {
return
}
await data.update<DataType.RepetitionCard, RepetitionCard>({
...repetition,
needsReview: false,
level: Math.min(repetition.level + 1, MAX_LEVEL),
repeatDate: addDays(new Date(), 2 ** repetition.level)
})
}
const failRepetition = async (cardId: string) => {
const repetition = await data.get<DataType.RepetitionCard, RepetitionCard>(
cardId
)
if (!repetition) {
return
}
const level = 1
await data.update<DataType.RepetitionCard, RepetitionCard>({
...repetition,
level,
needsReview: false,
repeatDate: addDays(new Date(), level)
})
}
const needsReview = async (cardId: string) => {
const repetition = await data.get<DataType.RepetitionCard, RepetitionCard>(
cardId
)
if (!repetition) {
return
}
await data.update<DataType.RepetitionCard, RepetitionCard>({
...repetition,
needsReview: true
})
}
watch(
cards,
() =>
nextTick(() => {
listenToClick()
}),
{ immediate: true }
)
watch(cardFiles, () => execute())
return {
cards,
successRepetition,
failRepetition,
needsReview,
isLoading: !isReady
}
}