@vue-skuilder/db 0.1.38 → 0.1.39
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 +13 -7
- package/dist/index.d.ts +13 -7
- package/dist/index.js +45 -26
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +45 -26
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/study/SessionController.ts +92 -64
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.39",
|
|
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.39",
|
|
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.39"
|
|
66
66
|
}
|
|
@@ -171,14 +171,6 @@ export class SessionController<TView = unknown> extends Loggable {
|
|
|
171
171
|
*/
|
|
172
172
|
private _suppressQualityReplan: boolean = false;
|
|
173
173
|
|
|
174
|
-
/**
|
|
175
|
-
* Guards against infinite depletion-triggered replans. Set to true
|
|
176
|
-
* when a depletion replan fires; cleared when a replan produces
|
|
177
|
-
* content (newQ.length > 0 after replan) or when an explicit
|
|
178
|
-
* (non-auto) replan is requested.
|
|
179
|
-
*/
|
|
180
|
-
private _depletionReplanAttempted: boolean = false;
|
|
181
|
-
|
|
182
174
|
/**
|
|
183
175
|
* When > 0, the session timer cannot end the session. Decremented on
|
|
184
176
|
* each nextCard() draw. Set by replans that include `minFollowUpCards`.
|
|
@@ -370,12 +362,6 @@ export class SessionController<TView = unknown> extends Loggable {
|
|
|
370
362
|
// Normalise: bare hints object (legacy callers) → ReplanOptions wrapper
|
|
371
363
|
const opts = this.normalizeReplanOptions(options);
|
|
372
364
|
|
|
373
|
-
// Explicit (non-auto) replans clear the depletion guard — the caller
|
|
374
|
-
// is providing fresh intent that may change pipeline results.
|
|
375
|
-
if (opts.hints || opts.label || opts.limit) {
|
|
376
|
-
this._depletionReplanAttempted = false;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
365
|
const hasIntent = this._replanHasIntent(opts);
|
|
380
366
|
|
|
381
367
|
if (this._replanPromise) {
|
|
@@ -495,6 +481,26 @@ export class SessionController<TView = unknown> extends Loggable {
|
|
|
495
481
|
await this._executeReplan(opts);
|
|
496
482
|
}
|
|
497
483
|
|
|
484
|
+
/**
|
|
485
|
+
* Run a replan, bypassing requestReplan()'s coalesce logic.
|
|
486
|
+
*
|
|
487
|
+
* Use this when correctness depends on a *fresh* pipeline run, not on
|
|
488
|
+
* the existence of *some* in-flight replan. Specifically: the
|
|
489
|
+
* wedge-breaker path in nextCard(), where coalescing into a previous
|
|
490
|
+
* run that we now know produced insufficient content would re-create
|
|
491
|
+
* the bug we're trying to prevent.
|
|
492
|
+
*
|
|
493
|
+
* Still tracks _replanPromise like requestReplan() does so concurrent
|
|
494
|
+
* observers (auto-trigger guards in nextCard()) see consistent state.
|
|
495
|
+
*/
|
|
496
|
+
private async _replanUncoalesced(opts: ReplanOptions): Promise<void> {
|
|
497
|
+
const run = this._runReplan(opts);
|
|
498
|
+
this._replanPromise = run.finally(() => {
|
|
499
|
+
if (this._replanPromise === run) this._replanPromise = null;
|
|
500
|
+
});
|
|
501
|
+
await run;
|
|
502
|
+
}
|
|
503
|
+
|
|
498
504
|
/**
|
|
499
505
|
* Normalise the requestReplan argument. Accepts either a ReplanOptions
|
|
500
506
|
* object (new API) or a plain Record<string, unknown> (legacy callers
|
|
@@ -565,12 +571,6 @@ export class SessionController<TView = unknown> extends Loggable {
|
|
|
565
571
|
);
|
|
566
572
|
}
|
|
567
573
|
|
|
568
|
-
// If the replan produced content, clear the depletion guard so future
|
|
569
|
-
// depletions can trigger fresh replans.
|
|
570
|
-
if (this.newQ.length > 0) {
|
|
571
|
-
this._depletionReplanAttempted = false;
|
|
572
|
-
}
|
|
573
|
-
|
|
574
574
|
await this.hydrationService.ensureHydratedCards();
|
|
575
575
|
const labelTag = opts.label ? ` [${opts.label}]` : '';
|
|
576
576
|
this.log(`Replan complete${labelTag}: newQ now has ${this.newQ.length} cards (mode=${mode})`);
|
|
@@ -941,60 +941,45 @@ export class SessionController<TView = unknown> extends Loggable {
|
|
|
941
941
|
await this._replanPromise;
|
|
942
942
|
}
|
|
943
943
|
|
|
944
|
-
// ---
|
|
945
|
-
//
|
|
944
|
+
// --- Replan triggers ---
|
|
945
|
+
//
|
|
946
|
+
// Two flavors:
|
|
947
|
+
//
|
|
948
|
+
// (a) OPPORTUNISTIC PREFETCH (best-effort, may coalesce, may no-op):
|
|
949
|
+
// `depletion` and `quality` triggers below. Their job is to make
|
|
950
|
+
// replans happen *early*, so the user doesn't wait. They are
|
|
951
|
+
// *not* responsible for making replans happen *at all* — if every
|
|
952
|
+
// one of them gets eaten by coalescing or suppression, that's
|
|
953
|
+
// fine. They are perf optimizations.
|
|
946
954
|
//
|
|
947
|
-
//
|
|
948
|
-
//
|
|
949
|
-
//
|
|
950
|
-
//
|
|
951
|
-
// nextCard(), the _replanPromise await at the top ensures the
|
|
952
|
-
// replan has landed before we try to draw.
|
|
953
|
-
// - newQ === 0 && all queues empty: blocking await — nothing else
|
|
954
|
-
// to serve, so we must wait for the pipeline before proceeding.
|
|
955
|
-
// - newQ === 0 && other queues have content: background — the user
|
|
956
|
-
// draws from reviewQ/failedQ while the pipeline runs.
|
|
955
|
+
// (b) LOAD-BEARING WEDGE-BREAKER (correctness):
|
|
956
|
+
// Below, right before the draw loop. Invariant: if the clock is
|
|
957
|
+
// ticking and we'd otherwise serve null, the pipeline runs. No
|
|
958
|
+
// coalesce, no latch, no flag. This is the *only* guarantee.
|
|
957
959
|
//
|
|
958
|
-
//
|
|
959
|
-
//
|
|
960
|
-
// Suppressed after a burst replan to avoid clobbering burst cards.
|
|
960
|
+
// Rule of thumb: a redundant pipeline run is a perf bug, a missing
|
|
961
|
+
// pipeline run is a correctness bug. Bias toward the cheaper failure.
|
|
961
962
|
|
|
962
|
-
//
|
|
963
|
-
//
|
|
964
|
-
//
|
|
963
|
+
// Opportunistic depletion: newQ running dry → background prefetch.
|
|
964
|
+
// No latch — if this fires repeatedly when the pipeline keeps coming
|
|
965
|
+
// back empty, the wedge-breaker's local backoff handles spin protection.
|
|
965
966
|
if (
|
|
966
967
|
this.newQ.length <= 1 &&
|
|
967
968
|
this._secondsRemaining > 0 &&
|
|
968
|
-
!this._replanPromise
|
|
969
|
-
!this._depletionReplanAttempted
|
|
969
|
+
!this._replanPromise
|
|
970
970
|
) {
|
|
971
|
-
this._suppressQualityReplan = false; // burst is (nearly) consumed
|
|
972
|
-
this._depletionReplanAttempted = true;
|
|
973
|
-
|
|
971
|
+
this._suppressQualityReplan = false; // burst is (nearly) consumed
|
|
974
972
|
const otherContent = this.reviewQ.length + this.failedQ.length;
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
);
|
|
982
|
-
await this.requestReplan();
|
|
983
|
-
} else {
|
|
984
|
-
// Either 1 card remains (look-ahead) or other queues can cover.
|
|
985
|
-
// Fire in background — pipeline runs while the user works.
|
|
986
|
-
this.log(
|
|
987
|
-
`[AutoReplan:depletion] newQ has ${this.newQ.length} card(s) ` +
|
|
988
|
-
`(${otherContent} in other queues) with ${this._secondsRemaining}s remaining. ` +
|
|
989
|
-
`Triggering background replan.`
|
|
990
|
-
);
|
|
991
|
-
void this.requestReplan();
|
|
992
|
-
}
|
|
973
|
+
this.log(
|
|
974
|
+
`[AutoReplan:depletion] newQ has ${this.newQ.length} card(s) ` +
|
|
975
|
+
`(${otherContent} in other queues) with ${this._secondsRemaining}s remaining. ` +
|
|
976
|
+
`Triggering background replan.`
|
|
977
|
+
);
|
|
978
|
+
void this.requestReplan();
|
|
993
979
|
}
|
|
994
980
|
|
|
995
|
-
//
|
|
996
|
-
//
|
|
997
|
-
// Suppressed after a burst replan to avoid clobbering burst cards.
|
|
981
|
+
// Opportunistic quality: few well-indicated cards remain.
|
|
982
|
+
// Suppressed after a burst replan to avoid clobbering burst cards.
|
|
998
983
|
const REPLAN_BUFFER = 3;
|
|
999
984
|
if (
|
|
1000
985
|
!this._suppressQualityReplan &&
|
|
@@ -1015,6 +1000,49 @@ export class SessionController<TView = unknown> extends Loggable {
|
|
|
1015
1000
|
return null;
|
|
1016
1001
|
}
|
|
1017
1002
|
|
|
1003
|
+
// Wedge-breaker (correctness path).
|
|
1004
|
+
//
|
|
1005
|
+
// If we'd otherwise be about to draw from empty queues with time on
|
|
1006
|
+
// the clock, run the pipeline. Bypasses requestReplan() coalesce
|
|
1007
|
+
// because coalescing into a previous run that we now know produced
|
|
1008
|
+
// insufficient content is the exact failure mode this defends against.
|
|
1009
|
+
//
|
|
1010
|
+
// Bounded by an empty-streak counter: if the pipeline consistently
|
|
1011
|
+
// returns nothing, we eventually give up and let the session end
|
|
1012
|
+
// gracefully rather than spin forever.
|
|
1013
|
+
const WEDGE_MAX_EMPTY_STREAK = 3;
|
|
1014
|
+
const WEDGE_BACKOFF_MS = 250;
|
|
1015
|
+
let wedgeEmptyStreak = 0;
|
|
1016
|
+
while (
|
|
1017
|
+
this._secondsRemaining > 0 &&
|
|
1018
|
+
this.newQ.length === 0 &&
|
|
1019
|
+
this.reviewQ.length === 0 &&
|
|
1020
|
+
this.failedQ.length === 0
|
|
1021
|
+
) {
|
|
1022
|
+
this.log(
|
|
1023
|
+
`[WedgeBreaker] All queues empty with ${this._secondsRemaining}s remaining. ` +
|
|
1024
|
+
`Running pipeline (attempt ${wedgeEmptyStreak + 1}/${WEDGE_MAX_EMPTY_STREAK}).`
|
|
1025
|
+
);
|
|
1026
|
+
await this._replanUncoalesced({ label: 'wedge-breaker' });
|
|
1027
|
+
if (
|
|
1028
|
+
this.newQ.length === 0 &&
|
|
1029
|
+
this.reviewQ.length === 0 &&
|
|
1030
|
+
this.failedQ.length === 0
|
|
1031
|
+
) {
|
|
1032
|
+
wedgeEmptyStreak++;
|
|
1033
|
+
if (wedgeEmptyStreak >= WEDGE_MAX_EMPTY_STREAK) {
|
|
1034
|
+
this.log(
|
|
1035
|
+
`[WedgeBreaker] Pipeline returned no content ${WEDGE_MAX_EMPTY_STREAK} consecutive ` +
|
|
1036
|
+
`times. Giving up; session will end.`
|
|
1037
|
+
);
|
|
1038
|
+
break;
|
|
1039
|
+
}
|
|
1040
|
+
await new Promise((resolve) => setTimeout(resolve, WEDGE_BACKOFF_MS));
|
|
1041
|
+
} else {
|
|
1042
|
+
wedgeEmptyStreak = 0;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1018
1046
|
// Try multiple cards in case some fail hydration (e.g., deleted from DB)
|
|
1019
1047
|
const MAX_SKIP = 20;
|
|
1020
1048
|
for (let attempt = 0; attempt < MAX_SKIP; attempt++) {
|