@vue-skuilder/db 0.1.11 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/core/index.d.mts +7 -6
  2. package/dist/core/index.d.ts +7 -6
  3. package/dist/core/index.js +146 -37
  4. package/dist/core/index.js.map +1 -1
  5. package/dist/core/index.mjs +146 -37
  6. package/dist/core/index.mjs.map +1 -1
  7. package/dist/{dataLayerProvider-VieuAAkV.d.mts → dataLayerProvider-BiP3kWix.d.mts} +1 -1
  8. package/dist/{dataLayerProvider-juuqUHOP.d.ts → dataLayerProvider-DSdeyRT3.d.ts} +1 -1
  9. package/dist/impl/couch/index.d.mts +3 -3
  10. package/dist/impl/couch/index.d.ts +3 -3
  11. package/dist/impl/couch/index.js +146 -37
  12. package/dist/impl/couch/index.js.map +1 -1
  13. package/dist/impl/couch/index.mjs +146 -37
  14. package/dist/impl/couch/index.mjs.map +1 -1
  15. package/dist/impl/static/index.d.mts +14 -6
  16. package/dist/impl/static/index.d.ts +14 -6
  17. package/dist/impl/static/index.js +147 -39
  18. package/dist/impl/static/index.js.map +1 -1
  19. package/dist/impl/static/index.mjs +147 -39
  20. package/dist/impl/static/index.mjs.map +1 -1
  21. package/dist/{index-DZyxHCcf.d.mts → index-Bmll7Xse.d.mts} +1 -1
  22. package/dist/{index-CWY6yhkV.d.ts → index-CD8BZz2k.d.ts} +1 -1
  23. package/dist/index.d.mts +119 -24
  24. package/dist/index.d.ts +119 -24
  25. package/dist/index.js +785 -261
  26. package/dist/index.js.map +1 -1
  27. package/dist/index.mjs +789 -265
  28. package/dist/index.mjs.map +1 -1
  29. package/dist/{types-DtoI27Xh.d.ts → types-CewsN87z.d.ts} +1 -1
  30. package/dist/{types-Che4wTwA.d.mts → types-Dbp5DaRR.d.mts} +1 -1
  31. package/dist/{types-legacy-B8ahaCbj.d.mts → types-legacy-6ettoclI.d.mts} +13 -2
  32. package/dist/{types-legacy-B8ahaCbj.d.ts → types-legacy-6ettoclI.d.ts} +13 -2
  33. package/dist/{userDB-DJ8HMw83.d.mts → userDB-C4yyAnpp.d.mts} +3 -3
  34. package/dist/{userDB-B7zTQ123.d.ts → userDB-CD6s6ZCp.d.ts} +3 -3
  35. package/dist/util/packer/index.d.mts +3 -3
  36. package/dist/util/packer/index.d.ts +3 -3
  37. package/package.json +3 -3
  38. package/src/core/navigators/hardcodedOrder.ts +64 -0
  39. package/src/core/navigators/index.ts +1 -0
  40. package/src/core/types/contentNavigationStrategy.ts +2 -1
  41. package/src/core/types/types-legacy.ts +2 -2
  42. package/src/impl/common/BaseUserDB.ts +15 -11
  43. package/src/impl/couch/courseDB.ts +74 -27
  44. package/src/impl/couch/updateQueue.ts +8 -3
  45. package/src/impl/static/StaticDataLayerProvider.ts +57 -17
  46. package/src/impl/static/courseDB.ts +17 -12
  47. package/src/impl/static/coursesDB.ts +10 -6
  48. package/src/study/ItemQueue.ts +58 -0
  49. package/src/study/SessionController.ts +132 -178
  50. package/src/study/services/CardHydrationService.ts +153 -0
  51. package/src/study/services/EloService.ts +85 -0
  52. package/src/study/services/ResponseProcessor.ts +224 -0
  53. package/src/study/services/SrsService.ts +44 -0
@@ -0,0 +1,85 @@
1
+ import { adjustCourseScores, toCourseElo } from '@vue-skuilder/common';
2
+ import { DataLayerProvider, UserDBInterface, CourseRegistrationDoc } from '@db/core';
3
+ import { StudySessionRecord } from '../SessionController';
4
+ import { logger } from '@db/util/logger';
5
+
6
+ /**
7
+ * Service responsible for ELO rating calculations and updates.
8
+ */
9
+ export class EloService {
10
+ private dataLayer: DataLayerProvider;
11
+ private user: UserDBInterface;
12
+
13
+ constructor(dataLayer: DataLayerProvider, user: UserDBInterface) {
14
+ this.dataLayer = dataLayer;
15
+ this.user = user;
16
+ }
17
+
18
+ /**
19
+ * Updates both user and card ELO ratings based on user performance.
20
+ * @param userScore Score between 0-1 representing user performance
21
+ * @param course_id Course identifier
22
+ * @param card_id Card identifier
23
+ * @param userCourseRegDoc User's course registration document (will be mutated)
24
+ * @param currentCard Current card session record
25
+ * @param k Optional K-factor for ELO calculation
26
+ */
27
+ public async updateUserAndCardElo(
28
+ userScore: number,
29
+ course_id: string,
30
+ card_id: string,
31
+ userCourseRegDoc: CourseRegistrationDoc,
32
+ currentCard: StudySessionRecord,
33
+ k?: number
34
+ ): Promise<void> {
35
+ if (k) {
36
+ logger.warn(`k value interpretation not currently implemented`);
37
+ }
38
+ const courseDB = this.dataLayer.getCourseDB(currentCard.card.course_id);
39
+ const userElo = toCourseElo(userCourseRegDoc.courses.find((c) => c.courseID === course_id)!.elo);
40
+ const cardElo = (await courseDB.getCardEloData([currentCard.card.card_id]))[0];
41
+
42
+ if (cardElo && userElo) {
43
+ const eloUpdate = adjustCourseScores(userElo, cardElo, userScore);
44
+ userCourseRegDoc.courses.find((c) => c.courseID === course_id)!.elo = eloUpdate.userElo;
45
+
46
+ const results = await Promise.allSettled([
47
+ this.user.updateUserElo(course_id, eloUpdate.userElo),
48
+ courseDB.updateCardElo(card_id, eloUpdate.cardElo),
49
+ ]);
50
+
51
+ // Check the results of each operation
52
+ const userEloStatus = results[0].status === 'fulfilled';
53
+ const cardEloStatus = results[1].status === 'fulfilled';
54
+
55
+ if (userEloStatus && cardEloStatus) {
56
+ const user = (results[0] as PromiseFulfilledResult<any>).value;
57
+ const card = (results[1] as PromiseFulfilledResult<any>).value;
58
+
59
+ if (user.ok && card && card.ok) {
60
+ logger.info(
61
+ `[EloService] Updated ELOS:
62
+ \tUser: ${JSON.stringify(eloUpdate.userElo)})
63
+ \tCard: ${JSON.stringify(eloUpdate.cardElo)})
64
+ `
65
+ );
66
+ }
67
+ } else {
68
+ // Log which operations succeeded and which failed
69
+ logger.warn(
70
+ `[EloService] Partial ELO update:
71
+ \tUser ELO update: ${userEloStatus ? 'SUCCESS' : 'FAILED'}
72
+ \tCard ELO update: ${cardEloStatus ? 'SUCCESS' : 'FAILED'}`
73
+ );
74
+
75
+ if (!userEloStatus && results[0].status === 'rejected') {
76
+ logger.error('[EloService] User ELO update error:', results[0].reason);
77
+ }
78
+
79
+ if (!cardEloStatus && results[1].status === 'rejected') {
80
+ logger.error('[EloService] Card ELO update error:', results[1].reason);
81
+ }
82
+ }
83
+ }
84
+ }
85
+ }
@@ -0,0 +1,224 @@
1
+ import {
2
+ CardHistory,
3
+ CardRecord,
4
+ CourseRegistrationDoc,
5
+ isQuestionRecord,
6
+ QuestionRecord,
7
+ StudySessionItem,
8
+ } from '@db/core';
9
+ import { logger } from '@db/util/logger';
10
+ import { ResponseResult, StudySessionRecord } from '../SessionController';
11
+ import { EloService } from './EloService';
12
+ import { SrsService } from './SrsService';
13
+
14
+ /**
15
+ * Service responsible for orchestrating the complete response processing workflow.
16
+ * Coordinates SRS scheduling and ELO updates for user card interactions.
17
+ */
18
+ export class ResponseProcessor {
19
+ private srsService: SrsService;
20
+ private eloService: EloService;
21
+
22
+ constructor(srsService: SrsService, eloService: EloService) {
23
+ this.srsService = srsService;
24
+ this.eloService = eloService;
25
+ }
26
+
27
+ /**
28
+ * Processes a user's response to a card, handling SRS scheduling and ELO updates.
29
+ * @param cardRecord User's response record
30
+ * @param cardHistory Promise resolving to the card's history
31
+ * @param studySessionItem Current study session item
32
+ * @param courseRegistrationDoc User's course registration (for ELO updates)
33
+ * @param currentCard Current study session record
34
+ * @param courseId Course identifier
35
+ * @param cardId Card identifier
36
+ * @param maxAttemptsPerView Maximum attempts allowed per view
37
+ * @param maxSessionViews Maximum session views for this card
38
+ * @param sessionViews Current number of session views
39
+ * @returns ResponseResult with navigation and UI instructions
40
+ */
41
+ public async processResponse(
42
+ cardRecord: CardRecord,
43
+ cardHistory: Promise<CardHistory<CardRecord>>,
44
+ studySessionItem: StudySessionItem,
45
+ courseRegistrationDoc: CourseRegistrationDoc,
46
+ currentCard: StudySessionRecord,
47
+ courseId: string,
48
+ cardId: string,
49
+ maxAttemptsPerView: number,
50
+ maxSessionViews: number,
51
+ sessionViews: number
52
+ ): Promise<ResponseResult> {
53
+ // Handle non-question records (simple dismiss)
54
+ if (!isQuestionRecord(cardRecord)) {
55
+ return {
56
+ nextCardAction: 'dismiss-success',
57
+ shouldLoadNextCard: true,
58
+ isCorrect: true, // non-question records are considered "correct"
59
+ shouldClearFeedbackShadow: true,
60
+ };
61
+ }
62
+
63
+ const history = await cardHistory;
64
+
65
+ // Handle correct responses
66
+ if (cardRecord.isCorrect) {
67
+ return this.processCorrectResponse(
68
+ cardRecord,
69
+ history,
70
+ studySessionItem,
71
+ courseRegistrationDoc,
72
+ currentCard,
73
+ courseId,
74
+ cardId
75
+ );
76
+ } else {
77
+ // Handle incorrect responses
78
+ return this.processIncorrectResponse(
79
+ cardRecord,
80
+ history,
81
+ courseRegistrationDoc,
82
+ currentCard,
83
+ courseId,
84
+ cardId,
85
+ maxAttemptsPerView,
86
+ maxSessionViews,
87
+ sessionViews
88
+ );
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Handles processing for correct responses: SRS scheduling and ELO updates.
94
+ */
95
+ private processCorrectResponse(
96
+ cardRecord: QuestionRecord,
97
+ history: CardHistory<CardRecord>,
98
+ studySessionItem: StudySessionItem,
99
+ courseRegistrationDoc: CourseRegistrationDoc,
100
+ currentCard: StudySessionRecord,
101
+ courseId: string,
102
+ cardId: string
103
+ ): ResponseResult {
104
+ // Only schedule and update ELO for first-time attempts
105
+ if (cardRecord.priorAttemps === 0) {
106
+ // Schedule the card for future review based on performance (async, non-blocking)
107
+ void this.srsService.scheduleReview(history, studySessionItem);
108
+
109
+ // Update ELO ratings
110
+ if (history.records.length === 1) {
111
+ // First interaction with this card - standard ELO update (async, non-blocking)
112
+ const userScore = 0.5 + (cardRecord.performance as number) / 2;
113
+ void this.eloService.updateUserAndCardElo(
114
+ userScore,
115
+ courseId,
116
+ cardId,
117
+ courseRegistrationDoc,
118
+ currentCard
119
+ );
120
+ } else {
121
+ // Multiple interactions - reduce K-factor to limit ELO volatility (async, non-blocking)
122
+ const k = Math.ceil(32 / history.records.length);
123
+ const userScore = 0.5 + (cardRecord.performance as number) / 2;
124
+ void this.eloService.updateUserAndCardElo(
125
+ userScore,
126
+ courseId,
127
+ cardId,
128
+ courseRegistrationDoc,
129
+ currentCard,
130
+ k
131
+ );
132
+ }
133
+
134
+ logger.info(
135
+ '[ResponseProcessor] Processed correct response with SRS scheduling and ELO update'
136
+ );
137
+
138
+ return {
139
+ nextCardAction: 'dismiss-success',
140
+ shouldLoadNextCard: true,
141
+ isCorrect: true,
142
+ performanceScore: cardRecord.performance as number,
143
+ shouldClearFeedbackShadow: true,
144
+ };
145
+ } else {
146
+ logger.info(
147
+ '[ResponseProcessor] Processed correct response (retry attempt - no scheduling/ELO)'
148
+ );
149
+
150
+ return {
151
+ nextCardAction: 'marked-failed',
152
+ shouldLoadNextCard: true,
153
+ isCorrect: true,
154
+ performanceScore: cardRecord.performance as number,
155
+ shouldClearFeedbackShadow: true,
156
+ };
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Handles processing for incorrect responses: ELO updates only.
162
+ */
163
+ private processIncorrectResponse(
164
+ cardRecord: QuestionRecord,
165
+ history: CardHistory<CardRecord>,
166
+ courseRegistrationDoc: CourseRegistrationDoc,
167
+ currentCard: StudySessionRecord,
168
+ courseId: string,
169
+ cardId: string,
170
+ maxAttemptsPerView: number,
171
+ maxSessionViews: number,
172
+ sessionViews: number
173
+ ): ResponseResult {
174
+ // Update ELO for first-time failures (not subsequent attempts on same card) (async, non-blocking)
175
+ if (history.records.length !== 1 && cardRecord.priorAttemps === 0) {
176
+ void this.eloService.updateUserAndCardElo(
177
+ 0, // Failed response = 0 score
178
+ courseId,
179
+ cardId,
180
+ courseRegistrationDoc,
181
+ currentCard
182
+ );
183
+ logger.info('[ResponseProcessor] Processed incorrect response with ELO update');
184
+ } else {
185
+ logger.info('[ResponseProcessor] Processed incorrect response (no ELO update needed)');
186
+ }
187
+
188
+ // Determine navigation based on attempt limits
189
+ if (currentCard.records.length >= maxAttemptsPerView) {
190
+ if (sessionViews >= maxSessionViews) {
191
+ // Too many session views - dismiss completely with ELO penalty (async, non-blocking)
192
+ void this.eloService.updateUserAndCardElo(
193
+ 0,
194
+ courseId,
195
+ cardId,
196
+ courseRegistrationDoc,
197
+ currentCard
198
+ );
199
+ return {
200
+ nextCardAction: 'dismiss-failed',
201
+ shouldLoadNextCard: true,
202
+ isCorrect: false,
203
+ shouldClearFeedbackShadow: true,
204
+ };
205
+ } else {
206
+ // Mark as failed for later retry
207
+ return {
208
+ nextCardAction: 'marked-failed',
209
+ shouldLoadNextCard: true,
210
+ isCorrect: false,
211
+ shouldClearFeedbackShadow: true,
212
+ };
213
+ }
214
+ } else {
215
+ // Allow more attempts on same card
216
+ return {
217
+ nextCardAction: 'none',
218
+ shouldLoadNextCard: false,
219
+ isCorrect: false,
220
+ shouldClearFeedbackShadow: true,
221
+ };
222
+ }
223
+ }
224
+ }
@@ -0,0 +1,44 @@
1
+ import moment from 'moment';
2
+ import { CardHistory, CardRecord, UserDBInterface } from '@db/core';
3
+ import { isReview, StudySessionItem } from '@db/impl/couch';
4
+ import { newInterval } from '../SpacedRepetition';
5
+ import { logger } from '@db/util/logger';
6
+
7
+ /**
8
+ * Service responsible for Spaced Repetition System (SRS) scheduling logic.
9
+ */
10
+ export class SrsService {
11
+ private user: UserDBInterface;
12
+
13
+ constructor(user: UserDBInterface) {
14
+ this.user = user;
15
+ }
16
+
17
+ /**
18
+ * Calculates the next review time for a card based on its history and
19
+ * schedules it in the user's database.
20
+ * @param history The full history of the card.
21
+ * @param item The study session item, used to determine if a previous review needs to be cleared.
22
+ */
23
+ public async scheduleReview(
24
+ history: CardHistory<CardRecord>,
25
+ item: StudySessionItem
26
+ ): Promise<void> {
27
+ const nextInterval = newInterval(this.user, history);
28
+ const nextReviewTime = moment.utc().add(nextInterval, 'seconds');
29
+
30
+ if (isReview(item)) {
31
+ logger.info(`[SrsService] Removing previously scheduled review for: ${item.cardID}`);
32
+ void this.user.removeScheduledCardReview(item.reviewID);
33
+ }
34
+
35
+ void this.user.scheduleCardReview({
36
+ user: this.user.getUsername(),
37
+ course_id: history.courseID,
38
+ card_id: history.cardID,
39
+ time: nextReviewTime,
40
+ scheduledFor: item.contentSourceType,
41
+ schedulingAgentId: item.contentSourceID,
42
+ });
43
+ }
44
+ }