@vue-skuilder/db 0.2.0 → 0.2.2

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.
@@ -279,8 +279,40 @@ declare class CourseDB implements CourseDBInterface {
279
279
  * @returns Cards sorted by score descending
280
280
  */
281
281
  private _pendingHints;
282
+ /**
283
+ * Session-scoped cache of the broad ELO-neighbor pool used by
284
+ * getCardsCenteredAtELO. The `elo` view query re-indexes on first touch per
285
+ * call (PouchDB 9 ignores `stale`), so without this each plan/replan pays
286
+ * ~1.5-2s. The pool is fetched once and re-ranked against the live (roaming)
287
+ * ELO in memory on subsequent calls.
288
+ */
289
+ private _eloPoolCache;
290
+ private readonly _eloPoolTtlMs;
291
+ /**
292
+ * Cached assembled navigator (Pipeline). createNavigator() reads strategy
293
+ * docs and builds a fresh Pipeline every call — whose internal `_tagCache`
294
+ * and `_cachedOrchestration` are designed to make replans cheap but never
295
+ * survive, because the instance is discarded each run. Caching it lets those
296
+ * caches persist across plan/replan within a session (SessionController holds
297
+ * one CourseDB instance for the session's lifetime). Rebuilt on user change,
298
+ * TTL expiry, or explicit invalidation after a strategy-doc write.
299
+ */
300
+ private _cachedNavigator;
301
+ private readonly _navigatorTtlMs;
282
302
  setEphemeralHints(hints: ReplanHints): void;
283
303
  getWeightedCards(limit: number): Promise<GeneratorResult>;
304
+ /**
305
+ * Return the assembled navigator, reusing the cached instance when possible.
306
+ * Reuse preserves the Pipeline's per-session caches (tags, orchestration
307
+ * context) across replans, which is the dominant per-replan cost once the
308
+ * ELO-pool cost is removed. Rebuilds on user change or TTL expiry.
309
+ */
310
+ private _getCachedNavigator;
311
+ /**
312
+ * Drop the cached navigator so the next getWeightedCards() rebuilds it.
313
+ * Call after mutating this course's navigation strategy documents.
314
+ */
315
+ invalidateNavigatorCache(): void;
284
316
  getCardsCenteredAtELO(options?: {
285
317
  limit: number;
286
318
  elo: 'user' | 'random' | number;
@@ -279,8 +279,40 @@ declare class CourseDB implements CourseDBInterface {
279
279
  * @returns Cards sorted by score descending
280
280
  */
281
281
  private _pendingHints;
282
+ /**
283
+ * Session-scoped cache of the broad ELO-neighbor pool used by
284
+ * getCardsCenteredAtELO. The `elo` view query re-indexes on first touch per
285
+ * call (PouchDB 9 ignores `stale`), so without this each plan/replan pays
286
+ * ~1.5-2s. The pool is fetched once and re-ranked against the live (roaming)
287
+ * ELO in memory on subsequent calls.
288
+ */
289
+ private _eloPoolCache;
290
+ private readonly _eloPoolTtlMs;
291
+ /**
292
+ * Cached assembled navigator (Pipeline). createNavigator() reads strategy
293
+ * docs and builds a fresh Pipeline every call — whose internal `_tagCache`
294
+ * and `_cachedOrchestration` are designed to make replans cheap but never
295
+ * survive, because the instance is discarded each run. Caching it lets those
296
+ * caches persist across plan/replan within a session (SessionController holds
297
+ * one CourseDB instance for the session's lifetime). Rebuilt on user change,
298
+ * TTL expiry, or explicit invalidation after a strategy-doc write.
299
+ */
300
+ private _cachedNavigator;
301
+ private readonly _navigatorTtlMs;
282
302
  setEphemeralHints(hints: ReplanHints): void;
283
303
  getWeightedCards(limit: number): Promise<GeneratorResult>;
304
+ /**
305
+ * Return the assembled navigator, reusing the cached instance when possible.
306
+ * Reuse preserves the Pipeline's per-session caches (tags, orchestration
307
+ * context) across replans, which is the dominant per-replan cost once the
308
+ * ELO-pool cost is removed. Rebuilds on user change or TTL expiry.
309
+ */
310
+ private _getCachedNavigator;
311
+ /**
312
+ * Drop the cached navigator so the next getWeightedCards() rebuilds it.
313
+ * Call after mutating this course's navigation strategy documents.
314
+ */
315
+ invalidateNavigatorCache(): void;
284
316
  getCardsCenteredAtELO(options?: {
285
317
  limit: number;
286
318
  elo: 'user' | 'random' | number;
@@ -1753,10 +1753,8 @@ var init_elo = __esm({
1753
1753
  { limit, elo: "user" },
1754
1754
  (c) => !activeCards.some((ac) => c.cardID === ac.cardID)
1755
1755
  )).map((c) => ({ ...c, status: "new" }));
1756
- const cardIds = newCards.map((c) => c.cardID);
1757
- const cardEloData = await this.course.getCardEloData(cardIds);
1758
- const scored = newCards.map((c, i) => {
1759
- const cardElo = cardEloData[i]?.global?.score ?? 1e3;
1756
+ const scored = newCards.map((c) => {
1757
+ const cardElo = c.elo ?? 1e3;
1760
1758
  const distance = Math.abs(cardElo - userGlobalElo);
1761
1759
  const rawScore = Math.max(0, 1 - distance / 500);
1762
1760
  const samplingKey = rawScore > 0 ? Math.random() ** (1 / rawScore) : 0;
@@ -5468,6 +5466,7 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
5468
5466
  }
5469
5467
  async addNavigationStrategy(data) {
5470
5468
  logger.debug(`[courseDB] Adding navigation strategy: ${data._id}`);
5469
+ this.invalidateNavigatorCache();
5471
5470
  return this.remoteDB.put(data).then(() => {
5472
5471
  });
5473
5472
  }
@@ -5534,23 +5533,67 @@ ${e.stack}` : JSON.stringify(e);
5534
5533
  * @returns Cards sorted by score descending
5535
5534
  */
5536
5535
  _pendingHints = null;
5536
+ /**
5537
+ * Session-scoped cache of the broad ELO-neighbor pool used by
5538
+ * getCardsCenteredAtELO. The `elo` view query re-indexes on first touch per
5539
+ * call (PouchDB 9 ignores `stale`), so without this each plan/replan pays
5540
+ * ~1.5-2s. The pool is fetched once and re-ranked against the live (roaming)
5541
+ * ELO in memory on subsequent calls.
5542
+ */
5543
+ _eloPoolCache = null;
5544
+ _eloPoolTtlMs = 5 * 60 * 1e3;
5545
+ /**
5546
+ * Cached assembled navigator (Pipeline). createNavigator() reads strategy
5547
+ * docs and builds a fresh Pipeline every call — whose internal `_tagCache`
5548
+ * and `_cachedOrchestration` are designed to make replans cheap but never
5549
+ * survive, because the instance is discarded each run. Caching it lets those
5550
+ * caches persist across plan/replan within a session (SessionController holds
5551
+ * one CourseDB instance for the session's lifetime). Rebuilt on user change,
5552
+ * TTL expiry, or explicit invalidation after a strategy-doc write.
5553
+ */
5554
+ _cachedNavigator = null;
5555
+ _navigatorTtlMs = 5 * 60 * 1e3;
5537
5556
  setEphemeralHints(hints) {
5538
5557
  this._pendingHints = hints;
5539
5558
  }
5540
5559
  async getWeightedCards(limit) {
5541
5560
  const u = await this._getCurrentUser();
5542
5561
  try {
5543
- const navigator2 = await this.createNavigator(u);
5562
+ const { navigator: navigator2 } = await this._getCachedNavigator(u);
5544
5563
  if (this._pendingHints) {
5545
5564
  navigator2.setEphemeralHints(this._pendingHints);
5546
5565
  this._pendingHints = null;
5547
5566
  }
5548
- return navigator2.getWeightedCards(limit);
5567
+ const result = await navigator2.getWeightedCards(limit);
5568
+ return result;
5549
5569
  } catch (e) {
5550
5570
  logger.error(`[courseDB] Error getting weighted cards: ${e}`);
5551
5571
  throw e;
5552
5572
  }
5553
5573
  }
5574
+ /**
5575
+ * Return the assembled navigator, reusing the cached instance when possible.
5576
+ * Reuse preserves the Pipeline's per-session caches (tags, orchestration
5577
+ * context) across replans, which is the dominant per-replan cost once the
5578
+ * ELO-pool cost is removed. Rebuilds on user change or TTL expiry.
5579
+ */
5580
+ async _getCachedNavigator(user) {
5581
+ const userId = user.getUsername();
5582
+ const now = Date.now();
5583
+ if (this._cachedNavigator && this._cachedNavigator.userId === userId && now - this._cachedNavigator.builtAt < this._navigatorTtlMs) {
5584
+ return { navigator: this._cachedNavigator.navigator, cacheStatus: "hit" };
5585
+ }
5586
+ const navigator2 = await this.createNavigator(user);
5587
+ this._cachedNavigator = { navigator: navigator2, userId, builtAt: now };
5588
+ return { navigator: navigator2, cacheStatus: "miss" };
5589
+ }
5590
+ /**
5591
+ * Drop the cached navigator so the next getWeightedCards() rebuilds it.
5592
+ * Call after mutating this course's navigation strategy documents.
5593
+ */
5594
+ invalidateNavigatorCache() {
5595
+ this._cachedNavigator = null;
5596
+ }
5554
5597
  async getCardsCenteredAtELO(options = {
5555
5598
  limit: 99,
5556
5599
  elo: "user"
@@ -5573,20 +5616,31 @@ ${e.stack}` : JSON.stringify(e);
5573
5616
  } else {
5574
5617
  targetElo = options.elo;
5575
5618
  }
5576
- let cards = [];
5577
- let mult = 4;
5578
- let previousCount = -1;
5579
- let newCount = 0;
5580
- while (cards.length < options.limit && newCount !== previousCount) {
5581
- cards = await this.getCardsByELO(targetElo, mult * options.limit);
5582
- previousCount = newCount;
5583
- newCount = cards.length;
5584
- logger.debug(`Found ${cards.length} elo neighbor cards...`);
5585
- if (filter) {
5586
- cards = cards.filter(filter);
5587
- logger.debug(`Filtered to ${cards.length} cards...`);
5588
- }
5589
- mult *= 2;
5619
+ const POOL_SIZE = Math.max(2e3, options.limit * 4);
5620
+ const nowMs = Date.now();
5621
+ let cacheStatus = "hit";
5622
+ if (!this._eloPoolCache || nowMs - this._eloPoolCache.fetchedAt > this._eloPoolTtlMs) {
5623
+ const fetched = await this.getCardsByELO(targetElo, POOL_SIZE);
5624
+ if (fetched.length > 0) {
5625
+ this._eloPoolCache = { rows: fetched, fetchedAt: nowMs };
5626
+ }
5627
+ cacheStatus = "miss";
5628
+ }
5629
+ const rankAgainstCurrentElo = () => {
5630
+ const raw = this._eloPoolCache?.rows ?? [];
5631
+ const survivors = filter ? raw.filter((c) => filter(c)) : raw;
5632
+ return survivors.map((c) => ({ ...c })).sort(
5633
+ (a, b) => Math.abs((a.elo ?? targetElo) - targetElo) - Math.abs((b.elo ?? targetElo) - targetElo)
5634
+ );
5635
+ };
5636
+ let cards = rankAgainstCurrentElo();
5637
+ if (cards.length < options.limit && (cacheStatus === "hit" || !this._eloPoolCache)) {
5638
+ const fetched = await this.getCardsByELO(targetElo, POOL_SIZE);
5639
+ if (fetched.length > 0) {
5640
+ this._eloPoolCache = { rows: fetched, fetchedAt: nowMs };
5641
+ }
5642
+ cards = rankAgainstCurrentElo();
5643
+ cacheStatus = "refresh";
5590
5644
  }
5591
5645
  const selectedCards = [];
5592
5646
  while (selectedCards.length < options.limit && cards.length > 0) {
@@ -6441,9 +6495,14 @@ var init_UserDBDebugger = __esm({
6441
6495
  */
6442
6496
  async showScheduledReviews(courseId) {
6443
6497
  const userDB = getUserDB();
6444
- if (!userDB) return;
6498
+ if (!userDB) {
6499
+ logger.info("[UserDB Debug] Data layer not available");
6500
+ return;
6501
+ }
6502
+ logger.info(`[UserDB Debug] Fetching pending reviews${courseId ? ` for course: ${courseId}` : ""}...`);
6445
6503
  try {
6446
6504
  const reviews = await userDB.getPendingReviews(courseId);
6505
+ logger.info(`[UserDB Debug] Got ${reviews.length} reviews`);
6447
6506
  console.group(`\u{1F4C5} Scheduled Reviews${courseId ? ` (${courseId})` : ""}`);
6448
6507
  logger.info(`Total: ${reviews.length}`);
6449
6508
  if (reviews.length > 0) {