@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.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
|
|
1986
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
6010
|
-
|
|
6011
|
-
let
|
|
6012
|
-
|
|
6013
|
-
|
|
6014
|
-
|
|
6015
|
-
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
|
|
6019
|
-
|
|
6020
|
-
|
|
6021
|
-
|
|
6022
|
-
|
|
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)
|
|
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 <=
|
|
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(
|