@vue-skuilder/db 0.1.33 → 0.1.34
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 +26 -6
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +26 -6
- package/dist/core/index.mjs.map +1 -1
- package/dist/impl/couch/index.js +26 -6
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +26 -6
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.js +26 -6
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +26 -6
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/index.d.cts +12 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +62 -23
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +62 -23
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/core/navigators/generators/srs.ts +23 -1
- package/src/impl/common/BaseUserDB.ts +9 -6
- package/src/study/SessionController.ts +50 -20
- package/src/study/services/ResponseProcessor.ts +5 -1
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
7
|
-
"version": "0.1.
|
|
7
|
+
"version": "0.1.34",
|
|
8
8
|
"description": "Database layer for vue-skuilder",
|
|
9
9
|
"main": "dist/index.js",
|
|
10
10
|
"module": "dist/index.mjs",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"@nilock2/pouchdb-authentication": "^1.0.2",
|
|
51
|
-
"@vue-skuilder/common": "0.1.
|
|
51
|
+
"@vue-skuilder/common": "0.1.34",
|
|
52
52
|
"cross-fetch": "^4.1.0",
|
|
53
53
|
"moment": "^2.29.4",
|
|
54
54
|
"pouchdb": "^9.0.0",
|
|
@@ -62,5 +62,5 @@
|
|
|
62
62
|
"vite": "^8.0.0",
|
|
63
63
|
"vitest": "^4.1.0"
|
|
64
64
|
},
|
|
65
|
-
"stableVersion": "0.1.
|
|
65
|
+
"stableVersion": "0.1.34"
|
|
66
66
|
}
|
|
@@ -131,7 +131,29 @@ export default class SRSNavigator extends ContentNavigator implements CardGenera
|
|
|
131
131
|
const now = moment.utc();
|
|
132
132
|
|
|
133
133
|
// Filter to only cards that are actually due
|
|
134
|
-
|
|
134
|
+
let dueReviews = reviews.filter((r) => now.isAfter(moment.utc(r.reviewTime)));
|
|
135
|
+
|
|
136
|
+
// Remove scheduled reviews for cards tagged srs:skip (e.g. intro cards).
|
|
137
|
+
// These were scheduled before the tag convention existed — clean them up.
|
|
138
|
+
if (dueReviews.length > 0) {
|
|
139
|
+
const dueCardIds = [...new Set(dueReviews.map((r) => r.cardId))];
|
|
140
|
+
const tagsByCard = await this.course!.getAppliedTagsBatch(dueCardIds);
|
|
141
|
+
const skippedReviewIds: string[] = [];
|
|
142
|
+
dueReviews = dueReviews.filter((r) => {
|
|
143
|
+
const tags = tagsByCard.get(r.cardId) ?? [];
|
|
144
|
+
if (tags.includes('srs:skip')) {
|
|
145
|
+
skippedReviewIds.push(r._id);
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
return true;
|
|
149
|
+
});
|
|
150
|
+
if (skippedReviewIds.length > 0) {
|
|
151
|
+
logger.info(`[SRS] Removing ${skippedReviewIds.length} scheduled reviews for srs:skip cards`);
|
|
152
|
+
for (const id of skippedReviewIds) {
|
|
153
|
+
void this.user!.removeScheduledCardReview(id);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
135
157
|
|
|
136
158
|
// Compute backlog pressure - applies globally to all reviews
|
|
137
159
|
const backlogPressure = this.computeBacklogPressure(dueReviews.length);
|
|
@@ -889,18 +889,21 @@ Currently logged-in as ${this._username}.`
|
|
|
889
889
|
* @param course_id optional specification of individual course
|
|
890
890
|
*/
|
|
891
891
|
async getSeenCards(course_id?: string) {
|
|
892
|
-
|
|
892
|
+
// Doc IDs follow the pattern: cardH-{courseId}-{cardId}
|
|
893
|
+
// (see getCardHistoryID in core/util)
|
|
894
|
+
const basePrefix = DocTypePrefixes[DocType.CARDRECORD];
|
|
895
|
+
let filterPrefix = basePrefix;
|
|
893
896
|
if (course_id) {
|
|
894
|
-
|
|
897
|
+
filterPrefix += `-${course_id}-`;
|
|
895
898
|
}
|
|
896
|
-
const docs = await filterAllDocsByPrefix(this.localDB,
|
|
899
|
+
const docs = await filterAllDocsByPrefix(this.localDB, filterPrefix, {
|
|
897
900
|
include_docs: false,
|
|
898
901
|
});
|
|
899
|
-
// const docs = await this.localDB.allDocs({});
|
|
900
902
|
const ret: PouchDB.Core.DocumentId[] = [];
|
|
901
903
|
docs.rows.forEach((row) => {
|
|
902
|
-
if (row.id.startsWith(
|
|
903
|
-
|
|
904
|
+
if (row.id.startsWith(filterPrefix)) {
|
|
905
|
+
// Strip the full prefix to return bare cardId
|
|
906
|
+
ret.push(row.id.substr(filterPrefix.length));
|
|
904
907
|
}
|
|
905
908
|
});
|
|
906
909
|
return ret;
|
|
@@ -124,6 +124,16 @@ export class SessionController<TView = unknown> extends Loggable {
|
|
|
124
124
|
*/
|
|
125
125
|
private _defaultBatchLimit: number = 20;
|
|
126
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Maximum number of reviews enqueued at session start. Reviews live
|
|
129
|
+
* outside the replan flow — the queue drains via consumption and is
|
|
130
|
+
* not refilled mid-session. The session timer caps total review
|
|
131
|
+
* exposure, so overfilling here is intentional. Default is generous
|
|
132
|
+
* to accommodate Anki-style power users with hundreds of due reviews;
|
|
133
|
+
* apps targeting nimbler sessions should override via constructor.
|
|
134
|
+
*/
|
|
135
|
+
private _initialReviewCap: number = 200;
|
|
136
|
+
|
|
127
137
|
private sources: StudyContentSource[];
|
|
128
138
|
// dataLayer and getViewComponent now injected into CardHydrationService
|
|
129
139
|
private _sessionRecord: StudySessionRecord[] = [];
|
|
@@ -208,6 +218,8 @@ export class SessionController<TView = unknown> extends Loggable {
|
|
|
208
218
|
* @param options.defaultBatchLimit - Default pipeline batch size (default: 20).
|
|
209
219
|
* Smaller values for newer users cause more frequent replans, keeping plans
|
|
210
220
|
* aligned with rapidly-changing user state.
|
|
221
|
+
* @param options.initialReviewCap - Max reviews loaded at session start (default: 200).
|
|
222
|
+
* Applied only on initial planning; replans do not refill the review queue.
|
|
211
223
|
*/
|
|
212
224
|
constructor(
|
|
213
225
|
sources: StudyContentSource[],
|
|
@@ -215,7 +227,7 @@ export class SessionController<TView = unknown> extends Loggable {
|
|
|
215
227
|
dataLayer: DataLayerProvider,
|
|
216
228
|
getViewComponent: (viewId: string) => TView,
|
|
217
229
|
mixer?: SourceMixer,
|
|
218
|
-
options?: { defaultBatchLimit?: number }
|
|
230
|
+
options?: { defaultBatchLimit?: number; initialReviewCap?: number }
|
|
219
231
|
) {
|
|
220
232
|
super();
|
|
221
233
|
|
|
@@ -242,11 +254,15 @@ export class SessionController<TView = unknown> extends Loggable {
|
|
|
242
254
|
if (options?.defaultBatchLimit !== undefined) {
|
|
243
255
|
this._defaultBatchLimit = options.defaultBatchLimit;
|
|
244
256
|
}
|
|
257
|
+
if (options?.initialReviewCap !== undefined) {
|
|
258
|
+
this._initialReviewCap = options.initialReviewCap;
|
|
259
|
+
}
|
|
245
260
|
|
|
246
261
|
this.log(`Session constructed:
|
|
247
262
|
startTime: ${this.startTime}
|
|
248
263
|
endTime: ${this.endTime}
|
|
249
|
-
defaultBatchLimit: ${this._defaultBatchLimit}
|
|
264
|
+
defaultBatchLimit: ${this._defaultBatchLimit}
|
|
265
|
+
initialReviewCap: ${this._initialReviewCap}`);
|
|
250
266
|
}
|
|
251
267
|
|
|
252
268
|
private tick() {
|
|
@@ -354,18 +370,21 @@ export class SessionController<TView = unknown> extends Loggable {
|
|
|
354
370
|
return this._replanPromise;
|
|
355
371
|
}
|
|
356
372
|
|
|
357
|
-
//
|
|
358
|
-
//
|
|
373
|
+
// Exclude all cards already presented this session. The pipeline may
|
|
374
|
+
// not yet see their encounter records (async writes), so without this
|
|
375
|
+
// they can re-enter newQ via replaceAll and cause duplicates.
|
|
376
|
+
if (!opts.hints) opts.hints = {};
|
|
377
|
+
const hints = opts.hints;
|
|
378
|
+
const excludeSet = new Set(hints.excludeCards ?? []);
|
|
379
|
+
|
|
359
380
|
if (this._currentCard?.item.cardID) {
|
|
360
|
-
|
|
361
|
-
if (!opts.hints) opts.hints = {};
|
|
362
|
-
const hints = opts.hints;
|
|
363
|
-
const excludeCards = hints.excludeCards ?? [];
|
|
364
|
-
if (!excludeCards.includes(currentId)) {
|
|
365
|
-
excludeCards.push(currentId);
|
|
366
|
-
}
|
|
367
|
-
hints.excludeCards = excludeCards;
|
|
381
|
+
excludeSet.add(this._currentCard.item.cardID);
|
|
368
382
|
}
|
|
383
|
+
for (const rec of this._sessionRecord) {
|
|
384
|
+
excludeSet.add(rec.card.card_id);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
hints.excludeCards = [...excludeSet];
|
|
369
388
|
|
|
370
389
|
// Forward hints to all sources (CourseDB stashes them, Pipeline consumes them)
|
|
371
390
|
if (opts.hints) {
|
|
@@ -583,7 +602,11 @@ export class SessionController<TView = unknown> extends Loggable {
|
|
|
583
602
|
}): Promise<number> {
|
|
584
603
|
const replan = options?.replan ?? false;
|
|
585
604
|
const additive = options?.additive ?? false;
|
|
586
|
-
const
|
|
605
|
+
const newLimit = options?.limit ?? this._defaultBatchLimit;
|
|
606
|
+
// Initial planning inflates the per-source budget so reviews can fill up
|
|
607
|
+
// to _initialReviewCap independently of the new-card budget. Replans
|
|
608
|
+
// never touch reviewQ, so the inflation is unnecessary there.
|
|
609
|
+
const fetchLimit = replan ? newLimit : newLimit + this._initialReviewCap;
|
|
587
610
|
|
|
588
611
|
// Collect batches from each source
|
|
589
612
|
const batches: SourceBatch[] = [];
|
|
@@ -592,7 +615,7 @@ export class SessionController<TView = unknown> extends Loggable {
|
|
|
592
615
|
const source = this.sources[i];
|
|
593
616
|
try {
|
|
594
617
|
// Fetch weighted cards for mixing
|
|
595
|
-
const weighted = (await source.getWeightedCards!(
|
|
618
|
+
const weighted = (await source.getWeightedCards!(fetchLimit)).cards;
|
|
596
619
|
|
|
597
620
|
batches.push({
|
|
598
621
|
sourceIndex: i,
|
|
@@ -621,7 +644,7 @@ export class SessionController<TView = unknown> extends Loggable {
|
|
|
621
644
|
}
|
|
622
645
|
|
|
623
646
|
// Mix weighted cards across sources using configured strategy
|
|
624
|
-
const mixedWeighted = this.mixer.mix(batches,
|
|
647
|
+
const mixedWeighted = this.mixer.mix(batches, fetchLimit * this.sources.length);
|
|
625
648
|
|
|
626
649
|
// Capture mixer run for debugging - fetch course names
|
|
627
650
|
const sourceIds = batches.map((b) => {
|
|
@@ -643,20 +666,27 @@ export class SessionController<TView = unknown> extends Loggable {
|
|
|
643
666
|
);
|
|
644
667
|
const sourceNames = sourceIds.map((id) => this.courseNameCache.get(id));
|
|
645
668
|
const quotaPerSource =
|
|
646
|
-
this.mixer instanceof QuotaRoundRobinMixer ? Math.ceil((
|
|
669
|
+
this.mixer instanceof QuotaRoundRobinMixer ? Math.ceil((fetchLimit * this.sources.length) / batches.length) : undefined;
|
|
647
670
|
captureMixerRun(
|
|
648
671
|
this.mixer.constructor.name,
|
|
649
672
|
batches,
|
|
650
673
|
sourceIds,
|
|
651
674
|
sourceNames,
|
|
652
|
-
|
|
675
|
+
fetchLimit * this.sources.length,
|
|
653
676
|
quotaPerSource,
|
|
654
677
|
mixedWeighted
|
|
655
678
|
);
|
|
656
679
|
|
|
657
|
-
// Split mixed results by card origin
|
|
658
|
-
|
|
659
|
-
|
|
680
|
+
// Split mixed results by card origin, then apply per-origin caps. The
|
|
681
|
+
// pre-mixer fetch is inflated to fit both budgets; trimming here keeps
|
|
682
|
+
// newQ at the nimble batch size while letting reviewQ overfill up to
|
|
683
|
+
// _initialReviewCap (replan path discards reviewWeighted entirely).
|
|
684
|
+
const reviewWeighted = mixedWeighted
|
|
685
|
+
.filter((w) => getCardOrigin(w) === 'review')
|
|
686
|
+
.slice(0, this._initialReviewCap);
|
|
687
|
+
const newWeighted = mixedWeighted
|
|
688
|
+
.filter((w) => getCardOrigin(w) === 'new')
|
|
689
|
+
.slice(0, newLimit);
|
|
660
690
|
|
|
661
691
|
logger.debug(`[reviews] got ${reviewWeighted.length} reviews from mixer`);
|
|
662
692
|
|
|
@@ -173,7 +173,11 @@ export class ResponseProcessor {
|
|
|
173
173
|
// Only schedule and update ELO for first-time attempts
|
|
174
174
|
if (cardRecord.priorAttemps === 0) {
|
|
175
175
|
// Schedule the card for future review based on performance (async, non-blocking)
|
|
176
|
-
|
|
176
|
+
// Cards tagged srs:skip are one-time presentations (e.g. intro cards) — no review scheduling
|
|
177
|
+
const skipSrs = currentCard.card.tags.includes('srs:skip');
|
|
178
|
+
if (!skipSrs) {
|
|
179
|
+
void this.srsService.scheduleReview(history, studySessionItem);
|
|
180
|
+
}
|
|
177
181
|
|
|
178
182
|
// Parse performance (may be numeric or structured)
|
|
179
183
|
const { globalScore, taggedPerformance } = this.parsePerformance(cardRecord.performance);
|