@vue-skuilder/db 0.2.1 → 0.2.3

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.
Files changed (44) hide show
  1. package/dist/{contentSource-Ht3N2f-y.d.ts → contentSource-Cplhv3bJ.d.ts} +1 -1
  2. package/dist/{contentSource-BMlMwSiG.d.cts → contentSource-kI9_jwTu.d.cts} +1 -1
  3. package/dist/core/index.d.cts +5 -5
  4. package/dist/core/index.d.ts +5 -5
  5. package/dist/core/index.js +76 -21
  6. package/dist/core/index.js.map +1 -1
  7. package/dist/core/index.mjs +76 -21
  8. package/dist/core/index.mjs.map +1 -1
  9. package/dist/{dataLayerProvider-BEqB8VBR.d.cts → dataLayerProvider-CiA2Rr0v.d.cts} +1 -1
  10. package/dist/{dataLayerProvider-DObSXjnf.d.ts → dataLayerProvider-DrBqOUa3.d.ts} +1 -1
  11. package/dist/impl/couch/index.d.cts +35 -3
  12. package/dist/impl/couch/index.d.ts +35 -3
  13. package/dist/impl/couch/index.js +76 -21
  14. package/dist/impl/couch/index.js.map +1 -1
  15. package/dist/impl/couch/index.mjs +76 -21
  16. package/dist/impl/couch/index.mjs.map +1 -1
  17. package/dist/impl/static/index.d.cts +4 -4
  18. package/dist/impl/static/index.d.ts +4 -4
  19. package/dist/impl/static/index.js +4 -5
  20. package/dist/impl/static/index.js.map +1 -1
  21. package/dist/impl/static/index.mjs +4 -5
  22. package/dist/impl/static/index.mjs.map +1 -1
  23. package/dist/{index-BWvO-_rJ.d.ts → index-BLLT5BYE.d.ts} +1 -1
  24. package/dist/{index-Ba7hYbHj.d.cts → index-k9NFHpS1.d.cts} +1 -1
  25. package/dist/index.d.cts +164 -10
  26. package/dist/index.d.ts +164 -10
  27. package/dist/index.js +215 -28
  28. package/dist/index.js.map +1 -1
  29. package/dist/index.mjs +215 -28
  30. package/dist/index.mjs.map +1 -1
  31. package/dist/{types-W8n-B6HG.d.cts → types-BFUa1pa3.d.cts} +1 -1
  32. package/dist/{types-CJrLM1Ew.d.ts → types-CHgpWQAY.d.ts} +1 -1
  33. package/dist/{types-legacy-JXDxinpU.d.cts → types-legacy-4tlwHnXo.d.cts} +1 -1
  34. package/dist/{types-legacy-JXDxinpU.d.ts → types-legacy-4tlwHnXo.d.ts} +1 -1
  35. package/dist/util/packer/index.d.cts +3 -3
  36. package/dist/util/packer/index.d.ts +3 -3
  37. package/docs/session-lifecycle-and-replan.md +418 -0
  38. package/package.json +3 -3
  39. package/src/core/navigators/Pipeline.ts +5 -1
  40. package/src/core/navigators/generators/elo.ts +19 -6
  41. package/src/core/navigators/generators/srs.ts +10 -0
  42. package/src/impl/couch/courseDB.ts +146 -17
  43. package/src/study/SessionController.ts +295 -13
  44. package/src/study/services/CardHydrationService.ts +24 -0
@@ -1731,10 +1731,8 @@ var init_elo = __esm({
1731
1731
  { limit, elo: "user" },
1732
1732
  (c) => !activeCards.some((ac) => c.cardID === ac.cardID)
1733
1733
  )).map((c) => ({ ...c, status: "new" }));
1734
- const cardIds = newCards.map((c) => c.cardID);
1735
- const cardEloData = await this.course.getCardEloData(cardIds);
1736
- const scored = newCards.map((c, i) => {
1737
- const cardElo = cardEloData[i]?.global?.score ?? 1e3;
1734
+ const scored = newCards.map((c) => {
1735
+ const cardElo = c.elo ?? 1e3;
1738
1736
  const distance = Math.abs(cardElo - userGlobalElo);
1739
1737
  const rawScore = Math.max(0, 1 - distance / 500);
1740
1738
  const samplingKey = rawScore > 0 ? Math.random() ** (1 / rawScore) : 0;
@@ -3712,7 +3710,8 @@ var init_orchestration = __esm({
3712
3710
  // src/core/navigators/Pipeline.ts
3713
3711
  var Pipeline_exports = {};
3714
3712
  __export(Pipeline_exports, {
3715
- Pipeline: () => Pipeline
3713
+ Pipeline: () => Pipeline,
3714
+ mergeHints: () => mergeHints2
3716
3715
  });
3717
3716
  import { toCourseElo as toCourseElo5 } from "@vue-skuilder/common";
3718
3717
  function globToRegex(pattern) {
@@ -5451,6 +5450,7 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
5451
5450
  }
5452
5451
  async addNavigationStrategy(data) {
5453
5452
  logger.debug(`[courseDB] Adding navigation strategy: ${data._id}`);
5453
+ this.invalidateNavigatorCache();
5454
5454
  return this.remoteDB.put(data).then(() => {
5455
5455
  });
5456
5456
  }
@@ -5517,23 +5517,67 @@ ${e.stack}` : JSON.stringify(e);
5517
5517
  * @returns Cards sorted by score descending
5518
5518
  */
5519
5519
  _pendingHints = null;
5520
+ /**
5521
+ * Session-scoped cache of the broad ELO-neighbor pool used by
5522
+ * getCardsCenteredAtELO. The `elo` view query re-indexes on first touch per
5523
+ * call (PouchDB 9 ignores `stale`), so without this each plan/replan pays
5524
+ * ~1.5-2s. The pool is fetched once and re-ranked against the live (roaming)
5525
+ * ELO in memory on subsequent calls.
5526
+ */
5527
+ _eloPoolCache = null;
5528
+ _eloPoolTtlMs = 5 * 60 * 1e3;
5529
+ /**
5530
+ * Cached assembled navigator (Pipeline). createNavigator() reads strategy
5531
+ * docs and builds a fresh Pipeline every call — whose internal `_tagCache`
5532
+ * and `_cachedOrchestration` are designed to make replans cheap but never
5533
+ * survive, because the instance is discarded each run. Caching it lets those
5534
+ * caches persist across plan/replan within a session (SessionController holds
5535
+ * one CourseDB instance for the session's lifetime). Rebuilt on user change,
5536
+ * TTL expiry, or explicit invalidation after a strategy-doc write.
5537
+ */
5538
+ _cachedNavigator = null;
5539
+ _navigatorTtlMs = 5 * 60 * 1e3;
5520
5540
  setEphemeralHints(hints) {
5521
5541
  this._pendingHints = hints;
5522
5542
  }
5523
5543
  async getWeightedCards(limit) {
5524
5544
  const u = await this._getCurrentUser();
5525
5545
  try {
5526
- const navigator2 = await this.createNavigator(u);
5546
+ const { navigator: navigator2 } = await this._getCachedNavigator(u);
5527
5547
  if (this._pendingHints) {
5528
5548
  navigator2.setEphemeralHints(this._pendingHints);
5529
5549
  this._pendingHints = null;
5530
5550
  }
5531
- return navigator2.getWeightedCards(limit);
5551
+ const result = await navigator2.getWeightedCards(limit);
5552
+ return result;
5532
5553
  } catch (e) {
5533
5554
  logger.error(`[courseDB] Error getting weighted cards: ${e}`);
5534
5555
  throw e;
5535
5556
  }
5536
5557
  }
5558
+ /**
5559
+ * Return the assembled navigator, reusing the cached instance when possible.
5560
+ * Reuse preserves the Pipeline's per-session caches (tags, orchestration
5561
+ * context) across replans, which is the dominant per-replan cost once the
5562
+ * ELO-pool cost is removed. Rebuilds on user change or TTL expiry.
5563
+ */
5564
+ async _getCachedNavigator(user) {
5565
+ const userId = user.getUsername();
5566
+ const now = Date.now();
5567
+ if (this._cachedNavigator && this._cachedNavigator.userId === userId && now - this._cachedNavigator.builtAt < this._navigatorTtlMs) {
5568
+ return { navigator: this._cachedNavigator.navigator, cacheStatus: "hit" };
5569
+ }
5570
+ const navigator2 = await this.createNavigator(user);
5571
+ this._cachedNavigator = { navigator: navigator2, userId, builtAt: now };
5572
+ return { navigator: navigator2, cacheStatus: "miss" };
5573
+ }
5574
+ /**
5575
+ * Drop the cached navigator so the next getWeightedCards() rebuilds it.
5576
+ * Call after mutating this course's navigation strategy documents.
5577
+ */
5578
+ invalidateNavigatorCache() {
5579
+ this._cachedNavigator = null;
5580
+ }
5537
5581
  async getCardsCenteredAtELO(options = {
5538
5582
  limit: 99,
5539
5583
  elo: "user"
@@ -5556,20 +5600,31 @@ ${e.stack}` : JSON.stringify(e);
5556
5600
  } else {
5557
5601
  targetElo = options.elo;
5558
5602
  }
5559
- let cards = [];
5560
- let mult = 4;
5561
- let previousCount = -1;
5562
- let newCount = 0;
5563
- while (cards.length < options.limit && newCount !== previousCount) {
5564
- cards = await this.getCardsByELO(targetElo, mult * options.limit);
5565
- previousCount = newCount;
5566
- newCount = cards.length;
5567
- logger.debug(`Found ${cards.length} elo neighbor cards...`);
5568
- if (filter) {
5569
- cards = cards.filter(filter);
5570
- logger.debug(`Filtered to ${cards.length} cards...`);
5571
- }
5572
- mult *= 2;
5603
+ const POOL_SIZE = Math.max(2e3, options.limit * 4);
5604
+ const nowMs = Date.now();
5605
+ let cacheStatus = "hit";
5606
+ if (!this._eloPoolCache || nowMs - this._eloPoolCache.fetchedAt > this._eloPoolTtlMs) {
5607
+ const fetched = await this.getCardsByELO(targetElo, POOL_SIZE);
5608
+ if (fetched.length > 0) {
5609
+ this._eloPoolCache = { rows: fetched, fetchedAt: nowMs };
5610
+ }
5611
+ cacheStatus = "miss";
5612
+ }
5613
+ const rankAgainstCurrentElo = () => {
5614
+ const raw = this._eloPoolCache?.rows ?? [];
5615
+ const survivors = filter ? raw.filter((c) => filter(c)) : raw;
5616
+ return survivors.map((c) => ({ ...c })).sort(
5617
+ (a, b) => Math.abs((a.elo ?? targetElo) - targetElo) - Math.abs((b.elo ?? targetElo) - targetElo)
5618
+ );
5619
+ };
5620
+ let cards = rankAgainstCurrentElo();
5621
+ if (cards.length < options.limit && (cacheStatus === "hit" || !this._eloPoolCache)) {
5622
+ const fetched = await this.getCardsByELO(targetElo, POOL_SIZE);
5623
+ if (fetched.length > 0) {
5624
+ this._eloPoolCache = { rows: fetched, fetchedAt: nowMs };
5625
+ }
5626
+ cards = rankAgainstCurrentElo();
5627
+ cacheStatus = "refresh";
5573
5628
  }
5574
5629
  const selectedCards = [];
5575
5630
  while (selectedCards.length < options.limit && cards.length > 0) {