Skip to content

Commit 1f54475

Browse files
committed
migrate nextCard selection to SessionController...
and implement hydration logic (again, migrated from StudySession.vue)
1 parent 351672b commit 1f54475

File tree

3 files changed

+131
-51
lines changed

3 files changed

+131
-51
lines changed

agent/nextCard-perf/a.4.todo.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,18 @@ This file breaks down the tasks required to implement the client-side pre-fetchi
1010

1111
## Phase 2: Pre-fetching Logic
1212

13-
- [ ] **Task 2.1:** Inside `_fillHydratedQueue()`, implement the logic to determine how many cards to fetch (target buffer size is 5).
14-
- [ ] **Task 2.2:** Inside `_fillHydratedQueue()`, peek at the next items in `reviewQ` and `newQ` to get their card IDs.
15-
- [ ] **Task 2.3:** For each card ID, implement the necessary calls to fetch the full card document and its associated data document from CouchDB.
16-
- [ ] **Task 2.4:** Once fetched, create `HydratedCard` objects and enqueue them into `hydratedQ`.
13+
- [x] **Task 2.1:** Inside `_fillHydratedQueue()`, implement the logic to determine how many cards to fetch (target buffer size is 5).
14+
- [x] **Task 2.2:** Inside `_fillHydratedQueue()`, peek at the next items in `reviewQ` and `newQ` to get their card IDs.
15+
- [x] **Task 2.3:** For each card ID, implement the necessary calls to fetch the full card document and its associated data document from CouchDB.
16+
- [x] **Task 2.4:** Once fetched, create `HydratedCard` objects and enqueue them into `hydratedQ`.
17+
18+
> **Note:** The data fetching and hydration logic implemented in this phase was adapted directly from the `loadCard()` method in `packages/common-ui/src/components/StudySession.vue`.
1719
1820
## Phase 3: Update `nextCard()` and `prepareSession()`
1921

20-
- [ ] **Task 3.1:** Modify the `nextCard()` function to dequeue from `hydratedQ` instead of the other queues.
21-
- [ ] **Task 3.2:** After dequeuing in `nextCard()`, add a non-blocking call to `_fillHydratedQueue()` to trigger the background pre-fetch.
22-
- [ ] **Task 3.3:** In `prepareSession()`, after the `reviewQ` and `newQ` are populated, add an initial `await this._fillHydratedQueue()` call to ensure the buffer is ready for the first card.
22+
- [x] **Task 3.1:** Modify the `nextCard()` function to dequeue from `hydratedQ` instead of the other queues.
23+
- [x] **Task 3.2:** After dequeuing in `nextCard()`, add a non-blocking call to `_fillHydratedQueue()` to trigger the background pre-fetch.
24+
- [x] **Task 3.3:** In `prepareSession()`, after the `reviewQ` and `newQ` are populated, add an initial `await this._fillHydratedQueue()` call to ensure the buffer is ready for the first card.
2325

2426
## Phase 4: UI and Verification
2527

packages/common-ui/src/components/StudySession.vue

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,14 @@ export default defineComponent({
310310
// db.setChangeFcn(this.handleClassroomMessage());
311311
});
312312
313-
this.sessionController = markRaw(new SessionController(this.sessionContentSources, 60 * this.sessionTimeLimit));
313+
this.sessionController = markRaw(
314+
new SessionController(
315+
this.sessionContentSources,
316+
60 * this.sessionTimeLimit,
317+
this.dataLayer,
318+
this.getViewComponent
319+
)
320+
);
314321
this.sessionController.sessionRecord = this.sessionRecord;
315322
316323
await this.sessionController.prepareSession();

packages/db/src/study/SessionController.ts

Lines changed: 114 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,14 @@ export interface HydratedCard {
3333
data: ViewData[];
3434
}
3535

36+
import {
37+
CardData,
38+
DisplayableData,
39+
displayableDataToViewData,
40+
isCourseElo,
41+
toCourseElo,
42+
} from '@vue-skuilder/common';
43+
3644
class ItemQueue<T> {
3745
private q: T[] = [];
3846
private seenCardIds: string[] = [];
@@ -71,14 +79,20 @@ class ItemQueue<T> {
7179
public get toString(): string {
7280
return (
7381
`${typeof this.q[0]}:\n` +
74-
this.q.map((i) => `\t${(i as any).courseID}+${(i as any).cardID}: ${(i as any).status}`).join('\n')
82+
this.q
83+
.map((i) => `\t${(i as any).courseID}+${(i as any).cardID}: ${(i as any).status}`)
84+
.join('\n')
7585
);
7686
}
7787
}
7888

89+
import { DataLayerProvider } from '@db/core';
90+
7991
export class SessionController extends Loggable {
8092
_className = 'SessionController';
8193
private sources: StudyContentSource[];
94+
private dataLayer: DataLayerProvider;
95+
private getViewComponent: (viewId: string) => ViewComponent;
8296
private _sessionRecord: StudySessionRecord[] = [];
8397
public set sessionRecord(r: StudySessionRecord[]) {
8498
this._sessionRecord = r;
@@ -89,11 +103,7 @@ export class SessionController extends Loggable {
89103
private failedQ: ItemQueue<StudySessionFailedItem> = new ItemQueue<StudySessionFailedItem>();
90104
private hydratedQ: ItemQueue<HydratedCard> = new ItemQueue<HydratedCard>();
91105
private _currentCard: StudySessionItem | null = null;
92-
/**
93-
* Indicates whether the session has been initialized - eg, the
94-
* queues have been populated.
95-
*/
96-
private _isInitialized: boolean = false;
106+
97107

98108
private startTime: Date;
99109
private endTime: Date;
@@ -113,13 +123,20 @@ export class SessionController extends Loggable {
113123
/**
114124
*
115125
*/
116-
constructor(sources: StudyContentSource[], time: number) {
126+
constructor(
127+
sources: StudyContentSource[],
128+
time: number,
129+
dataLayer: DataLayerProvider,
130+
getViewComponent: (viewId: string) => ViewComponent
131+
) {
117132
super();
118133

119134
this.sources = sources;
120135
this.startTime = new Date();
121136
this._secondsRemaining = time;
122137
this.endTime = new Date(this.startTime.valueOf() + 1000 * this._secondsRemaining);
138+
this.dataLayer = dataLayer;
139+
this.getViewComponent = getViewComponent;
123140

124141
this.log(`Session constructed:
125142
startTime: ${this.startTime}
@@ -183,7 +200,9 @@ export class SessionController extends Loggable {
183200
this.error('Error preparing study session:', e);
184201
}
185202

186-
this._isInitialized = true;
203+
204+
205+
void this._fillHydratedQueue();
187206

188207
this._intervalHandle = setInterval(() => {
189208
this.tick();
@@ -232,6 +251,7 @@ export class SessionController extends Loggable {
232251

233252
let report = 'Review session created with:\n';
234253
this.reviewQ.addAll(dueCards, (c) => c.cardID);
254+
report += dueCards.map((card) => `Card ${card.courseID}::${card.cardID} `).join('\n');
235255
this.log(report);
236256
}
237257

@@ -258,48 +278,29 @@ export class SessionController extends Loggable {
258278
}
259279
}
260280

261-
private nextNewCard(): StudySessionNewItem | null {
262-
const item = this.newQ.dequeue();
263-
264-
// queue some more content if we are getting low
265-
if (this._isInitialized && this.newQ.length < 5) {
266-
void this.getNewCards();
267-
}
268-
269-
return item;
270-
}
271-
272-
public nextCard(
273-
// [ ] this is often slow. Why?
274-
action:
275-
| 'dismiss-success'
276-
| 'dismiss-failed'
277-
| 'marked-failed'
278-
| 'dismiss-error' = 'dismiss-success'
279-
): StudySessionItem | null {
280-
// dismiss (or sort to failedQ) the current card
281-
this.dismissCurrentCard(action);
281+
282282

283+
private _selectNextItemToHydrate(action: | 'dismiss-success'
284+
| 'dismiss-failed'
285+
| 'marked-failed'
286+
| 'dismiss-error' = 'dismiss-success'): StudySessionItem | null {
283287
const choice = Math.random();
284288
let newBound: number = 0.1;
285289
let reviewBound: number = 0.75;
286290

287291
if (this.reviewQ.length === 0 && this.failedQ.length === 0 && this.newQ.length === 0) {
288292
// all queues empty - session is over (and course is complete?)
289-
this._currentCard = null;
290-
return this._currentCard;
293+
return null;
291294
}
292295

293296
if (this._secondsRemaining < 2 && this.failedQ.length === 0) {
294297
// session is over!
295-
this._currentCard = null;
296-
return this._currentCard;
298+
return null;
297299
}
298300

299301
// supply new cards at start of session
300302
if (this.newQ.dequeueCount < this.sources.length && this.newQ.length) {
301-
this._currentCard = this.nextNewCard();
302-
return this._currentCard;
303+
return this.newQ.peek(0);
303304
}
304305

305306
const cleanupTime = this.estimateCleanupTime();
@@ -340,17 +341,31 @@ export class SessionController extends Loggable {
340341
}
341342

342343
if (choice < newBound && this.newQ.length) {
343-
this._currentCard = this.nextNewCard();
344+
return this.newQ.peek(0);
344345
} else if (choice < reviewBound && this.reviewQ.length) {
345-
this._currentCard = this.reviewQ.dequeue();
346+
return this.reviewQ.peek(0);
346347
} else if (this.failedQ.length) {
347-
this._currentCard = this.failedQ.dequeue();
348+
return this.failedQ.peek(0);
348349
} else {
349350
this.log(`No more cards available for the session!`);
350-
this._currentCard = null;
351+
return null;
351352
}
353+
}
354+
355+
public nextCard(
356+
// [ ] this is often slow. Why?
357+
action:
358+
| 'dismiss-success'
359+
| 'dismiss-failed'
360+
| 'marked-failed'
361+
| 'dismiss-error' = 'dismiss-success'
362+
): HydratedCard | null {
363+
// dismiss (or sort to failedQ) the current card
364+
this.dismissCurrentCard(action);
352365

353-
return this._currentCard;
366+
const card = this.hydratedQ.dequeue();
367+
void this._fillHydratedQueue();
368+
return card;
354369
}
355370

356371
private dismissCurrentCard(
@@ -390,7 +405,7 @@ export class SessionController extends Loggable {
390405
cardID: this._currentCard.cardID,
391406
courseID: this._currentCard.courseID,
392407
contentSourceID: this._currentCard.contentSourceID,
393-
contentSourceType: this._current_card.contentSourceType,
408+
contentSourceType: this._currentCard.contentSourceType,
394409
status: 'failed-new',
395410
};
396411
}
@@ -405,6 +420,62 @@ export class SessionController extends Loggable {
405420
}
406421

407422
private async _fillHydratedQueue() {
408-
// TODO: Implement pre-fetching logic here
423+
const BATCH_SIZE = 5;
424+
while (this.hydratedQ.length < BATCH_SIZE) {
425+
const nextItem = this._selectNextItemToHydrate();
426+
if (!nextItem) {
427+
return; // No more cards to hydrate
428+
}
429+
430+
try {
431+
const cardData = await this.dataLayer
432+
.getCourseDB(nextItem.courseID)
433+
.getCourseDoc<CardData>(nextItem.cardID);
434+
435+
if (!isCourseElo(cardData.elo)) {
436+
cardData.elo = toCourseElo(cardData.elo);
437+
}
438+
439+
const view = this.getViewComponent(cardData.id_view);
440+
const dataDocs = await Promise.all(
441+
cardData.id_displayable_data.map((id: string) =>
442+
this.dataLayer.getCourseDB(nextItem.courseID).getCourseDoc<DisplayableData>(id, {
443+
attachments: true,
444+
binary: true,
445+
})
446+
)
447+
);
448+
449+
const data = dataDocs.map(displayableDataToViewData).reverse();
450+
451+
this.hydratedQ.add(
452+
{
453+
item: nextItem,
454+
view,
455+
data,
456+
},
457+
nextItem.cardID
458+
);
459+
460+
// Remove the item from the original queue
461+
if (this.reviewQ.peek(0) === nextItem) {
462+
this.reviewQ.dequeue();
463+
} else if (this.newQ.peek(0) === nextItem) {
464+
this.newQ.dequeue();
465+
} else {
466+
this.failedQ.dequeue();
467+
}
468+
} catch (e) {
469+
this.error(`Error hydrating card ${nextItem.cardID}:`, e);
470+
// Remove the failed item from the queue
471+
if (this.reviewQ.peek(0) === nextItem) {
472+
this.reviewQ.dequeue();
473+
} else if (this.newQ.peek(0) === nextItem) {
474+
this.newQ.dequeue();
475+
} else {
476+
this.failedQ.dequeue();
477+
}
478+
}
479+
}
409480
}
410-
}
481+
}

0 commit comments

Comments
 (0)