@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.d.cts CHANGED
@@ -544,6 +544,19 @@ declare class SessionController<TView = unknown> extends Loggable {
544
544
  * know or care which strategy assigned the score.
545
545
  */
546
546
  private static readonly WELL_INDICATED_SCORE;
547
+ /**
548
+ * newQ length at or below which the opportunistic depletion-prefetch
549
+ * fires. Sets the lead time available for the background replan to land
550
+ * before the user actually empties the queue and falls into the
551
+ * (synchronous) wedge-breaker path.
552
+ *
553
+ * Set to a small absolute value (not a fraction of batch limit) because
554
+ * pipeline latency is roughly fixed regardless of batch size — what
555
+ * matters is "how many cards of user-pace do we have left." 3 cards
556
+ * × ~3-5s/card = ~10-15s of lead time, comfortably exceeding typical
557
+ * pipeline latency.
558
+ */
559
+ private static readonly DEPLETION_PREFETCH_THRESHOLD;
547
560
  /**
548
561
  * Internal replan execution. Runs the pipeline, builds a new newQ,
549
562
  * atomically swaps it in, and triggers hydration for the new contents.
package/dist/index.d.ts CHANGED
@@ -544,6 +544,19 @@ declare class SessionController<TView = unknown> extends Loggable {
544
544
  * know or care which strategy assigned the score.
545
545
  */
546
546
  private static readonly WELL_INDICATED_SCORE;
547
+ /**
548
+ * newQ length at or below which the opportunistic depletion-prefetch
549
+ * fires. Sets the lead time available for the background replan to land
550
+ * before the user actually empties the queue and falls into the
551
+ * (synchronous) wedge-breaker path.
552
+ *
553
+ * Set to a small absolute value (not a fraction of batch limit) because
554
+ * pipeline latency is roughly fixed regardless of batch size — what
555
+ * matters is "how many cards of user-pace do we have left." 3 cards
556
+ * × ~3-5s/card = ~10-15s of lead time, comfortably exceeding typical
557
+ * pipeline latency.
558
+ */
559
+ private static readonly DEPLETION_PREFETCH_THRESHOLD;
547
560
  /**
548
561
  * Internal replan execution. Runs the pipeline, builds a new newQ,
549
562
  * atomically swaps it in, and triggers hydration for the new contents.
package/dist/index.js CHANGED
@@ -1982,10 +1982,8 @@ var init_elo = __esm({
1982
1982
  { limit, elo: "user" },
1983
1983
  (c) => !activeCards.some((ac) => c.cardID === ac.cardID)
1984
1984
  )).map((c) => ({ ...c, status: "new" }));
1985
- const cardIds = newCards.map((c) => c.cardID);
1986
- const cardEloData = await this.course.getCardEloData(cardIds);
1987
- const scored = newCards.map((c, i) => {
1988
- const cardElo = cardEloData[i]?.global?.score ?? 1e3;
1985
+ const scored = newCards.map((c) => {
1986
+ const cardElo = c.elo ?? 1e3;
1989
1987
  const distance = Math.abs(cardElo - userGlobalElo);
1990
1988
  const rawScore = Math.max(0, 1 - distance / 500);
1991
1989
  const samplingKey = rawScore > 0 ? Math.random() ** (1 / rawScore) : 0;
@@ -5901,6 +5899,7 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
5901
5899
  }
5902
5900
  async addNavigationStrategy(data) {
5903
5901
  logger.debug(`[courseDB] Adding navigation strategy: ${data._id}`);
5902
+ this.invalidateNavigatorCache();
5904
5903
  return this.remoteDB.put(data).then(() => {
5905
5904
  });
5906
5905
  }
@@ -5967,23 +5966,67 @@ ${e.stack}` : JSON.stringify(e);
5967
5966
  * @returns Cards sorted by score descending
5968
5967
  */
5969
5968
  _pendingHints = null;
5969
+ /**
5970
+ * Session-scoped cache of the broad ELO-neighbor pool used by
5971
+ * getCardsCenteredAtELO. The `elo` view query re-indexes on first touch per
5972
+ * call (PouchDB 9 ignores `stale`), so without this each plan/replan pays
5973
+ * ~1.5-2s. The pool is fetched once and re-ranked against the live (roaming)
5974
+ * ELO in memory on subsequent calls.
5975
+ */
5976
+ _eloPoolCache = null;
5977
+ _eloPoolTtlMs = 5 * 60 * 1e3;
5978
+ /**
5979
+ * Cached assembled navigator (Pipeline). createNavigator() reads strategy
5980
+ * docs and builds a fresh Pipeline every call — whose internal `_tagCache`
5981
+ * and `_cachedOrchestration` are designed to make replans cheap but never
5982
+ * survive, because the instance is discarded each run. Caching it lets those
5983
+ * caches persist across plan/replan within a session (SessionController holds
5984
+ * one CourseDB instance for the session's lifetime). Rebuilt on user change,
5985
+ * TTL expiry, or explicit invalidation after a strategy-doc write.
5986
+ */
5987
+ _cachedNavigator = null;
5988
+ _navigatorTtlMs = 5 * 60 * 1e3;
5970
5989
  setEphemeralHints(hints) {
5971
5990
  this._pendingHints = hints;
5972
5991
  }
5973
5992
  async getWeightedCards(limit) {
5974
5993
  const u = await this._getCurrentUser();
5975
5994
  try {
5976
- const navigator2 = await this.createNavigator(u);
5995
+ const { navigator: navigator2 } = await this._getCachedNavigator(u);
5977
5996
  if (this._pendingHints) {
5978
5997
  navigator2.setEphemeralHints(this._pendingHints);
5979
5998
  this._pendingHints = null;
5980
5999
  }
5981
- return navigator2.getWeightedCards(limit);
6000
+ const result = await navigator2.getWeightedCards(limit);
6001
+ return result;
5982
6002
  } catch (e) {
5983
6003
  logger.error(`[courseDB] Error getting weighted cards: ${e}`);
5984
6004
  throw e;
5985
6005
  }
5986
6006
  }
6007
+ /**
6008
+ * Return the assembled navigator, reusing the cached instance when possible.
6009
+ * Reuse preserves the Pipeline's per-session caches (tags, orchestration
6010
+ * context) across replans, which is the dominant per-replan cost once the
6011
+ * ELO-pool cost is removed. Rebuilds on user change or TTL expiry.
6012
+ */
6013
+ async _getCachedNavigator(user) {
6014
+ const userId = user.getUsername();
6015
+ const now = Date.now();
6016
+ if (this._cachedNavigator && this._cachedNavigator.userId === userId && now - this._cachedNavigator.builtAt < this._navigatorTtlMs) {
6017
+ return { navigator: this._cachedNavigator.navigator, cacheStatus: "hit" };
6018
+ }
6019
+ const navigator2 = await this.createNavigator(user);
6020
+ this._cachedNavigator = { navigator: navigator2, userId, builtAt: now };
6021
+ return { navigator: navigator2, cacheStatus: "miss" };
6022
+ }
6023
+ /**
6024
+ * Drop the cached navigator so the next getWeightedCards() rebuilds it.
6025
+ * Call after mutating this course's navigation strategy documents.
6026
+ */
6027
+ invalidateNavigatorCache() {
6028
+ this._cachedNavigator = null;
6029
+ }
5987
6030
  async getCardsCenteredAtELO(options = {
5988
6031
  limit: 99,
5989
6032
  elo: "user"
@@ -6006,20 +6049,31 @@ ${e.stack}` : JSON.stringify(e);
6006
6049
  } else {
6007
6050
  targetElo = options.elo;
6008
6051
  }
6009
- let cards = [];
6010
- let mult = 4;
6011
- let previousCount = -1;
6012
- let newCount = 0;
6013
- while (cards.length < options.limit && newCount !== previousCount) {
6014
- cards = await this.getCardsByELO(targetElo, mult * options.limit);
6015
- previousCount = newCount;
6016
- newCount = cards.length;
6017
- logger.debug(`Found ${cards.length} elo neighbor cards...`);
6018
- if (filter) {
6019
- cards = cards.filter(filter);
6020
- logger.debug(`Filtered to ${cards.length} cards...`);
6021
- }
6022
- mult *= 2;
6052
+ const POOL_SIZE = Math.max(2e3, options.limit * 4);
6053
+ const nowMs = Date.now();
6054
+ let cacheStatus = "hit";
6055
+ if (!this._eloPoolCache || nowMs - this._eloPoolCache.fetchedAt > this._eloPoolTtlMs) {
6056
+ const fetched = await this.getCardsByELO(targetElo, POOL_SIZE);
6057
+ if (fetched.length > 0) {
6058
+ this._eloPoolCache = { rows: fetched, fetchedAt: nowMs };
6059
+ }
6060
+ cacheStatus = "miss";
6061
+ }
6062
+ const rankAgainstCurrentElo = () => {
6063
+ const raw = this._eloPoolCache?.rows ?? [];
6064
+ const survivors = filter ? raw.filter((c) => filter(c)) : raw;
6065
+ return survivors.map((c) => ({ ...c })).sort(
6066
+ (a, b) => Math.abs((a.elo ?? targetElo) - targetElo) - Math.abs((b.elo ?? targetElo) - targetElo)
6067
+ );
6068
+ };
6069
+ let cards = rankAgainstCurrentElo();
6070
+ if (cards.length < options.limit && (cacheStatus === "hit" || !this._eloPoolCache)) {
6071
+ const fetched = await this.getCardsByELO(targetElo, POOL_SIZE);
6072
+ if (fetched.length > 0) {
6073
+ this._eloPoolCache = { rows: fetched, fetchedAt: nowMs };
6074
+ }
6075
+ cards = rankAgainstCurrentElo();
6076
+ cacheStatus = "refresh";
6023
6077
  }
6024
6078
  const selectedCards = [];
6025
6079
  while (selectedCards.length < options.limit && cards.length > 0) {
@@ -9835,9 +9889,14 @@ var init_UserDBDebugger = __esm({
9835
9889
  */
9836
9890
  async showScheduledReviews(courseId) {
9837
9891
  const userDB = getUserDB();
9838
- if (!userDB) return;
9892
+ if (!userDB) {
9893
+ logger.info("[UserDB Debug] Data layer not available");
9894
+ return;
9895
+ }
9896
+ logger.info(`[UserDB Debug] Fetching pending reviews${courseId ? ` for course: ${courseId}` : ""}...`);
9839
9897
  try {
9840
9898
  const reviews = await userDB.getPendingReviews(courseId);
9899
+ logger.info(`[UserDB Debug] Got ${reviews.length} reviews`);
9841
9900
  console.group(`\u{1F4C5} Scheduled Reviews${courseId ? ` (${courseId})` : ""}`);
9842
9901
  logger.info(`Total: ${reviews.length}`);
9843
9902
  if (reviews.length > 0) {
@@ -13696,6 +13755,19 @@ var SessionController = class _SessionController extends Loggable {
13696
13755
  * know or care which strategy assigned the score.
13697
13756
  */
13698
13757
  static WELL_INDICATED_SCORE = 0.1;
13758
+ /**
13759
+ * newQ length at or below which the opportunistic depletion-prefetch
13760
+ * fires. Sets the lead time available for the background replan to land
13761
+ * before the user actually empties the queue and falls into the
13762
+ * (synchronous) wedge-breaker path.
13763
+ *
13764
+ * Set to a small absolute value (not a fraction of batch limit) because
13765
+ * pipeline latency is roughly fixed regardless of batch size — what
13766
+ * matters is "how many cards of user-pace do we have left." 3 cards
13767
+ * × ~3-5s/card = ~10-15s of lead time, comfortably exceeding typical
13768
+ * pipeline latency.
13769
+ */
13770
+ static DEPLETION_PREFETCH_THRESHOLD = 3;
13699
13771
  /**
13700
13772
  * Internal replan execution. Runs the pipeline, builds a new newQ,
13701
13773
  * atomically swaps it in, and triggers hydration for the new contents.
@@ -14002,7 +14074,7 @@ var SessionController = class _SessionController extends Loggable {
14002
14074
  this.log("nextCard: queues empty, awaiting in-flight replan before drawing");
14003
14075
  await this._replanPromise;
14004
14076
  }
14005
- if (this.newQ.length <= 1 && this._secondsRemaining > 0 && !this._replanPromise) {
14077
+ if (this.newQ.length <= _SessionController.DEPLETION_PREFETCH_THRESHOLD && this._secondsRemaining > 0 && !this._replanPromise) {
14006
14078
  this._suppressQualityReplan = false;
14007
14079
  const otherContent = this.reviewQ.length + this.failedQ.length;
14008
14080
  this.log(