@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.
package/dist/index.mjs CHANGED
@@ -1959,10 +1959,8 @@ var init_elo = __esm({
1959
1959
  { limit, elo: "user" },
1960
1960
  (c) => !activeCards.some((ac) => c.cardID === ac.cardID)
1961
1961
  )).map((c) => ({ ...c, status: "new" }));
1962
- const cardIds = newCards.map((c) => c.cardID);
1963
- const cardEloData = await this.course.getCardEloData(cardIds);
1964
- const scored = newCards.map((c, i) => {
1965
- const cardElo = cardEloData[i]?.global?.score ?? 1e3;
1962
+ const scored = newCards.map((c) => {
1963
+ const cardElo = c.elo ?? 1e3;
1966
1964
  const distance = Math.abs(cardElo - userGlobalElo);
1967
1965
  const rawScore = Math.max(0, 1 - distance / 500);
1968
1966
  const samplingKey = rawScore > 0 ? Math.random() ** (1 / rawScore) : 0;
@@ -5883,6 +5881,7 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
5883
5881
  }
5884
5882
  async addNavigationStrategy(data) {
5885
5883
  logger.debug(`[courseDB] Adding navigation strategy: ${data._id}`);
5884
+ this.invalidateNavigatorCache();
5886
5885
  return this.remoteDB.put(data).then(() => {
5887
5886
  });
5888
5887
  }
@@ -5949,23 +5948,67 @@ ${e.stack}` : JSON.stringify(e);
5949
5948
  * @returns Cards sorted by score descending
5950
5949
  */
5951
5950
  _pendingHints = null;
5951
+ /**
5952
+ * Session-scoped cache of the broad ELO-neighbor pool used by
5953
+ * getCardsCenteredAtELO. The `elo` view query re-indexes on first touch per
5954
+ * call (PouchDB 9 ignores `stale`), so without this each plan/replan pays
5955
+ * ~1.5-2s. The pool is fetched once and re-ranked against the live (roaming)
5956
+ * ELO in memory on subsequent calls.
5957
+ */
5958
+ _eloPoolCache = null;
5959
+ _eloPoolTtlMs = 5 * 60 * 1e3;
5960
+ /**
5961
+ * Cached assembled navigator (Pipeline). createNavigator() reads strategy
5962
+ * docs and builds a fresh Pipeline every call — whose internal `_tagCache`
5963
+ * and `_cachedOrchestration` are designed to make replans cheap but never
5964
+ * survive, because the instance is discarded each run. Caching it lets those
5965
+ * caches persist across plan/replan within a session (SessionController holds
5966
+ * one CourseDB instance for the session's lifetime). Rebuilt on user change,
5967
+ * TTL expiry, or explicit invalidation after a strategy-doc write.
5968
+ */
5969
+ _cachedNavigator = null;
5970
+ _navigatorTtlMs = 5 * 60 * 1e3;
5952
5971
  setEphemeralHints(hints) {
5953
5972
  this._pendingHints = hints;
5954
5973
  }
5955
5974
  async getWeightedCards(limit) {
5956
5975
  const u = await this._getCurrentUser();
5957
5976
  try {
5958
- const navigator2 = await this.createNavigator(u);
5977
+ const { navigator: navigator2 } = await this._getCachedNavigator(u);
5959
5978
  if (this._pendingHints) {
5960
5979
  navigator2.setEphemeralHints(this._pendingHints);
5961
5980
  this._pendingHints = null;
5962
5981
  }
5963
- return navigator2.getWeightedCards(limit);
5982
+ const result = await navigator2.getWeightedCards(limit);
5983
+ return result;
5964
5984
  } catch (e) {
5965
5985
  logger.error(`[courseDB] Error getting weighted cards: ${e}`);
5966
5986
  throw e;
5967
5987
  }
5968
5988
  }
5989
+ /**
5990
+ * Return the assembled navigator, reusing the cached instance when possible.
5991
+ * Reuse preserves the Pipeline's per-session caches (tags, orchestration
5992
+ * context) across replans, which is the dominant per-replan cost once the
5993
+ * ELO-pool cost is removed. Rebuilds on user change or TTL expiry.
5994
+ */
5995
+ async _getCachedNavigator(user) {
5996
+ const userId = user.getUsername();
5997
+ const now = Date.now();
5998
+ if (this._cachedNavigator && this._cachedNavigator.userId === userId && now - this._cachedNavigator.builtAt < this._navigatorTtlMs) {
5999
+ return { navigator: this._cachedNavigator.navigator, cacheStatus: "hit" };
6000
+ }
6001
+ const navigator2 = await this.createNavigator(user);
6002
+ this._cachedNavigator = { navigator: navigator2, userId, builtAt: now };
6003
+ return { navigator: navigator2, cacheStatus: "miss" };
6004
+ }
6005
+ /**
6006
+ * Drop the cached navigator so the next getWeightedCards() rebuilds it.
6007
+ * Call after mutating this course's navigation strategy documents.
6008
+ */
6009
+ invalidateNavigatorCache() {
6010
+ this._cachedNavigator = null;
6011
+ }
5969
6012
  async getCardsCenteredAtELO(options = {
5970
6013
  limit: 99,
5971
6014
  elo: "user"
@@ -5988,20 +6031,31 @@ ${e.stack}` : JSON.stringify(e);
5988
6031
  } else {
5989
6032
  targetElo = options.elo;
5990
6033
  }
5991
- let cards = [];
5992
- let mult = 4;
5993
- let previousCount = -1;
5994
- let newCount = 0;
5995
- while (cards.length < options.limit && newCount !== previousCount) {
5996
- cards = await this.getCardsByELO(targetElo, mult * options.limit);
5997
- previousCount = newCount;
5998
- newCount = cards.length;
5999
- logger.debug(`Found ${cards.length} elo neighbor cards...`);
6000
- if (filter) {
6001
- cards = cards.filter(filter);
6002
- logger.debug(`Filtered to ${cards.length} cards...`);
6003
- }
6004
- mult *= 2;
6034
+ const POOL_SIZE = Math.max(2e3, options.limit * 4);
6035
+ const nowMs = Date.now();
6036
+ let cacheStatus = "hit";
6037
+ if (!this._eloPoolCache || nowMs - this._eloPoolCache.fetchedAt > this._eloPoolTtlMs) {
6038
+ const fetched = await this.getCardsByELO(targetElo, POOL_SIZE);
6039
+ if (fetched.length > 0) {
6040
+ this._eloPoolCache = { rows: fetched, fetchedAt: nowMs };
6041
+ }
6042
+ cacheStatus = "miss";
6043
+ }
6044
+ const rankAgainstCurrentElo = () => {
6045
+ const raw = this._eloPoolCache?.rows ?? [];
6046
+ const survivors = filter ? raw.filter((c) => filter(c)) : raw;
6047
+ return survivors.map((c) => ({ ...c })).sort(
6048
+ (a, b) => Math.abs((a.elo ?? targetElo) - targetElo) - Math.abs((b.elo ?? targetElo) - targetElo)
6049
+ );
6050
+ };
6051
+ let cards = rankAgainstCurrentElo();
6052
+ if (cards.length < options.limit && (cacheStatus === "hit" || !this._eloPoolCache)) {
6053
+ const fetched = await this.getCardsByELO(targetElo, POOL_SIZE);
6054
+ if (fetched.length > 0) {
6055
+ this._eloPoolCache = { rows: fetched, fetchedAt: nowMs };
6056
+ }
6057
+ cards = rankAgainstCurrentElo();
6058
+ cacheStatus = "refresh";
6005
6059
  }
6006
6060
  const selectedCards = [];
6007
6061
  while (selectedCards.length < options.limit && cards.length > 0) {
@@ -9814,9 +9868,14 @@ var init_UserDBDebugger = __esm({
9814
9868
  */
9815
9869
  async showScheduledReviews(courseId) {
9816
9870
  const userDB = getUserDB();
9817
- if (!userDB) return;
9871
+ if (!userDB) {
9872
+ logger.info("[UserDB Debug] Data layer not available");
9873
+ return;
9874
+ }
9875
+ logger.info(`[UserDB Debug] Fetching pending reviews${courseId ? ` for course: ${courseId}` : ""}...`);
9818
9876
  try {
9819
9877
  const reviews = await userDB.getPendingReviews(courseId);
9878
+ logger.info(`[UserDB Debug] Got ${reviews.length} reviews`);
9820
9879
  console.group(`\u{1F4C5} Scheduled Reviews${courseId ? ` (${courseId})` : ""}`);
9821
9880
  logger.info(`Total: ${reviews.length}`);
9822
9881
  if (reviews.length > 0) {
@@ -13594,6 +13653,19 @@ var SessionController = class _SessionController extends Loggable {
13594
13653
  * know or care which strategy assigned the score.
13595
13654
  */
13596
13655
  static WELL_INDICATED_SCORE = 0.1;
13656
+ /**
13657
+ * newQ length at or below which the opportunistic depletion-prefetch
13658
+ * fires. Sets the lead time available for the background replan to land
13659
+ * before the user actually empties the queue and falls into the
13660
+ * (synchronous) wedge-breaker path.
13661
+ *
13662
+ * Set to a small absolute value (not a fraction of batch limit) because
13663
+ * pipeline latency is roughly fixed regardless of batch size — what
13664
+ * matters is "how many cards of user-pace do we have left." 3 cards
13665
+ * × ~3-5s/card = ~10-15s of lead time, comfortably exceeding typical
13666
+ * pipeline latency.
13667
+ */
13668
+ static DEPLETION_PREFETCH_THRESHOLD = 3;
13597
13669
  /**
13598
13670
  * Internal replan execution. Runs the pipeline, builds a new newQ,
13599
13671
  * atomically swaps it in, and triggers hydration for the new contents.
@@ -13900,7 +13972,7 @@ var SessionController = class _SessionController extends Loggable {
13900
13972
  this.log("nextCard: queues empty, awaiting in-flight replan before drawing");
13901
13973
  await this._replanPromise;
13902
13974
  }
13903
- if (this.newQ.length <= 1 && this._secondsRemaining > 0 && !this._replanPromise) {
13975
+ if (this.newQ.length <= _SessionController.DEPLETION_PREFETCH_THRESHOLD && this._secondsRemaining > 0 && !this._replanPromise) {
13904
13976
  this._suppressQualityReplan = false;
13905
13977
  const otherContent = this.reviewQ.length + this.failedQ.length;
13906
13978
  this.log(