@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.
@@ -1806,10 +1806,8 @@ var init_elo = __esm({
1806
1806
  { limit, elo: "user" },
1807
1807
  (c) => !activeCards.some((ac) => c.cardID === ac.cardID)
1808
1808
  )).map((c) => ({ ...c, status: "new" }));
1809
- const cardIds = newCards.map((c) => c.cardID);
1810
- const cardEloData = await this.course.getCardEloData(cardIds);
1811
- const scored = newCards.map((c, i) => {
1812
- const cardElo = cardEloData[i]?.global?.score ?? 1e3;
1809
+ const scored = newCards.map((c) => {
1810
+ const cardElo = c.elo ?? 1e3;
1813
1811
  const distance = Math.abs(cardElo - userGlobalElo);
1814
1812
  const rawScore = Math.max(0, 1 - distance / 500);
1815
1813
  const samplingKey = rawScore > 0 ? Math.random() ** (1 / rawScore) : 0;
@@ -5685,6 +5683,7 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
5685
5683
  }
5686
5684
  async addNavigationStrategy(data) {
5687
5685
  logger.debug(`[courseDB] Adding navigation strategy: ${data._id}`);
5686
+ this.invalidateNavigatorCache();
5688
5687
  return this.remoteDB.put(data).then(() => {
5689
5688
  });
5690
5689
  }
@@ -5751,23 +5750,67 @@ ${e.stack}` : JSON.stringify(e);
5751
5750
  * @returns Cards sorted by score descending
5752
5751
  */
5753
5752
  _pendingHints = null;
5753
+ /**
5754
+ * Session-scoped cache of the broad ELO-neighbor pool used by
5755
+ * getCardsCenteredAtELO. The `elo` view query re-indexes on first touch per
5756
+ * call (PouchDB 9 ignores `stale`), so without this each plan/replan pays
5757
+ * ~1.5-2s. The pool is fetched once and re-ranked against the live (roaming)
5758
+ * ELO in memory on subsequent calls.
5759
+ */
5760
+ _eloPoolCache = null;
5761
+ _eloPoolTtlMs = 5 * 60 * 1e3;
5762
+ /**
5763
+ * Cached assembled navigator (Pipeline). createNavigator() reads strategy
5764
+ * docs and builds a fresh Pipeline every call — whose internal `_tagCache`
5765
+ * and `_cachedOrchestration` are designed to make replans cheap but never
5766
+ * survive, because the instance is discarded each run. Caching it lets those
5767
+ * caches persist across plan/replan within a session (SessionController holds
5768
+ * one CourseDB instance for the session's lifetime). Rebuilt on user change,
5769
+ * TTL expiry, or explicit invalidation after a strategy-doc write.
5770
+ */
5771
+ _cachedNavigator = null;
5772
+ _navigatorTtlMs = 5 * 60 * 1e3;
5754
5773
  setEphemeralHints(hints) {
5755
5774
  this._pendingHints = hints;
5756
5775
  }
5757
5776
  async getWeightedCards(limit) {
5758
5777
  const u = await this._getCurrentUser();
5759
5778
  try {
5760
- const navigator2 = await this.createNavigator(u);
5779
+ const { navigator: navigator2 } = await this._getCachedNavigator(u);
5761
5780
  if (this._pendingHints) {
5762
5781
  navigator2.setEphemeralHints(this._pendingHints);
5763
5782
  this._pendingHints = null;
5764
5783
  }
5765
- return navigator2.getWeightedCards(limit);
5784
+ const result = await navigator2.getWeightedCards(limit);
5785
+ return result;
5766
5786
  } catch (e) {
5767
5787
  logger.error(`[courseDB] Error getting weighted cards: ${e}`);
5768
5788
  throw e;
5769
5789
  }
5770
5790
  }
5791
+ /**
5792
+ * Return the assembled navigator, reusing the cached instance when possible.
5793
+ * Reuse preserves the Pipeline's per-session caches (tags, orchestration
5794
+ * context) across replans, which is the dominant per-replan cost once the
5795
+ * ELO-pool cost is removed. Rebuilds on user change or TTL expiry.
5796
+ */
5797
+ async _getCachedNavigator(user) {
5798
+ const userId = user.getUsername();
5799
+ const now = Date.now();
5800
+ if (this._cachedNavigator && this._cachedNavigator.userId === userId && now - this._cachedNavigator.builtAt < this._navigatorTtlMs) {
5801
+ return { navigator: this._cachedNavigator.navigator, cacheStatus: "hit" };
5802
+ }
5803
+ const navigator2 = await this.createNavigator(user);
5804
+ this._cachedNavigator = { navigator: navigator2, userId, builtAt: now };
5805
+ return { navigator: navigator2, cacheStatus: "miss" };
5806
+ }
5807
+ /**
5808
+ * Drop the cached navigator so the next getWeightedCards() rebuilds it.
5809
+ * Call after mutating this course's navigation strategy documents.
5810
+ */
5811
+ invalidateNavigatorCache() {
5812
+ this._cachedNavigator = null;
5813
+ }
5771
5814
  async getCardsCenteredAtELO(options = {
5772
5815
  limit: 99,
5773
5816
  elo: "user"
@@ -5790,20 +5833,31 @@ ${e.stack}` : JSON.stringify(e);
5790
5833
  } else {
5791
5834
  targetElo = options.elo;
5792
5835
  }
5793
- let cards = [];
5794
- let mult = 4;
5795
- let previousCount = -1;
5796
- let newCount = 0;
5797
- while (cards.length < options.limit && newCount !== previousCount) {
5798
- cards = await this.getCardsByELO(targetElo, mult * options.limit);
5799
- previousCount = newCount;
5800
- newCount = cards.length;
5801
- logger.debug(`Found ${cards.length} elo neighbor cards...`);
5802
- if (filter) {
5803
- cards = cards.filter(filter);
5804
- logger.debug(`Filtered to ${cards.length} cards...`);
5805
- }
5806
- mult *= 2;
5836
+ const POOL_SIZE = Math.max(2e3, options.limit * 4);
5837
+ const nowMs = Date.now();
5838
+ let cacheStatus = "hit";
5839
+ if (!this._eloPoolCache || nowMs - this._eloPoolCache.fetchedAt > this._eloPoolTtlMs) {
5840
+ const fetched = await this.getCardsByELO(targetElo, POOL_SIZE);
5841
+ if (fetched.length > 0) {
5842
+ this._eloPoolCache = { rows: fetched, fetchedAt: nowMs };
5843
+ }
5844
+ cacheStatus = "miss";
5845
+ }
5846
+ const rankAgainstCurrentElo = () => {
5847
+ const raw = this._eloPoolCache?.rows ?? [];
5848
+ const survivors = filter ? raw.filter((c) => filter(c)) : raw;
5849
+ return survivors.map((c) => ({ ...c })).sort(
5850
+ (a, b) => Math.abs((a.elo ?? targetElo) - targetElo) - Math.abs((b.elo ?? targetElo) - targetElo)
5851
+ );
5852
+ };
5853
+ let cards = rankAgainstCurrentElo();
5854
+ if (cards.length < options.limit && (cacheStatus === "hit" || !this._eloPoolCache)) {
5855
+ const fetched = await this.getCardsByELO(targetElo, POOL_SIZE);
5856
+ if (fetched.length > 0) {
5857
+ this._eloPoolCache = { rows: fetched, fetchedAt: nowMs };
5858
+ }
5859
+ cards = rankAgainstCurrentElo();
5860
+ cacheStatus = "refresh";
5807
5861
  }
5808
5862
  const selectedCards = [];
5809
5863
  while (selectedCards.length < options.limit && cards.length > 0) {
@@ -7685,9 +7739,14 @@ var init_UserDBDebugger = __esm({
7685
7739
  */
7686
7740
  async showScheduledReviews(courseId) {
7687
7741
  const userDB = getUserDB();
7688
- if (!userDB) return;
7742
+ if (!userDB) {
7743
+ logger.info("[UserDB Debug] Data layer not available");
7744
+ return;
7745
+ }
7746
+ logger.info(`[UserDB Debug] Fetching pending reviews${courseId ? ` for course: ${courseId}` : ""}...`);
7689
7747
  try {
7690
7748
  const reviews = await userDB.getPendingReviews(courseId);
7749
+ logger.info(`[UserDB Debug] Got ${reviews.length} reviews`);
7691
7750
  console.group(`\u{1F4C5} Scheduled Reviews${courseId ? ` (${courseId})` : ""}`);
7692
7751
  logger.info(`Total: ${reviews.length}`);
7693
7752
  if (reviews.length > 0) {