@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/core/index.js +80 -21
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +80 -21
- package/dist/core/index.mjs.map +1 -1
- package/dist/impl/couch/index.d.cts +32 -0
- package/dist/impl/couch/index.d.ts +32 -0
- package/dist/impl/couch/index.js +80 -21
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +80 -21
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.js +8 -5
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +8 -5
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/index.d.cts +13 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +94 -22
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +94 -22
- package/dist/index.mjs.map +1 -1
- package/docs/session-lifecycle-and-replan.md +418 -0
- package/package.json +3 -3
- package/src/core/UserDBDebugger.ts +7 -1
- package/src/core/navigators/Pipeline.ts +4 -0
- package/src/core/navigators/generators/elo.ts +19 -6
- package/src/core/navigators/generators/srs.ts +10 -0
- package/src/impl/couch/courseDB.ts +146 -17
- package/src/study/SessionController.ts +56 -1
- package/src/study/services/CardHydrationService.ts +24 -0
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
|
|
1963
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
5992
|
-
|
|
5993
|
-
let
|
|
5994
|
-
|
|
5995
|
-
|
|
5996
|
-
|
|
5997
|
-
|
|
5998
|
-
|
|
5999
|
-
|
|
6000
|
-
|
|
6001
|
-
|
|
6002
|
-
|
|
6003
|
-
|
|
6004
|
-
|
|
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)
|
|
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 <=
|
|
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(
|