aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/client/db/index.ts
blob: 4c381d2ff70cbea0b2f3021234cc8302d673f593 (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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
import Dexie, { type EntityTable } from "dexie";

/**
 * Card states for FSRS algorithm
 */
export const CardState = {
	New: 0,
	Learning: 1,
	Review: 2,
	Relearning: 3,
} as const;

export type CardStateType = (typeof CardState)[keyof typeof CardState];

/**
 * Rating values for reviews
 */
export const Rating = {
	Again: 1,
	Hard: 2,
	Good: 3,
	Easy: 4,
} as const;

export type RatingType = (typeof Rating)[keyof typeof Rating];

/**
 * Local deck stored in IndexedDB
 * Includes _synced flag for offline sync tracking
 */
export interface LocalDeck {
	id: string;
	userId: string;
	name: string;
	description: string | null;
	newCardsPerDay: number;
	createdAt: Date;
	updatedAt: Date;
	deletedAt: Date | null;
	syncVersion: number;
	_synced: boolean;
}

/**
 * Local card stored in IndexedDB
 * Includes _synced flag for offline sync tracking
 */
export interface LocalCard {
	id: string;
	deckId: string;
	front: string;
	back: string;

	// FSRS fields
	state: CardStateType;
	due: Date;
	stability: number;
	difficulty: number;
	elapsedDays: number;
	scheduledDays: number;
	reps: number;
	lapses: number;
	lastReview: Date | null;

	createdAt: Date;
	updatedAt: Date;
	deletedAt: Date | null;
	syncVersion: number;
	_synced: boolean;
}

/**
 * Local review log stored in IndexedDB
 * Includes _synced flag for offline sync tracking
 * ReviewLog is append-only (no conflicts possible)
 */
export interface LocalReviewLog {
	id: string;
	cardId: string;
	userId: string;
	rating: RatingType;
	state: CardStateType;
	scheduledDays: number;
	elapsedDays: number;
	reviewedAt: Date;
	durationMs: number | null;
	syncVersion: number;
	_synced: boolean;
}

/**
 * Kioku local database using Dexie (IndexedDB wrapper)
 *
 * This database stores decks, cards, and review logs locally for offline support.
 * Each entity has a _synced flag to track whether it has been synchronized with the server.
 */
export class KiokuDatabase extends Dexie {
	decks!: EntityTable<LocalDeck, "id">;
	cards!: EntityTable<LocalCard, "id">;
	reviewLogs!: EntityTable<LocalReviewLog, "id">;

	constructor() {
		super("kioku");

		this.version(1).stores({
			// Primary key is 'id', indexed fields follow
			// userId: for filtering by user
			// updatedAt: for sync ordering
			// Note: _synced is not indexed (boolean fields can't be indexed in IndexedDB)
			// Use .filter() to find unsynced items
			decks: "id, userId, updatedAt",

			// deckId: for filtering cards by deck
			// due: for finding due cards
			// state: for filtering by card state
			cards: "id, deckId, updatedAt, due, state",

			// cardId: for finding reviews for a card
			// userId: for filtering by user
			// reviewedAt: for ordering reviews
			reviewLogs: "id, cardId, userId, reviewedAt",
		});
	}
}

/**
 * Singleton instance of the Kioku database
 */
export const db = new KiokuDatabase();