@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.
- package/dist/core/index.d.mts +5 -5
- package/dist/core/index.d.ts +5 -5
- package/dist/core/index.js +212 -50
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +212 -50
- package/dist/core/index.mjs.map +1 -1
- package/dist/{dataLayerProvider-DqtNroSh.d.ts → dataLayerProvider-VieuAAkV.d.mts} +8 -1
- package/dist/{dataLayerProvider-BInqI_RF.d.mts → dataLayerProvider-juuqUHOP.d.ts} +8 -1
- package/dist/impl/couch/index.d.mts +19 -7
- package/dist/impl/couch/index.d.ts +19 -7
- package/dist/impl/couch/index.js +229 -63
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +228 -62
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.d.mts +13 -6
- package/dist/impl/static/index.d.ts +13 -6
- package/dist/impl/static/index.js +142 -46
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +142 -46
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/{index-CLL31bEy.d.ts → index-CWY6yhkV.d.ts} +1 -1
- package/dist/{index-CUNnL38E.d.mts → index-DZyxHCcf.d.mts} +1 -1
- package/dist/index.d.mts +28 -20
- package/dist/index.d.ts +28 -20
- package/dist/index.js +374 -108
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +378 -108
- package/dist/index.mjs.map +1 -1
- package/dist/pouch/index.d.mts +1 -0
- package/dist/pouch/index.d.ts +1 -0
- package/dist/pouch/index.js +49 -0
- package/dist/pouch/index.js.map +1 -0
- package/dist/pouch/index.mjs +16 -0
- package/dist/pouch/index.mjs.map +1 -0
- package/dist/{types-DC-ckZug.d.mts → types-Che4wTwA.d.mts} +1 -1
- package/dist/{types-BefDGkKa.d.ts → types-DtoI27Xh.d.ts} +1 -1
- package/dist/{types-legacy-Birv-Jx6.d.mts → types-legacy-B8ahaCbj.d.mts} +5 -1
- package/dist/{types-legacy-Birv-Jx6.d.ts → types-legacy-B8ahaCbj.d.ts} +5 -1
- package/dist/{userDB-C33Hzjgn.d.mts → userDB-B7zTQ123.d.ts} +88 -55
- package/dist/{userDB-DusL7OXe.d.ts → userDB-DJ8HMw83.d.mts} +88 -55
- package/dist/util/packer/index.d.mts +3 -3
- package/dist/util/packer/index.d.ts +3 -3
- package/package.json +3 -3
- package/src/core/interfaces/contentSource.ts +3 -2
- package/src/core/interfaces/courseDB.ts +26 -3
- package/src/core/interfaces/dataLayerProvider.ts +9 -1
- package/src/core/interfaces/userDB.ts +80 -64
- package/src/core/navigators/elo.ts +10 -7
- package/src/core/navigators/index.ts +1 -1
- package/src/core/types/types-legacy.ts +5 -0
- package/src/impl/common/BaseUserDB.ts +45 -3
- package/src/impl/couch/CouchDBSyncStrategy.ts +2 -2
- package/src/impl/couch/PouchDataLayerProvider.ts +21 -0
- package/src/impl/couch/adminDB.ts +2 -2
- package/src/impl/couch/auth.ts +13 -4
- package/src/impl/couch/classroomDB.ts +10 -12
- package/src/impl/couch/courseAPI.ts +2 -2
- package/src/impl/couch/courseDB.ts +130 -11
- package/src/impl/couch/courseLookupDB.ts +4 -3
- package/src/impl/couch/index.ts +36 -4
- package/src/impl/couch/pouchdb-setup.ts +3 -3
- package/src/impl/couch/updateQueue.ts +51 -33
- package/src/impl/static/StaticDataLayerProvider.ts +11 -0
- package/src/impl/static/courseDB.ts +47 -8
- package/src/pouch/index.ts +2 -0
- package/src/study/SessionController.ts +168 -51
- package/src/study/SpacedRepetition.ts +1 -1
- 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
|
-
|
|
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 ===
|
|
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(
|
|
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` +
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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
|
-
|
|
225
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
289
|
-
|
|
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
|
-
|
|
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
|
-
|
|
351
|
+
return this.newQ.peek(0);
|
|
337
352
|
} else if (choice < reviewBound && this.reviewQ.length) {
|
|
338
|
-
|
|
353
|
+
return this.reviewQ.peek(0);
|
|
339
354
|
} else if (this.failedQ.length) {
|
|
340
|
-
|
|
355
|
+
return this.failedQ.peek(0);
|
|
341
356
|
} else {
|
|
342
357
|
this.log(`No more cards available for the session!`);
|
|
343
|
-
|
|
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
|
|
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);
|