@vue-skuilder/db 0.2.13 → 0.2.15

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 (37) hide show
  1. package/dist/{contentSource-C-0t0y0V.d.ts → contentSource-B1p-vdz7.d.ts} +7 -0
  2. package/dist/{contentSource-jSkcOt2s.d.cts → contentSource-Brz42x7n.d.cts} +7 -0
  3. package/dist/core/index.d.cts +3 -3
  4. package/dist/core/index.d.ts +3 -3
  5. package/dist/core/index.js +6 -3
  6. package/dist/core/index.js.map +1 -1
  7. package/dist/core/index.mjs +6 -3
  8. package/dist/core/index.mjs.map +1 -1
  9. package/dist/{dataLayerProvider-BB0oi9T0.d.ts → dataLayerProvider-BWayUIoK.d.ts} +1 -1
  10. package/dist/{dataLayerProvider-BDClIrFC.d.cts → dataLayerProvider-CpwpT1rM.d.cts} +1 -1
  11. package/dist/impl/couch/index.d.cts +2 -2
  12. package/dist/impl/couch/index.d.ts +2 -2
  13. package/dist/impl/couch/index.js +6 -3
  14. package/dist/impl/couch/index.js.map +1 -1
  15. package/dist/impl/couch/index.mjs +6 -3
  16. package/dist/impl/couch/index.mjs.map +1 -1
  17. package/dist/impl/static/index.d.cts +2 -2
  18. package/dist/impl/static/index.d.ts +2 -2
  19. package/dist/impl/static/index.js +6 -3
  20. package/dist/impl/static/index.js.map +1 -1
  21. package/dist/impl/static/index.mjs +6 -3
  22. package/dist/impl/static/index.mjs.map +1 -1
  23. package/dist/index.d.cts +47 -5
  24. package/dist/index.d.ts +47 -5
  25. package/dist/index.js +50 -30
  26. package/dist/index.js.map +1 -1
  27. package/dist/index.mjs +45 -30
  28. package/dist/index.mjs.map +1 -1
  29. package/package.json +3 -3
  30. package/src/core/interfaces/userDB.ts +8 -0
  31. package/src/impl/common/BaseUserDB.ts +2 -2
  32. package/src/impl/common/userDBHelpers.ts +14 -1
  33. package/src/index.ts +6 -0
  34. package/src/study/SessionOverlay.ts +16 -3
  35. package/src/study/index.ts +4 -0
  36. package/src/study/services/EloService.ts +20 -8
  37. package/src/study/services/ResponseProcessor.ts +62 -50
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
7
- "version": "0.2.13",
7
+ "version": "0.2.15",
8
8
  "description": "Database layer for vue-skuilder",
9
9
  "main": "dist/index.js",
10
10
  "module": "dist/index.mjs",
@@ -48,7 +48,7 @@
48
48
  },
49
49
  "dependencies": {
50
50
  "@nilock2/pouchdb-authentication": "^1.0.2",
51
- "@vue-skuilder/common": "0.2.13",
51
+ "@vue-skuilder/common": "0.2.15",
52
52
  "cross-fetch": "^4.1.0",
53
53
  "moment": "^2.29.4",
54
54
  "pouchdb": "^9.0.0",
@@ -62,5 +62,5 @@
62
62
  "vite": "^8.0.0",
63
63
  "vitest": "^4.1.0"
64
64
  },
65
- "stableVersion": "0.2.13"
65
+ "stableVersion": "0.2.15"
66
66
  }
@@ -55,6 +55,14 @@ export interface UserDBReader {
55
55
  */
56
56
  getPendingReviews(courseId?: string): Promise<ScheduledCard[]>;
57
57
 
58
+ /**
59
+ * Get all scheduled reviews due within the next `daysCount` days —
60
+ * including ones not yet due. `getPendingReviews` is the `daysCount = 0`
61
+ * special case: it returns only reviews already due, so a review scheduled
62
+ * moments ago (necessarily in the future) will never appear in it.
63
+ */
64
+ getReviewsForcast(daysCount: number, courseId?: string): Promise<ScheduledCard[]>;
65
+
58
66
  getActivityRecords(): Promise<ActivityRecord[]>;
59
67
 
60
68
  /**
@@ -416,9 +416,9 @@ Currently logged-in as ${this._username}.`
416
416
  .map((r) => r.doc!);
417
417
  }
418
418
 
419
- public async getReviewsForcast(daysCount: number) {
419
+ public async getReviewsForcast(daysCount: number, course_id?: string) {
420
420
  const time = moment.utc().add(daysCount, 'days');
421
- return this.getReviewstoDate(time);
421
+ return this.getReviewstoDate(time, course_id);
422
422
  }
423
423
 
424
424
  public async getPendingReviews(course_id?: string) {
@@ -7,6 +7,19 @@ import { ScheduledCard } from '@db/core/types/user';
7
7
 
8
8
  export const REVIEW_TIME_FORMAT: string = 'YYYY-MM-DD--kk:mm:ss-SSS';
9
9
 
10
+ /**
11
+ * Build a ScheduledCard `_id` for the given review time.
12
+ *
13
+ * The id suffix is not a mere uniquifier: `getReviewstoDate` PARSES the id
14
+ * (with {@link REVIEW_TIME_FORMAT}) to decide due-ness, so the id is the
15
+ * datum. Anything that writes or rewrites scheduled-card docs (scheduling,
16
+ * user-state snapshot rebasing) must construct ids through this helper —
17
+ * a hand-rolled formatter will get moment's `kk` token (01-24 hours) wrong.
18
+ */
19
+ export function makeScheduledCardId(reviewTime: string | Date | moment.Moment): string {
20
+ return DocTypePrefixes[DocType.SCHEDULED_CARD] + moment.utc(reviewTime).format(REVIEW_TIME_FORMAT);
21
+ }
22
+
10
23
  import pouch from '../couch/pouchdb-setup';
11
24
  import { getDbPath } from '../../util/dataDirectory';
12
25
 
@@ -123,7 +136,7 @@ export function scheduleCardReviewLocal(
123
136
  const now = moment.utc();
124
137
  logger.info(`Scheduling for review in: ${review.time.diff(now, 'h') / 24} days`);
125
138
  void userDB.put<ScheduledCard>({
126
- _id: DocTypePrefixes[DocType.SCHEDULED_CARD] + review.time.format(REVIEW_TIME_FORMAT),
139
+ _id: makeScheduledCardId(review.time),
127
140
  cardId: review.card_id,
128
141
  reviewTime: review.time.toISOString(),
129
142
  courseId: review.course_id,
package/src/index.ts CHANGED
@@ -9,6 +9,12 @@ export * from './study';
9
9
  export * from './util';
10
10
  export * from './factory';
11
11
 
12
+ // Scheduled-card id construction: the id embeds the review time and is parsed
13
+ // by the due-review read path, so out-of-tree writers (e.g. user-state
14
+ // snapshot rebasing) must build ids with this rather than reimplementing the
15
+ // moment format.
16
+ export { REVIEW_TIME_FORMAT, makeScheduledCardId } from './impl/common/userDBHelpers';
17
+
12
18
  // Export CouchDB user types for use in Express backend
13
19
  export type {
14
20
  UserAccountStatus,
@@ -81,6 +81,19 @@ export interface SessionDebugTarget {
81
81
  getDebugSnapshot(): SessionDebugSnapshot;
82
82
  }
83
83
 
84
+ /**
85
+ * The live-session surface available to out-of-tree callers through the
86
+ * active-controller registry: the debug snapshot, plus the small set of
87
+ * user-facing session controls a view may need without holding a ref into
88
+ * the `<StudySession>` component's internals (e.g. wiring a timer's
89
+ * add-time button from a composable). Keep this narrow — it is not a
90
+ * general escape hatch to `SessionController`.
91
+ */
92
+ export interface ActiveSessionHandle extends SessionDebugTarget {
93
+ /** Extend the session clock by `seconds`. */
94
+ addTime(seconds: number): void;
95
+ }
96
+
84
97
  // ----------------------------------------------------------------------------
85
98
  // Active-controller registry
86
99
  // ----------------------------------------------------------------------------
@@ -90,14 +103,14 @@ export interface SessionDebugTarget {
90
103
  // registrar without pulling in the overlay's DOM code or risking an import
91
104
  // cycle with SessionDebugger.
92
105
 
93
- let activeController: SessionDebugTarget | null = null;
106
+ let activeController: ActiveSessionHandle | null = null;
94
107
 
95
108
  /** Called by SessionController's constructor. Pass `null` to deregister. */
96
- export function registerActiveController(controller: SessionDebugTarget | null): void {
109
+ export function registerActiveController(controller: ActiveSessionHandle | null): void {
97
110
  activeController = controller;
98
111
  }
99
112
 
100
- export function getActiveController(): SessionDebugTarget | null {
113
+ export function getActiveController(): ActiveSessionHandle | null {
101
114
  return activeController;
102
115
  }
103
116
 
@@ -4,3 +4,7 @@ export * from './SpacedRepetition';
4
4
  export * from './TagFilteredContentSource';
5
5
  export * from './MixerDebugger';
6
6
  export * from './SessionDebugger';
7
+ // `getActiveController()` + `SessionDebugSnapshot` — the live-state read the
8
+ // SessionOverlay renders internally. Exported so hosts can build their own UI
9
+ // over the same snapshot (LettersPractice's /showHN annotated view does).
10
+ export * from './SessionOverlay';
@@ -41,14 +41,20 @@ export class EloService {
41
41
  logger.warn(`k value interpretation not currently implemented`);
42
42
  }
43
43
  const courseDB = this.dataLayer.getCourseDB(currentCard.card.course_id);
44
- const userElo = toCourseElo(
45
- userCourseRegDoc.courses.find((c) => c.courseID === course_id)!.elo
46
- );
44
+ const courseReg = userCourseRegDoc.courses.find((c) => c.courseID === course_id);
45
+ if (!courseReg) {
46
+ logger.error(
47
+ `[EloService] No registration for course ${course_id} on user's registration doc — ` +
48
+ `skipping ELO update for card ${card_id}. (Is the user registered for this course?)`
49
+ );
50
+ return;
51
+ }
52
+ const userElo = toCourseElo(courseReg.elo);
47
53
  const cardElo = (await courseDB.getCardEloData([currentCard.card.card_id]))[0];
48
54
 
49
55
  if (cardElo && userElo) {
50
56
  const eloUpdate = adjustCourseScores(userElo, cardElo, userScore);
51
- userCourseRegDoc.courses.find((c) => c.courseID === course_id)!.elo = eloUpdate.userElo;
57
+ courseReg.elo = eloUpdate.userElo;
52
58
 
53
59
  const results = await Promise.allSettled([
54
60
  this.user.updateUserElo(course_id, eloUpdate.userElo),
@@ -108,9 +114,15 @@ export class EloService {
108
114
  currentCard: StudySessionRecord
109
115
  ): Promise<void> {
110
116
  const courseDB = this.dataLayer.getCourseDB(currentCard.card.course_id);
111
- const userElo = toCourseElo(
112
- userCourseRegDoc.courses.find((c) => c.courseID === course_id)!.elo
113
- );
117
+ const courseReg = userCourseRegDoc.courses.find((c) => c.courseID === course_id);
118
+ if (!courseReg) {
119
+ logger.error(
120
+ `[EloService] No registration for course ${course_id} on user's registration doc — ` +
121
+ `skipping per-tag ELO update for card ${card_id}. (Is the user registered for this course?)`
122
+ );
123
+ return;
124
+ }
125
+ const userElo = toCourseElo(courseReg.elo);
114
126
 
115
127
  const [cardEloResults, cardTagsMap] = await Promise.all([
116
128
  courseDB.getCardEloData([currentCard.card.card_id]),
@@ -134,7 +146,7 @@ export class EloService {
134
146
 
135
147
  if (cardElo && userElo) {
136
148
  const eloUpdate = adjustCourseScoresPerTag(userElo, cardElo, enriched);
137
- userCourseRegDoc.courses.find((c) => c.courseID === course_id)!.elo = eloUpdate.userElo;
149
+ courseReg.elo = eloUpdate.userElo;
138
150
 
139
151
  const results = await Promise.allSettled([
140
152
  this.user.updateUserElo(course_id, eloUpdate.userElo),
@@ -35,6 +35,16 @@ export class ResponseProcessor {
35
35
  this.eloService = eloService;
36
36
  }
37
37
 
38
+ /**
39
+ * ELO updates are fired without awaiting so response handling isn't blocked
40
+ * on DB writes — but an unhandled rejection silently drops the update.
41
+ * This produces a catch handler that surfaces the failure in logs.
42
+ */
43
+ private logEloFailure(context: string, cardId: string): (e: unknown) => void {
44
+ return (e) =>
45
+ logger.error(`[ResponseProcessor] ELO update failed (${context}) for ${cardId}:`, e);
46
+ }
47
+
38
48
  /**
39
49
  * Parses performance data into global score and optional per-tag scores.
40
50
  *
@@ -194,37 +204,37 @@ export class ResponseProcessor {
194
204
  `scored=[${scoredTags.join(', ')}] count-only=[${nullTags.join(', ')}]`
195
205
  );
196
206
 
197
- void this.eloService.updateUserAndCardEloPerTag(
198
- taggedPerformance,
199
- courseId,
200
- cardId,
201
- courseRegistrationDoc,
202
- currentCard
203
- );
207
+ void this.eloService
208
+ .updateUserAndCardEloPerTag(
209
+ taggedPerformance,
210
+ courseId,
211
+ cardId,
212
+ courseRegistrationDoc,
213
+ currentCard
214
+ )
215
+ .catch(this.logEloFailure('correct per-tag', cardId));
204
216
  } else {
205
217
  // Standard single-score ELO update (backward compatible)
206
218
  const userScore = 0.5 + globalScore / 2;
207
219
 
208
220
  if (history.records.length === 1) {
209
221
  // First interaction with this card - standard ELO update
210
- void this.eloService.updateUserAndCardElo(
211
- userScore,
212
- courseId,
213
- cardId,
214
- courseRegistrationDoc,
215
- currentCard
216
- );
222
+ void this.eloService
223
+ .updateUserAndCardElo(userScore, courseId, cardId, courseRegistrationDoc, currentCard)
224
+ .catch(this.logEloFailure('correct', cardId));
217
225
  } else {
218
226
  // Multiple interactions - reduce K-factor to limit ELO volatility
219
227
  const k = Math.ceil(32 / history.records.length);
220
- void this.eloService.updateUserAndCardElo(
221
- userScore,
222
- courseId,
223
- cardId,
224
- courseRegistrationDoc,
225
- currentCard,
226
- k
227
- );
228
+ void this.eloService
229
+ .updateUserAndCardElo(
230
+ userScore,
231
+ courseId,
232
+ cardId,
233
+ courseRegistrationDoc,
234
+ currentCard,
235
+ k
236
+ )
237
+ .catch(this.logEloFailure('correct repeat-view', cardId));
228
238
  }
229
239
  logger.info(
230
240
  `[FirstContactElo] correct first-attempt ELO update (score=${userScore.toFixed(3)}) ` +
@@ -286,13 +296,15 @@ export class ResponseProcessor {
286
296
  if (cardRecord.priorAttemps === 0) {
287
297
  if (taggedPerformance) {
288
298
  // Per-tag ELO update for incorrect response
289
- void this.eloService.updateUserAndCardEloPerTag(
290
- taggedPerformance,
291
- courseId,
292
- cardId,
293
- courseRegistrationDoc,
294
- currentCard
295
- );
299
+ void this.eloService
300
+ .updateUserAndCardEloPerTag(
301
+ taggedPerformance,
302
+ courseId,
303
+ cardId,
304
+ courseRegistrationDoc,
305
+ currentCard
306
+ )
307
+ .catch(this.logEloFailure('incorrect per-tag', cardId));
296
308
  logger.info(
297
309
  `[FirstContactElo] incorrect first-attempt per-tag ELO update for ${cardId} ` +
298
310
  `(historyLen=${history.records.length}, priorAttemps=${cardRecord.priorAttemps}, ` +
@@ -300,13 +312,15 @@ export class ResponseProcessor {
300
312
  );
301
313
  } else {
302
314
  // Standard single-score ELO update
303
- void this.eloService.updateUserAndCardElo(
304
- 0, // Failed response = 0 score
305
- courseId,
306
- cardId,
307
- courseRegistrationDoc,
308
- currentCard
309
- );
315
+ void this.eloService
316
+ .updateUserAndCardElo(
317
+ 0, // Failed response = 0 score
318
+ courseId,
319
+ cardId,
320
+ courseRegistrationDoc,
321
+ currentCard
322
+ )
323
+ .catch(this.logEloFailure('incorrect', cardId));
310
324
  logger.info(
311
325
  `[FirstContactElo] incorrect first-attempt ELO update (score=0) for ${cardId} ` +
312
326
  `(historyLen=${history.records.length}, priorAttemps=${cardRecord.priorAttemps})`
@@ -329,21 +343,19 @@ export class ResponseProcessor {
329
343
  if (!eloUpdated) {
330
344
  if (taggedPerformance) {
331
345
  // Use tagged performance for final failure
332
- void this.eloService.updateUserAndCardEloPerTag(
333
- taggedPerformance,
334
- courseId,
335
- cardId,
336
- courseRegistrationDoc,
337
- currentCard
338
- );
346
+ void this.eloService
347
+ .updateUserAndCardEloPerTag(
348
+ taggedPerformance,
349
+ courseId,
350
+ cardId,
351
+ courseRegistrationDoc,
352
+ currentCard
353
+ )
354
+ .catch(this.logEloFailure('dismiss-failed per-tag', cardId));
339
355
  } else {
340
- void this.eloService.updateUserAndCardElo(
341
- 0,
342
- courseId,
343
- cardId,
344
- courseRegistrationDoc,
345
- currentCard
346
- );
356
+ void this.eloService
357
+ .updateUserAndCardElo(0, courseId, cardId, courseRegistrationDoc, currentCard)
358
+ .catch(this.logEloFailure('dismiss-failed', cardId));
347
359
  }
348
360
  logger.info(
349
361
  `[FirstContactElo] dismiss-failed final ELO penalty for ${cardId} ` +