@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.
@@ -1829,10 +1829,8 @@ var init_elo = __esm({
1829
1829
  { limit, elo: "user" },
1830
1830
  (c) => !activeCards.some((ac) => c.cardID === ac.cardID)
1831
1831
  )).map((c) => ({ ...c, status: "new" }));
1832
- const cardIds = newCards.map((c) => c.cardID);
1833
- const cardEloData = await this.course.getCardEloData(cardIds);
1834
- const scored = newCards.map((c, i) => {
1835
- const cardElo = cardEloData[i]?.global?.score ?? 1e3;
1832
+ const scored = newCards.map((c) => {
1833
+ const cardElo = c.elo ?? 1e3;
1836
1834
  const distance = Math.abs(cardElo - userGlobalElo);
1837
1835
  const rawScore = Math.max(0, 1 - distance / 500);
1838
1836
  const samplingKey = rawScore > 0 ? Math.random() ** (1 / rawScore) : 0;
@@ -5703,6 +5701,7 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
5703
5701
  }
5704
5702
  async addNavigationStrategy(data) {
5705
5703
  logger.debug(`[courseDB] Adding navigation strategy: ${data._id}`);
5704
+ this.invalidateNavigatorCache();
5706
5705
  return this.remoteDB.put(data).then(() => {
5707
5706
  });
5708
5707
  }
@@ -5769,23 +5768,67 @@ ${e.stack}` : JSON.stringify(e);
5769
5768
  * @returns Cards sorted by score descending
5770
5769
  */
5771
5770
  _pendingHints = null;
5771
+ /**
5772
+ * Session-scoped cache of the broad ELO-neighbor pool used by
5773
+ * getCardsCenteredAtELO. The `elo` view query re-indexes on first touch per
5774
+ * call (PouchDB 9 ignores `stale`), so without this each plan/replan pays
5775
+ * ~1.5-2s. The pool is fetched once and re-ranked against the live (roaming)
5776
+ * ELO in memory on subsequent calls.
5777
+ */
5778
+ _eloPoolCache = null;
5779
+ _eloPoolTtlMs = 5 * 60 * 1e3;
5780
+ /**
5781
+ * Cached assembled navigator (Pipeline). createNavigator() reads strategy
5782
+ * docs and builds a fresh Pipeline every call — whose internal `_tagCache`
5783
+ * and `_cachedOrchestration` are designed to make replans cheap but never
5784
+ * survive, because the instance is discarded each run. Caching it lets those
5785
+ * caches persist across plan/replan within a session (SessionController holds
5786
+ * one CourseDB instance for the session's lifetime). Rebuilt on user change,
5787
+ * TTL expiry, or explicit invalidation after a strategy-doc write.
5788
+ */
5789
+ _cachedNavigator = null;
5790
+ _navigatorTtlMs = 5 * 60 * 1e3;
5772
5791
  setEphemeralHints(hints) {
5773
5792
  this._pendingHints = hints;
5774
5793
  }
5775
5794
  async getWeightedCards(limit) {
5776
5795
  const u = await this._getCurrentUser();
5777
5796
  try {
5778
- const navigator2 = await this.createNavigator(u);
5797
+ const { navigator: navigator2 } = await this._getCachedNavigator(u);
5779
5798
  if (this._pendingHints) {
5780
5799
  navigator2.setEphemeralHints(this._pendingHints);
5781
5800
  this._pendingHints = null;
5782
5801
  }
5783
- return navigator2.getWeightedCards(limit);
5802
+ const result = await navigator2.getWeightedCards(limit);
5803
+ return result;
5784
5804
  } catch (e) {
5785
5805
  logger.error(`[courseDB] Error getting weighted cards: ${e}`);
5786
5806
  throw e;
5787
5807
  }
5788
5808
  }
5809
+ /**
5810
+ * Return the assembled navigator, reusing the cached instance when possible.
5811
+ * Reuse preserves the Pipeline's per-session caches (tags, orchestration
5812
+ * context) across replans, which is the dominant per-replan cost once the
5813
+ * ELO-pool cost is removed. Rebuilds on user change or TTL expiry.
5814
+ */
5815
+ async _getCachedNavigator(user) {
5816
+ const userId = user.getUsername();
5817
+ const now = Date.now();
5818
+ if (this._cachedNavigator && this._cachedNavigator.userId === userId && now - this._cachedNavigator.builtAt < this._navigatorTtlMs) {
5819
+ return { navigator: this._cachedNavigator.navigator, cacheStatus: "hit" };
5820
+ }
5821
+ const navigator2 = await this.createNavigator(user);
5822
+ this._cachedNavigator = { navigator: navigator2, userId, builtAt: now };
5823
+ return { navigator: navigator2, cacheStatus: "miss" };
5824
+ }
5825
+ /**
5826
+ * Drop the cached navigator so the next getWeightedCards() rebuilds it.
5827
+ * Call after mutating this course's navigation strategy documents.
5828
+ */
5829
+ invalidateNavigatorCache() {
5830
+ this._cachedNavigator = null;
5831
+ }
5789
5832
  async getCardsCenteredAtELO(options = {
5790
5833
  limit: 99,
5791
5834
  elo: "user"
@@ -5808,20 +5851,31 @@ ${e.stack}` : JSON.stringify(e);
5808
5851
  } else {
5809
5852
  targetElo = options.elo;
5810
5853
  }
5811
- let cards = [];
5812
- let mult = 4;
5813
- let previousCount = -1;
5814
- let newCount = 0;
5815
- while (cards.length < options.limit && newCount !== previousCount) {
5816
- cards = await this.getCardsByELO(targetElo, mult * options.limit);
5817
- previousCount = newCount;
5818
- newCount = cards.length;
5819
- logger.debug(`Found ${cards.length} elo neighbor cards...`);
5820
- if (filter) {
5821
- cards = cards.filter(filter);
5822
- logger.debug(`Filtered to ${cards.length} cards...`);
5823
- }
5824
- mult *= 2;
5854
+ const POOL_SIZE = Math.max(2e3, options.limit * 4);
5855
+ const nowMs = Date.now();
5856
+ let cacheStatus = "hit";
5857
+ if (!this._eloPoolCache || nowMs - this._eloPoolCache.fetchedAt > this._eloPoolTtlMs) {
5858
+ const fetched = await this.getCardsByELO(targetElo, POOL_SIZE);
5859
+ if (fetched.length > 0) {
5860
+ this._eloPoolCache = { rows: fetched, fetchedAt: nowMs };
5861
+ }
5862
+ cacheStatus = "miss";
5863
+ }
5864
+ const rankAgainstCurrentElo = () => {
5865
+ const raw = this._eloPoolCache?.rows ?? [];
5866
+ const survivors = filter ? raw.filter((c) => filter(c)) : raw;
5867
+ return survivors.map((c) => ({ ...c })).sort(
5868
+ (a, b) => Math.abs((a.elo ?? targetElo) - targetElo) - Math.abs((b.elo ?? targetElo) - targetElo)
5869
+ );
5870
+ };
5871
+ let cards = rankAgainstCurrentElo();
5872
+ if (cards.length < options.limit && (cacheStatus === "hit" || !this._eloPoolCache)) {
5873
+ const fetched = await this.getCardsByELO(targetElo, POOL_SIZE);
5874
+ if (fetched.length > 0) {
5875
+ this._eloPoolCache = { rows: fetched, fetchedAt: nowMs };
5876
+ }
5877
+ cards = rankAgainstCurrentElo();
5878
+ cacheStatus = "refresh";
5825
5879
  }
5826
5880
  const selectedCards = [];
5827
5881
  while (selectedCards.length < options.limit && cards.length > 0) {
@@ -7707,9 +7761,14 @@ var init_UserDBDebugger = __esm({
7707
7761
  */
7708
7762
  async showScheduledReviews(courseId) {
7709
7763
  const userDB = getUserDB();
7710
- if (!userDB) return;
7764
+ if (!userDB) {
7765
+ logger.info("[UserDB Debug] Data layer not available");
7766
+ return;
7767
+ }
7768
+ logger.info(`[UserDB Debug] Fetching pending reviews${courseId ? ` for course: ${courseId}` : ""}...`);
7711
7769
  try {
7712
7770
  const reviews = await userDB.getPendingReviews(courseId);
7771
+ logger.info(`[UserDB Debug] Got ${reviews.length} reviews`);
7713
7772
  console.group(`\u{1F4C5} Scheduled Reviews${courseId ? ` (${courseId})` : ""}`);
7714
7773
  logger.info(`Total: ${reviews.length}`);
7715
7774
  if (reviews.length > 0) {