@vue-skuilder/db 0.1.11-7 → 0.1.11

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 (68) hide show
  1. package/dist/core/index.d.mts +5 -5
  2. package/dist/core/index.d.ts +5 -5
  3. package/dist/core/index.js +212 -50
  4. package/dist/core/index.js.map +1 -1
  5. package/dist/core/index.mjs +212 -50
  6. package/dist/core/index.mjs.map +1 -1
  7. package/dist/{dataLayerProvider-DqtNroSh.d.ts → dataLayerProvider-VieuAAkV.d.mts} +8 -1
  8. package/dist/{dataLayerProvider-BInqI_RF.d.mts → dataLayerProvider-juuqUHOP.d.ts} +8 -1
  9. package/dist/impl/couch/index.d.mts +19 -7
  10. package/dist/impl/couch/index.d.ts +19 -7
  11. package/dist/impl/couch/index.js +229 -63
  12. package/dist/impl/couch/index.js.map +1 -1
  13. package/dist/impl/couch/index.mjs +228 -62
  14. package/dist/impl/couch/index.mjs.map +1 -1
  15. package/dist/impl/static/index.d.mts +13 -6
  16. package/dist/impl/static/index.d.ts +13 -6
  17. package/dist/impl/static/index.js +142 -46
  18. package/dist/impl/static/index.js.map +1 -1
  19. package/dist/impl/static/index.mjs +142 -46
  20. package/dist/impl/static/index.mjs.map +1 -1
  21. package/dist/{index-CLL31bEy.d.ts → index-CWY6yhkV.d.ts} +1 -1
  22. package/dist/{index-CUNnL38E.d.mts → index-DZyxHCcf.d.mts} +1 -1
  23. package/dist/index.d.mts +28 -20
  24. package/dist/index.d.ts +28 -20
  25. package/dist/index.js +374 -108
  26. package/dist/index.js.map +1 -1
  27. package/dist/index.mjs +378 -108
  28. package/dist/index.mjs.map +1 -1
  29. package/dist/pouch/index.d.mts +1 -0
  30. package/dist/pouch/index.d.ts +1 -0
  31. package/dist/pouch/index.js +49 -0
  32. package/dist/pouch/index.js.map +1 -0
  33. package/dist/pouch/index.mjs +16 -0
  34. package/dist/pouch/index.mjs.map +1 -0
  35. package/dist/{types-DC-ckZug.d.mts → types-Che4wTwA.d.mts} +1 -1
  36. package/dist/{types-BefDGkKa.d.ts → types-DtoI27Xh.d.ts} +1 -1
  37. package/dist/{types-legacy-Birv-Jx6.d.mts → types-legacy-B8ahaCbj.d.mts} +5 -1
  38. package/dist/{types-legacy-Birv-Jx6.d.ts → types-legacy-B8ahaCbj.d.ts} +5 -1
  39. package/dist/{userDB-C33Hzjgn.d.mts → userDB-B7zTQ123.d.ts} +88 -55
  40. package/dist/{userDB-DusL7OXe.d.ts → userDB-DJ8HMw83.d.mts} +88 -55
  41. package/dist/util/packer/index.d.mts +3 -3
  42. package/dist/util/packer/index.d.ts +3 -3
  43. package/package.json +3 -3
  44. package/src/core/interfaces/contentSource.ts +3 -2
  45. package/src/core/interfaces/courseDB.ts +26 -3
  46. package/src/core/interfaces/dataLayerProvider.ts +9 -1
  47. package/src/core/interfaces/userDB.ts +80 -64
  48. package/src/core/navigators/elo.ts +10 -7
  49. package/src/core/navigators/index.ts +1 -1
  50. package/src/core/types/types-legacy.ts +5 -0
  51. package/src/impl/common/BaseUserDB.ts +45 -3
  52. package/src/impl/couch/CouchDBSyncStrategy.ts +2 -2
  53. package/src/impl/couch/PouchDataLayerProvider.ts +21 -0
  54. package/src/impl/couch/adminDB.ts +2 -2
  55. package/src/impl/couch/auth.ts +13 -4
  56. package/src/impl/couch/classroomDB.ts +10 -12
  57. package/src/impl/couch/courseAPI.ts +2 -2
  58. package/src/impl/couch/courseDB.ts +130 -11
  59. package/src/impl/couch/courseLookupDB.ts +4 -3
  60. package/src/impl/couch/index.ts +36 -4
  61. package/src/impl/couch/pouchdb-setup.ts +3 -3
  62. package/src/impl/couch/updateQueue.ts +51 -33
  63. package/src/impl/static/StaticDataLayerProvider.ts +11 -0
  64. package/src/impl/static/courseDB.ts +47 -8
  65. package/src/pouch/index.ts +2 -0
  66. package/src/study/SessionController.ts +168 -51
  67. package/src/study/SpacedRepetition.ts +1 -1
  68. package/tsup.config.ts +1 -0
@@ -10,6 +10,7 @@ import {
10
10
  import { CardRecord } from '@db/core';
11
11
  import { Loggable } from '@db/util';
12
12
  import { ScheduledCard } from '@db/core/types/user';
13
+ import { ViewData } from '@vue-skuilder/common';
13
14
 
14
15
  function randomInt(min: number, max: number): number {
15
16
  return Math.floor(Math.random() * (max - min + 1)) + min;
@@ -25,7 +26,21 @@ export interface StudySessionRecord {
25
26
  records: CardRecord[];
26
27
  }
27
28
 
28
- class ItemQueue<T extends StudySessionItem> {
29
+ export interface HydratedCard<TView = unknown> {
30
+ item: StudySessionItem;
31
+ view: TView;
32
+ data: ViewData[];
33
+ }
34
+
35
+ import {
36
+ CardData,
37
+ DisplayableData,
38
+ displayableDataToViewData,
39
+ isCourseElo,
40
+ toCourseElo,
41
+ } from '@vue-skuilder/common';
42
+
43
+ class ItemQueue<T> {
29
44
  private q: T[] = [];
30
45
  private seenCardIds: string[] = [];
31
46
  private _dequeueCount: number = 0;
@@ -33,16 +48,16 @@ class ItemQueue<T extends StudySessionItem> {
33
48
  return this._dequeueCount;
34
49
  }
35
50
 
36
- public add(item: T) {
37
- if (this.seenCardIds.find((d) => d === item.cardID)) {
51
+ public add(item: T, cardId: string) {
52
+ if (this.seenCardIds.find((d) => d === cardId)) {
38
53
  return; // do not re-add a card to the same queue
39
54
  }
40
55
 
41
- this.seenCardIds.push(item.cardID);
56
+ this.seenCardIds.push(cardId);
42
57
  this.q.push(item);
43
58
  }
44
- public addAll(items: T[]) {
45
- items.forEach((i) => this.add(i));
59
+ public addAll(items: T[], cardIdExtractor: (item: T) => string) {
60
+ items.forEach((i) => this.add(i, cardIdExtractor(i)));
46
61
  }
47
62
  public get length() {
48
63
  return this.q.length;
@@ -62,14 +77,21 @@ class ItemQueue<T extends StudySessionItem> {
62
77
 
63
78
  public get toString(): string {
64
79
  return (
65
- `${typeof this.q[0]}:\n` + this.q.map((i) => `\t${i.qualifiedID}: ${i.status}`).join('\n')
80
+ `${typeof this.q[0]}:\n` +
81
+ this.q
82
+ .map((i) => `\t${(i as any).courseID}+${(i as any).cardID}: ${(i as any).status}`)
83
+ .join('\n')
66
84
  );
67
85
  }
68
86
  }
69
87
 
70
- export class SessionController extends Loggable {
88
+ import { DataLayerProvider } from '@db/core';
89
+
90
+ export class SessionController<TView = unknown> extends Loggable {
71
91
  _className = 'SessionController';
72
92
  private sources: StudyContentSource[];
93
+ private dataLayer: DataLayerProvider;
94
+ private getViewComponent: (viewId: string) => TView;
73
95
  private _sessionRecord: StudySessionRecord[] = [];
74
96
  public set sessionRecord(r: StudySessionRecord[]) {
75
97
  this._sessionRecord = r;
@@ -78,12 +100,9 @@ export class SessionController extends Loggable {
78
100
  private reviewQ: ItemQueue<StudySessionReviewItem> = new ItemQueue<StudySessionReviewItem>();
79
101
  private newQ: ItemQueue<StudySessionNewItem> = new ItemQueue<StudySessionNewItem>();
80
102
  private failedQ: ItemQueue<StudySessionFailedItem> = new ItemQueue<StudySessionFailedItem>();
103
+ private hydratedQ: ItemQueue<HydratedCard<TView>> = new ItemQueue<HydratedCard<TView>>();
81
104
  private _currentCard: StudySessionItem | null = null;
82
- /**
83
- * Indicates whether the session has been initialized - eg, the
84
- * queues have been populated.
85
- */
86
- private _isInitialized: boolean = false;
105
+ private hydration_in_progress: boolean = false;
87
106
 
88
107
  private startTime: Date;
89
108
  private endTime: Date;
@@ -103,13 +122,20 @@ export class SessionController extends Loggable {
103
122
  /**
104
123
  *
105
124
  */
106
- constructor(sources: StudyContentSource[], time: number) {
125
+ constructor(
126
+ sources: StudyContentSource[],
127
+ time: number,
128
+ dataLayer: DataLayerProvider,
129
+ getViewComponent: (viewId: string) => TView
130
+ ) {
107
131
  super();
108
132
 
109
133
  this.sources = sources;
110
134
  this.startTime = new Date();
111
135
  this._secondsRemaining = time;
112
136
  this.endTime = new Date(this.startTime.valueOf() + 1000 * this._secondsRemaining);
137
+ this.dataLayer = dataLayer;
138
+ this.getViewComponent = getViewComponent;
113
139
 
114
140
  this.log(`Session constructed:
115
141
  startTime: ${this.startTime}
@@ -173,7 +199,7 @@ export class SessionController extends Loggable {
173
199
  this.error('Error preparing study session:', e);
174
200
  }
175
201
 
176
- this._isInitialized = true;
202
+ await this._fillHydratedQueue();
177
203
 
178
204
  this._intervalHandle = setInterval(() => {
179
205
  this.tick();
@@ -221,11 +247,8 @@ export class SessionController extends Loggable {
221
247
  }
222
248
 
223
249
  let report = 'Review session created with:\n';
224
- for (let i = 0; i < dueCards.length; i++) {
225
- const card = dueCards[i];
226
- this.reviewQ.add(card);
227
- report += `\t${card.qualifiedID}}\n`;
228
- }
250
+ this.reviewQ.addAll(dueCards, (c) => c.cardID);
251
+ report += dueCards.map((card) => `Card ${card.courseID}::${card.cardID} `).join('\n');
229
252
  this.log(report);
230
253
  }
231
254
 
@@ -244,55 +267,47 @@ export class SessionController extends Loggable {
244
267
  for (let i = 0; i < newContent.length; i++) {
245
268
  if (newContent[i].length > 0) {
246
269
  const item = newContent[i].splice(0, 1)[0];
247
- this.log(`Adding new card: ${item.qualifiedID}`);
248
- this.newQ.add(item);
270
+ this.log(`Adding new card: ${item.courseID}::${item.cardID}`);
271
+ this.newQ.add(item, item.cardID);
249
272
  n--;
250
273
  }
251
274
  }
252
275
  }
253
276
  }
254
277
 
255
- private nextNewCard(): StudySessionNewItem | null {
256
- const item = this.newQ.dequeue();
257
-
258
- // queue some more content if we are getting low
259
- if (this._isInitialized && this.newQ.length < 5) {
260
- void this.getNewCards();
261
- }
262
-
263
- return item;
264
- }
265
-
266
- public nextCard(
278
+ private _selectNextItemToHydrate(
267
279
  action:
268
280
  | 'dismiss-success'
269
281
  | 'dismiss-failed'
270
282
  | 'marked-failed'
271
283
  | 'dismiss-error' = 'dismiss-success'
272
284
  ): StudySessionItem | null {
273
- // dismiss (or sort to failedQ) the current card
274
- this.dismissCurrentCard(action);
275
-
276
285
  const choice = Math.random();
277
286
  let newBound: number = 0.1;
278
287
  let reviewBound: number = 0.75;
279
288
 
280
289
  if (this.reviewQ.length === 0 && this.failedQ.length === 0 && this.newQ.length === 0) {
281
290
  // all queues empty - session is over (and course is complete?)
282
- this._currentCard = null;
283
- return this._currentCard;
291
+ return null;
284
292
  }
285
293
 
286
294
  if (this._secondsRemaining < 2 && this.failedQ.length === 0) {
287
295
  // session is over!
288
- this._currentCard = null;
289
- return this._currentCard;
296
+ return null;
297
+ }
298
+
299
+ // If timer expired, only return failed cards
300
+ if (this._secondsRemaining <= 0) {
301
+ if (this.failedQ.length > 0) {
302
+ return this.failedQ.peek(0);
303
+ } else {
304
+ return null; // No more failed cards, session over
305
+ }
290
306
  }
291
307
 
292
308
  // supply new cards at start of session
293
309
  if (this.newQ.dequeueCount < this.sources.length && this.newQ.length) {
294
- this._currentCard = this.nextNewCard();
295
- return this._currentCard;
310
+ return this.newQ.peek(0);
296
311
  }
297
312
 
298
313
  const cleanupTime = this.estimateCleanupTime();
@@ -333,17 +348,41 @@ export class SessionController extends Loggable {
333
348
  }
334
349
 
335
350
  if (choice < newBound && this.newQ.length) {
336
- this._currentCard = this.nextNewCard();
351
+ return this.newQ.peek(0);
337
352
  } else if (choice < reviewBound && this.reviewQ.length) {
338
- this._currentCard = this.reviewQ.dequeue();
353
+ return this.reviewQ.peek(0);
339
354
  } else if (this.failedQ.length) {
340
- this._currentCard = this.failedQ.dequeue();
355
+ return this.failedQ.peek(0);
341
356
  } else {
342
357
  this.log(`No more cards available for the session!`);
343
- this._currentCard = null;
358
+ return null;
359
+ }
360
+ }
361
+
362
+ public async nextCard(
363
+ action:
364
+ | 'dismiss-success'
365
+ | 'dismiss-failed'
366
+ | 'marked-failed'
367
+ | 'dismiss-error' = 'dismiss-success'
368
+ ): Promise<HydratedCard<TView> | null> {
369
+ // dismiss (or sort to failedQ) the current card
370
+ this.dismissCurrentCard(action);
371
+
372
+ let card = this.hydratedQ.dequeue();
373
+
374
+ // If no hydrated card but source cards available, wait for hydration
375
+ if (!card && this.hasAvailableCards()) {
376
+ void this._fillHydratedQueue(); // Start hydration in background
377
+ card = await this.nextHydratedCard(); // Wait for first available card
378
+ }
379
+
380
+ // Trigger background hydration to maintain cache (async, non-blocking)
381
+ if (this.hydratedQ.length < 3) {
382
+ void this._fillHydratedQueue();
344
383
  }
345
384
 
346
- return this._currentCard;
385
+ return card;
347
386
  }
348
387
 
349
388
  private dismissCurrentCard(
@@ -373,7 +412,6 @@ export class SessionController extends Loggable {
373
412
  failedItem = {
374
413
  cardID: this._currentCard.cardID,
375
414
  courseID: this._currentCard.courseID,
376
- qualifiedID: this._currentCard.qualifiedID,
377
415
  contentSourceID: this._currentCard.contentSourceID,
378
416
  contentSourceType: this._currentCard.contentSourceType,
379
417
  status: 'failed-review',
@@ -383,14 +421,13 @@ export class SessionController extends Loggable {
383
421
  failedItem = {
384
422
  cardID: this._currentCard.cardID,
385
423
  courseID: this._currentCard.courseID,
386
- qualifiedID: this._currentCard.qualifiedID,
387
424
  contentSourceID: this._currentCard.contentSourceID,
388
425
  contentSourceType: this._currentCard.contentSourceType,
389
426
  status: 'failed-new',
390
427
  };
391
428
  }
392
429
 
393
- this.failedQ.add(failedItem);
430
+ this.failedQ.add(failedItem, failedItem.cardID);
394
431
  } else if (action === 'dismiss-error') {
395
432
  // some error logging?
396
433
  } else if (action === 'dismiss-failed') {
@@ -398,4 +435,84 @@ export class SessionController extends Loggable {
398
435
  }
399
436
  }
400
437
  }
438
+
439
+ private hasAvailableCards(): boolean {
440
+ return this.reviewQ.length > 0 || this.newQ.length > 0 || this.failedQ.length > 0;
441
+ }
442
+
443
+ private async nextHydratedCard(): Promise<HydratedCard<TView> | null> {
444
+ // Wait for a card to become available in hydratedQ
445
+ while (this.hydratedQ.length === 0 && this.hasAvailableCards()) {
446
+ await new Promise((resolve) => setTimeout(resolve, 25)); // Short polling interval
447
+ }
448
+ return this.hydratedQ.dequeue();
449
+ }
450
+
451
+ private async _fillHydratedQueue() {
452
+ if (this.hydration_in_progress) {
453
+ return; // Prevent concurrent hydration
454
+ }
455
+
456
+ const BUFFER_SIZE = 5;
457
+ this.hydration_in_progress = true;
458
+
459
+ while (this.hydratedQ.length < BUFFER_SIZE) {
460
+ const nextItem = this._selectNextItemToHydrate();
461
+ if (!nextItem) {
462
+ return; // No more cards to hydrate
463
+ }
464
+
465
+ try {
466
+ const cardData = await this.dataLayer
467
+ .getCourseDB(nextItem.courseID)
468
+ .getCourseDoc<CardData>(nextItem.cardID);
469
+
470
+ if (!isCourseElo(cardData.elo)) {
471
+ cardData.elo = toCourseElo(cardData.elo);
472
+ }
473
+
474
+ const view = this.getViewComponent(cardData.id_view);
475
+ const dataDocs = await Promise.all(
476
+ cardData.id_displayable_data.map((id: string) =>
477
+ this.dataLayer.getCourseDB(nextItem.courseID).getCourseDoc<DisplayableData>(id, {
478
+ attachments: true,
479
+ binary: true,
480
+ })
481
+ )
482
+ );
483
+
484
+ const data = dataDocs.map(displayableDataToViewData).reverse();
485
+
486
+ this.hydratedQ.add(
487
+ {
488
+ item: nextItem,
489
+ view,
490
+ data,
491
+ },
492
+ nextItem.cardID
493
+ );
494
+
495
+ // Remove the item from the original queue
496
+ if (this.reviewQ.peek(0) === nextItem) {
497
+ this.reviewQ.dequeue();
498
+ } else if (this.newQ.peek(0) === nextItem) {
499
+ this.newQ.dequeue();
500
+ } else {
501
+ this.failedQ.dequeue();
502
+ }
503
+ } catch (e) {
504
+ this.error(`Error hydrating card ${nextItem.cardID}:`, e);
505
+ // Remove the failed item from the queue
506
+ if (this.reviewQ.peek(0) === nextItem) {
507
+ this.reviewQ.dequeue();
508
+ } else if (this.newQ.peek(0) === nextItem) {
509
+ this.newQ.dequeue();
510
+ } else {
511
+ this.failedQ.dequeue();
512
+ }
513
+ }
514
+ }
515
+
516
+ this.hydration_in_progress = false;
517
+ }
401
518
  }
@@ -39,7 +39,7 @@ function newQuestionInterval(user: DocumentUpdater, cardHistory: CardHistory<Que
39
39
  }
40
40
 
41
41
  if (currentAttempt.isCorrect) {
42
- const skill = currentAttempt.performance as number;
42
+ const skill = Math.min(1.0, Math.max(0.0, currentAttempt.performance as number));
43
43
  logger.debug(`Demontrated skill: \t${skill}`);
44
44
  const interval: number = lastInterval * (0.75 + skill);
45
45
  cardHistory.lapses = getLapses(cardHistory.records);
package/tsup.config.ts CHANGED
@@ -4,6 +4,7 @@ export default defineConfig({
4
4
  entry: [
5
5
  'src/index.ts',
6
6
  'src/core/index.ts',
7
+ 'src/pouch/index.ts',
7
8
  'src/impl/couch/index.ts',
8
9
  'src/impl/static/index.ts',
9
10
  'src/util/packer/index.ts',