@vue-skuilder/db 0.2.16 → 0.2.18
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/{SyncStrategy-CyATpyLQ.d.cts → SyncStrategy-BJMZq3WO.d.cts} +17 -0
- package/dist/{SyncStrategy-CyATpyLQ.d.ts → SyncStrategy-BJMZq3WO.d.ts} +17 -0
- package/dist/{contentSource-Brz42x7n.d.cts → contentSource-CBqZCoGU.d.cts} +85 -1
- package/dist/{contentSource-B1p-vdz7.d.ts → contentSource-CudEz5Tm.d.ts} +85 -1
- package/dist/core/index.d.cts +51 -4
- package/dist/core/index.d.ts +51 -4
- package/dist/core/index.js +258 -7
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +254 -7
- package/dist/core/index.mjs.map +1 -1
- package/dist/{dataLayerProvider-CpwpT1rM.d.cts → dataLayerProvider-BBA8tJNx.d.cts} +1 -1
- package/dist/{dataLayerProvider-BWayUIoK.d.ts → dataLayerProvider-C9WgkBzR.d.ts} +1 -1
- package/dist/impl/couch/index.d.cts +16 -3
- package/dist/impl/couch/index.d.ts +16 -3
- package/dist/impl/couch/index.js +284 -9
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +284 -9
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.d.cts +3 -3
- package/dist/impl/static/index.d.ts +3 -3
- package/dist/impl/static/index.js +250 -7
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +250 -7
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/index.d.cts +8 -6
- package/dist/index.d.ts +8 -6
- package/dist/index.js +354 -10
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +350 -10
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -3
- package/src/core/index.ts +1 -0
- package/src/core/interfaces/userDB.ts +11 -0
- package/src/core/navigators/StrategyPressureDebugger.ts +76 -0
- package/src/core/navigators/generators/prescribed.ts +122 -7
- package/src/core/navigators/index.ts +12 -0
- package/src/core/types/hydration.ts +78 -0
- package/src/impl/common/BaseUserDB.ts +202 -2
- package/src/impl/common/SyncStrategy.ts +19 -0
- package/src/impl/couch/CouchDBSyncStrategy.ts +54 -4
- package/src/study/SessionController.ts +7 -1
- package/src/study/SessionDebugger.ts +2 -0
- package/src/study/SessionOverlay.ts +113 -0
- package/tests/impl/hydration.test.ts +281 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { U as UserDBInterface, a as UserDBReader, C as CourseDBInterface, b as CoursesDBInterface, c as ClassroomDBInterface, A as AdminDBInterface } from './contentSource-
|
|
1
|
+
import { U as UserDBInterface, a as UserDBReader, C as CourseDBInterface, b as CoursesDBInterface, c as ClassroomDBInterface, A as AdminDBInterface } from './contentSource-CBqZCoGU.cjs';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Main factory interface for data access
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { U as UserDBInterface, a as UserDBReader, C as CourseDBInterface, b as CoursesDBInterface, c as ClassroomDBInterface, A as AdminDBInterface } from './contentSource-
|
|
1
|
+
import { U as UserDBInterface, a as UserDBReader, C as CourseDBInterface, b as CoursesDBInterface, c as ClassroomDBInterface, A as AdminDBInterface } from './contentSource-CudEz5Tm.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Main factory interface for data access
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { T as TagStub, a as Tag, S as SkuilderCourseData, Q as QualifiedCardID } from '../../types-legacy-4tlwHnXo.cjs';
|
|
2
2
|
import { Moment } from 'moment';
|
|
3
|
-
import { A as AdminDBInterface, g as AssignedContent, h as StudyContentSource, i as StudentClassroomDBInterface, U as UserDBInterface, G as GeneratorResult, T as TeacherClassroomDBInterface, b as CoursesDBInterface, C as CourseDBInterface, d as CourseInfo, D as DataLayerResult, e as ContentNavigationStrategyData, f as ContentNavigator, R as ReplanHints, S as StudySessionItem, j as ScheduledCard } from '../../contentSource-
|
|
4
|
-
export { q as ContentSourceID, k as StudySessionFailedItem, l as StudySessionFailedNewItem, m as StudySessionFailedReviewItem, n as StudySessionNewItem, o as StudySessionReviewItem, r as getStudySource, p as isReview } from '../../contentSource-
|
|
3
|
+
import { A as AdminDBInterface, g as AssignedContent, h as StudyContentSource, i as StudentClassroomDBInterface, U as UserDBInterface, G as GeneratorResult, T as TeacherClassroomDBInterface, b as CoursesDBInterface, C as CourseDBInterface, d as CourseInfo, D as DataLayerResult, e as ContentNavigationStrategyData, f as ContentNavigator, R as ReplanHints, S as StudySessionItem, j as ScheduledCard } from '../../contentSource-CBqZCoGU.cjs';
|
|
4
|
+
export { q as ContentSourceID, k as StudySessionFailedItem, l as StudySessionFailedNewItem, m as StudySessionFailedReviewItem, n as StudySessionNewItem, o as StudySessionReviewItem, r as getStudySource, p as isReview } from '../../contentSource-CBqZCoGU.cjs';
|
|
5
5
|
import * as _vue_skuilder_common from '@vue-skuilder/common';
|
|
6
6
|
import { ClassroomConfig, DataShape, CourseElo, CourseConfig as CourseConfig$1 } from '@vue-skuilder/common';
|
|
7
|
-
import { S as SyncStrategy, A as AccountCreationResult, a as AuthenticationResult } from '../../SyncStrategy-
|
|
7
|
+
import { S as SyncStrategy, A as AccountCreationResult, a as AuthenticationResult } from '../../SyncStrategy-BJMZq3WO.cjs';
|
|
8
8
|
|
|
9
9
|
type NamespacedDatashape = string;
|
|
10
10
|
interface DataShape55 {
|
|
@@ -497,8 +497,21 @@ declare class CourseSyncService {
|
|
|
497
497
|
*/
|
|
498
498
|
declare class CouchDBSyncStrategy implements SyncStrategy {
|
|
499
499
|
private syncHandle?;
|
|
500
|
+
/** In-flight one-shot hydration pull, so a timed-out pull can be abandoned. */
|
|
501
|
+
private hydrationHandle?;
|
|
500
502
|
setupRemoteDB(username: string): PouchDB.Database;
|
|
501
503
|
getWriteDB(username: string): PouchDB.Database;
|
|
504
|
+
/**
|
|
505
|
+
* One-shot remote → local pull. Resolves once the local mirror is caught up.
|
|
506
|
+
*
|
|
507
|
+
* Pull only: pushing here would upload whatever the local DB happens to hold
|
|
508
|
+
* before we know what the remote already has, which is the conflict-leaf
|
|
509
|
+
* problem hydration exists to avoid. Local changes go up when startSync()
|
|
510
|
+
* takes over.
|
|
511
|
+
*/
|
|
512
|
+
hydrate(localDB: PouchDB.Database, remoteDB: PouchDB.Database): Promise<{
|
|
513
|
+
docsWritten: number;
|
|
514
|
+
}>;
|
|
502
515
|
startSync(localDB: PouchDB.Database, remoteDB: PouchDB.Database): void;
|
|
503
516
|
stopSync?(): void;
|
|
504
517
|
canCreateAccount(): boolean;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { T as TagStub, a as Tag, S as SkuilderCourseData, Q as QualifiedCardID } from '../../types-legacy-4tlwHnXo.js';
|
|
2
2
|
import { Moment } from 'moment';
|
|
3
|
-
import { A as AdminDBInterface, g as AssignedContent, h as StudyContentSource, i as StudentClassroomDBInterface, U as UserDBInterface, G as GeneratorResult, T as TeacherClassroomDBInterface, b as CoursesDBInterface, C as CourseDBInterface, d as CourseInfo, D as DataLayerResult, e as ContentNavigationStrategyData, f as ContentNavigator, R as ReplanHints, S as StudySessionItem, j as ScheduledCard } from '../../contentSource-
|
|
4
|
-
export { q as ContentSourceID, k as StudySessionFailedItem, l as StudySessionFailedNewItem, m as StudySessionFailedReviewItem, n as StudySessionNewItem, o as StudySessionReviewItem, r as getStudySource, p as isReview } from '../../contentSource-
|
|
3
|
+
import { A as AdminDBInterface, g as AssignedContent, h as StudyContentSource, i as StudentClassroomDBInterface, U as UserDBInterface, G as GeneratorResult, T as TeacherClassroomDBInterface, b as CoursesDBInterface, C as CourseDBInterface, d as CourseInfo, D as DataLayerResult, e as ContentNavigationStrategyData, f as ContentNavigator, R as ReplanHints, S as StudySessionItem, j as ScheduledCard } from '../../contentSource-CudEz5Tm.js';
|
|
4
|
+
export { q as ContentSourceID, k as StudySessionFailedItem, l as StudySessionFailedNewItem, m as StudySessionFailedReviewItem, n as StudySessionNewItem, o as StudySessionReviewItem, r as getStudySource, p as isReview } from '../../contentSource-CudEz5Tm.js';
|
|
5
5
|
import * as _vue_skuilder_common from '@vue-skuilder/common';
|
|
6
6
|
import { ClassroomConfig, DataShape, CourseElo, CourseConfig as CourseConfig$1 } from '@vue-skuilder/common';
|
|
7
|
-
import { S as SyncStrategy, A as AccountCreationResult, a as AuthenticationResult } from '../../SyncStrategy-
|
|
7
|
+
import { S as SyncStrategy, A as AccountCreationResult, a as AuthenticationResult } from '../../SyncStrategy-BJMZq3WO.js';
|
|
8
8
|
|
|
9
9
|
type NamespacedDatashape = string;
|
|
10
10
|
interface DataShape55 {
|
|
@@ -497,8 +497,21 @@ declare class CourseSyncService {
|
|
|
497
497
|
*/
|
|
498
498
|
declare class CouchDBSyncStrategy implements SyncStrategy {
|
|
499
499
|
private syncHandle?;
|
|
500
|
+
/** In-flight one-shot hydration pull, so a timed-out pull can be abandoned. */
|
|
501
|
+
private hydrationHandle?;
|
|
500
502
|
setupRemoteDB(username: string): PouchDB.Database;
|
|
501
503
|
getWriteDB(username: string): PouchDB.Database;
|
|
504
|
+
/**
|
|
505
|
+
* One-shot remote → local pull. Resolves once the local mirror is caught up.
|
|
506
|
+
*
|
|
507
|
+
* Pull only: pushing here would upload whatever the local DB happens to hold
|
|
508
|
+
* before we know what the remote already has, which is the conflict-leaf
|
|
509
|
+
* problem hydration exists to avoid. Local changes go up when startSync()
|
|
510
|
+
* takes over.
|
|
511
|
+
*/
|
|
512
|
+
hydrate(localDB: PouchDB.Database, remoteDB: PouchDB.Database): Promise<{
|
|
513
|
+
docsWritten: number;
|
|
514
|
+
}>;
|
|
502
515
|
startSync(localDB: PouchDB.Database, remoteDB: PouchDB.Database): void;
|
|
503
516
|
stopSync?(): void;
|
|
504
517
|
canCreateAccount(): boolean;
|
package/dist/impl/couch/index.js
CHANGED
|
@@ -1588,6 +1588,30 @@ var init_SrsDebugger = __esm({
|
|
|
1588
1588
|
}
|
|
1589
1589
|
});
|
|
1590
1590
|
|
|
1591
|
+
// src/core/navigators/StrategyPressureDebugger.ts
|
|
1592
|
+
var StrategyPressureDebugger_exports = {};
|
|
1593
|
+
__export(StrategyPressureDebugger_exports, {
|
|
1594
|
+
captureStrategyPressure: () => captureStrategyPressure,
|
|
1595
|
+
clearStrategyPressureDebug: () => clearStrategyPressureDebug,
|
|
1596
|
+
getStrategyPressureDebug: () => getStrategyPressureDebug
|
|
1597
|
+
});
|
|
1598
|
+
function captureStrategyPressure(snapshot) {
|
|
1599
|
+
snapshots2.set(`${snapshot.source}:${snapshot.courseId}`, snapshot);
|
|
1600
|
+
}
|
|
1601
|
+
function getStrategyPressureDebug() {
|
|
1602
|
+
return [...snapshots2.values()].sort((a, b) => b.timestamp - a.timestamp);
|
|
1603
|
+
}
|
|
1604
|
+
function clearStrategyPressureDebug() {
|
|
1605
|
+
snapshots2.clear();
|
|
1606
|
+
}
|
|
1607
|
+
var snapshots2;
|
|
1608
|
+
var init_StrategyPressureDebugger = __esm({
|
|
1609
|
+
"src/core/navigators/StrategyPressureDebugger.ts"() {
|
|
1610
|
+
"use strict";
|
|
1611
|
+
snapshots2 = /* @__PURE__ */ new Map();
|
|
1612
|
+
}
|
|
1613
|
+
});
|
|
1614
|
+
|
|
1591
1615
|
// src/core/navigators/generators/CompositeGenerator.ts
|
|
1592
1616
|
var CompositeGenerator_exports = {};
|
|
1593
1617
|
__export(CompositeGenerator_exports, {
|
|
@@ -1952,6 +1976,13 @@ function shuffleInPlace(arr) {
|
|
|
1952
1976
|
[arr[i], arr[j]] = [arr[j], arr[i]];
|
|
1953
1977
|
}
|
|
1954
1978
|
}
|
|
1979
|
+
function formatAge(ms) {
|
|
1980
|
+
const minutes = Math.max(0, Math.round(ms / 6e4));
|
|
1981
|
+
if (minutes < 60) return `${minutes}m`;
|
|
1982
|
+
const hours = Math.round(minutes / 60);
|
|
1983
|
+
if (hours < 24) return `${hours}h`;
|
|
1984
|
+
return `${Math.round(hours / 24)}d`;
|
|
1985
|
+
}
|
|
1955
1986
|
function pickTopByScore(cards, limit) {
|
|
1956
1987
|
return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
|
|
1957
1988
|
}
|
|
@@ -1960,6 +1991,7 @@ var init_prescribed = __esm({
|
|
|
1960
1991
|
"src/core/navigators/generators/prescribed.ts"() {
|
|
1961
1992
|
"use strict";
|
|
1962
1993
|
init_navigators();
|
|
1994
|
+
init_StrategyPressureDebugger();
|
|
1963
1995
|
init_logger();
|
|
1964
1996
|
DEFAULT_FRESHNESS_WINDOW = 3;
|
|
1965
1997
|
DEFAULT_MAX_DIRECT_PER_RUN = 3;
|
|
@@ -2036,6 +2068,7 @@ var init_prescribed = __esm({
|
|
|
2036
2068
|
const groupRuntimes = [];
|
|
2037
2069
|
const priorPracticeDebt = progress.practiceDebt ?? {};
|
|
2038
2070
|
const nextPracticeDebt = {};
|
|
2071
|
+
const practiceDebtsByGroup = /* @__PURE__ */ new Map();
|
|
2039
2072
|
for (const group of this.config.groups) {
|
|
2040
2073
|
const runtime = this.buildGroupRuntimeState({
|
|
2041
2074
|
group,
|
|
@@ -2084,7 +2117,7 @@ var init_prescribed = __esm({
|
|
|
2084
2117
|
courseId,
|
|
2085
2118
|
emittedIds
|
|
2086
2119
|
);
|
|
2087
|
-
const
|
|
2120
|
+
const practice = this.buildPracticeCards({
|
|
2088
2121
|
group,
|
|
2089
2122
|
courseId,
|
|
2090
2123
|
emittedIds,
|
|
@@ -2097,7 +2130,8 @@ var init_prescribed = __esm({
|
|
|
2097
2130
|
priorPracticeDebt,
|
|
2098
2131
|
nextPracticeDebt
|
|
2099
2132
|
});
|
|
2100
|
-
|
|
2133
|
+
practiceDebtsByGroup.set(group.id, practice.debts);
|
|
2134
|
+
emitted.push(...directCards, ...supportCards, ...discoveredSupportCards, ...practice.cards);
|
|
2101
2135
|
}
|
|
2102
2136
|
nextState.practiceDebt = nextPracticeDebt;
|
|
2103
2137
|
const hintSummary = this.buildSupportHintSummary(groupRuntimes);
|
|
@@ -2113,6 +2147,7 @@ var init_prescribed = __esm({
|
|
|
2113
2147
|
} else {
|
|
2114
2148
|
logger.info("[Prescribed] No hints to emit (no blocked targets or no support tags)");
|
|
2115
2149
|
}
|
|
2150
|
+
this.capturePressureSnapshot(courseId, groupRuntimes, practiceDebtsByGroup, emitted, hints);
|
|
2116
2151
|
if (emitted.length === 0) {
|
|
2117
2152
|
logger.info(
|
|
2118
2153
|
"[Prescribed] 0 cards emitted (all targets blocked, authored/discovered support candidates exhausted)" + (hints ? " \u2014 boost hints emitted but may not survive filters" : "")
|
|
@@ -2174,6 +2209,60 @@ var init_prescribed = __esm({
|
|
|
2174
2209
|
supportTags: [...supportTags].sort()
|
|
2175
2210
|
};
|
|
2176
2211
|
}
|
|
2212
|
+
/**
|
|
2213
|
+
* Translate this run's per-group runtimes and practice debts into gauges on
|
|
2214
|
+
* the generic strategy-pressure debug channel (rendered by the live session
|
|
2215
|
+
* overlay's "strategy backpressure" section).
|
|
2216
|
+
*/
|
|
2217
|
+
capturePressureSnapshot(courseId, groupRuntimes, practiceDebtsByGroup, emitted, hints) {
|
|
2218
|
+
const now = Date.now();
|
|
2219
|
+
const gauges = [];
|
|
2220
|
+
for (const runtime of groupRuntimes) {
|
|
2221
|
+
const group = runtime.group;
|
|
2222
|
+
const window2 = group.freshnessWindowSessions ?? DEFAULT_FRESHNESS_WINDOW;
|
|
2223
|
+
gauges.push({
|
|
2224
|
+
id: `group:${group.id}:target`,
|
|
2225
|
+
label: `${group.id} targets`,
|
|
2226
|
+
multiplier: runtime.pressureMultiplier,
|
|
2227
|
+
max: MAX_TARGET_MULTIPLIER,
|
|
2228
|
+
detail: `${runtime.pendingTargets.length} pending (${runtime.surfaceableTargets.length} surfaceable, ${runtime.blockedTargets.length} blocked) \xB7 sinceSurfaced ${runtime.sessionsSinceSurfaced}/${window2}`,
|
|
2229
|
+
items: runtime.blockedTargets.map((cardId) => ({ label: cardId, value: "blocked" }))
|
|
2230
|
+
});
|
|
2231
|
+
if (runtime.blockedTargets.length > 0) {
|
|
2232
|
+
const mode = runtime.supportCandidates.length > 0 ? "direct-support" : runtime.discoveredSupportCandidates.length > 0 ? "inserted-support" : "boost-only";
|
|
2233
|
+
gauges.push({
|
|
2234
|
+
id: `group:${group.id}:support`,
|
|
2235
|
+
label: `${group.id} support`,
|
|
2236
|
+
multiplier: runtime.supportMultiplier,
|
|
2237
|
+
max: MAX_SUPPORT_MULTIPLIER,
|
|
2238
|
+
detail: `mode=${mode} \xB7 ${runtime.supportTags.length} support tag(s)`,
|
|
2239
|
+
items: runtime.supportTags.map((tag) => ({ label: tag }))
|
|
2240
|
+
});
|
|
2241
|
+
}
|
|
2242
|
+
const debts = practiceDebtsByGroup.get(group.id) ?? [];
|
|
2243
|
+
if (debts.length > 0) {
|
|
2244
|
+
gauges.push({
|
|
2245
|
+
id: `group:${group.id}:practice-debt`,
|
|
2246
|
+
label: `${group.id} practice debt`,
|
|
2247
|
+
multiplier: Math.max(...debts.map((d) => d.multiplier)),
|
|
2248
|
+
max: MAX_PRACTICE_MULTIPLIER,
|
|
2249
|
+
detail: `${debts.length} under-practiced skill(s)`,
|
|
2250
|
+
items: debts.map((d) => ({
|
|
2251
|
+
label: d.tag,
|
|
2252
|
+
value: `\xD7${d.multiplier.toFixed(2)} \xB7 open ${formatAge(now - new Date(d.firstOwedAt).getTime())}`
|
|
2253
|
+
}))
|
|
2254
|
+
});
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
captureStrategyPressure({
|
|
2258
|
+
source: "prescribed",
|
|
2259
|
+
courseId,
|
|
2260
|
+
gauges,
|
|
2261
|
+
topScore: emitted.length > 0 ? Math.max(...emitted.map((c) => c.score)) : null,
|
|
2262
|
+
hintsLabel: hints?._label,
|
|
2263
|
+
timestamp: now
|
|
2264
|
+
});
|
|
2265
|
+
}
|
|
2177
2266
|
parseConfig(serializedData) {
|
|
2178
2267
|
try {
|
|
2179
2268
|
const parsed = JSON.parse(serializedData);
|
|
@@ -2330,6 +2419,7 @@ var init_prescribed = __esm({
|
|
|
2330
2419
|
supportTags: [...supportTags],
|
|
2331
2420
|
pressureMultiplier,
|
|
2332
2421
|
supportMultiplier,
|
|
2422
|
+
sessionsSinceSurfaced,
|
|
2333
2423
|
debugVersion: PRESCRIBED_DEBUG_VERSION
|
|
2334
2424
|
};
|
|
2335
2425
|
}
|
|
@@ -2462,13 +2552,13 @@ var init_prescribed = __esm({
|
|
|
2462
2552
|
nextPracticeDebt
|
|
2463
2553
|
} = args;
|
|
2464
2554
|
const patterns = group.practiceTagPatterns ?? [];
|
|
2465
|
-
if (patterns.length === 0) return [];
|
|
2555
|
+
if (patterns.length === 0) return { cards: [], debts: [] };
|
|
2466
2556
|
const practiceMinCount = group.practiceMinCount ?? DEFAULT_PRACTICE_MIN_COUNT;
|
|
2467
2557
|
const maxPractice = group.maxPracticeCardsPerRun ?? DEFAULT_MAX_PRACTICE_PER_RUN;
|
|
2468
2558
|
const practiceTags = [...cardsByTag.keys()].filter(
|
|
2469
2559
|
(tag) => patterns.some((p) => matchesTagPattern(tag, p)) && this.isUnlockedGatedSkill(tag, hierarchyConfigs, userTagElo, userGlobalElo) && (userTagElo[tag]?.count ?? 0) < practiceMinCount
|
|
2470
2560
|
);
|
|
2471
|
-
if (practiceTags.length === 0) return [];
|
|
2561
|
+
if (practiceTags.length === 0) return { cards: [], debts: [] };
|
|
2472
2562
|
const now = Date.now();
|
|
2473
2563
|
const DAY_MS = 24 * 60 * 60 * 1e3;
|
|
2474
2564
|
const tagMultiplier = /* @__PURE__ */ new Map();
|
|
@@ -2483,6 +2573,11 @@ var init_prescribed = __esm({
|
|
|
2483
2573
|
);
|
|
2484
2574
|
tagMultiplier.set(tag, mult);
|
|
2485
2575
|
}
|
|
2576
|
+
const debts = practiceTags.map((tag) => ({
|
|
2577
|
+
tag,
|
|
2578
|
+
multiplier: tagMultiplier.get(tag) ?? PRACTICE_BASE_MULT,
|
|
2579
|
+
firstOwedAt: nextPracticeDebt[tag]
|
|
2580
|
+
})).sort((a, b) => b.multiplier - a.multiplier || a.tag.localeCompare(b.tag));
|
|
2486
2581
|
const practiceCardIds = this.findDiscoveredSupportCards({
|
|
2487
2582
|
supportTags: practiceTags,
|
|
2488
2583
|
cardsByTag,
|
|
@@ -2491,7 +2586,7 @@ var init_prescribed = __esm({
|
|
|
2491
2586
|
excludedIds: emittedIds,
|
|
2492
2587
|
limit: maxPractice
|
|
2493
2588
|
});
|
|
2494
|
-
if (practiceCardIds.length === 0) return [];
|
|
2589
|
+
if (practiceCardIds.length === 0) return { cards: [], debts };
|
|
2495
2590
|
logger.info(
|
|
2496
2591
|
`[Prescribed] Group '${group.id}' practice: ${practiceTags.length} unlocked under-practiced skill(s), emitting ${practiceCardIds.length} drill card(s)`
|
|
2497
2592
|
);
|
|
@@ -2521,7 +2616,7 @@ var init_prescribed = __esm({
|
|
|
2521
2616
|
]
|
|
2522
2617
|
});
|
|
2523
2618
|
}
|
|
2524
|
-
return cards;
|
|
2619
|
+
return { cards, debts };
|
|
2525
2620
|
}
|
|
2526
2621
|
/**
|
|
2527
2622
|
* True for a skill that was *gated and is now reached*: it has at least one
|
|
@@ -4952,6 +5047,7 @@ var init_3 = __esm({
|
|
|
4952
5047
|
"./PipelineAssembler.ts": () => Promise.resolve().then(() => (init_PipelineAssembler(), PipelineAssembler_exports)),
|
|
4953
5048
|
"./PipelineDebugger.ts": () => Promise.resolve().then(() => (init_PipelineDebugger(), PipelineDebugger_exports)),
|
|
4954
5049
|
"./SrsDebugger.ts": () => Promise.resolve().then(() => (init_SrsDebugger(), SrsDebugger_exports)),
|
|
5050
|
+
"./StrategyPressureDebugger.ts": () => Promise.resolve().then(() => (init_StrategyPressureDebugger(), StrategyPressureDebugger_exports)),
|
|
4955
5051
|
"./defaults.ts": () => Promise.resolve().then(() => (init_defaults(), defaults_exports)),
|
|
4956
5052
|
"./diversityRerank.ts": () => Promise.resolve().then(() => (init_diversityRerank(), diversityRerank_exports)),
|
|
4957
5053
|
"./filters/WeightedFilter.ts": () => Promise.resolve().then(() => (init_WeightedFilter(), WeightedFilter_exports)),
|
|
@@ -4984,7 +5080,9 @@ __export(navigators_exports, {
|
|
|
4984
5080
|
NavigatorRole: () => NavigatorRole,
|
|
4985
5081
|
NavigatorRoles: () => NavigatorRoles,
|
|
4986
5082
|
Navigators: () => Navigators,
|
|
5083
|
+
captureStrategyPressure: () => captureStrategyPressure,
|
|
4987
5084
|
clearSrsBacklogDebug: () => clearSrsBacklogDebug,
|
|
5085
|
+
clearStrategyPressureDebug: () => clearStrategyPressureDebug,
|
|
4988
5086
|
diversityRerank: () => diversityRerank,
|
|
4989
5087
|
getActivePipeline: () => getActivePipeline,
|
|
4990
5088
|
getCardOrigin: () => getCardOrigin,
|
|
@@ -4992,6 +5090,7 @@ __export(navigators_exports, {
|
|
|
4992
5090
|
getRegisteredNavigatorNames: () => getRegisteredNavigatorNames,
|
|
4993
5091
|
getRegisteredNavigatorRole: () => getRegisteredNavigatorRole,
|
|
4994
5092
|
getSrsBacklogDebug: () => getSrsBacklogDebug,
|
|
5093
|
+
getStrategyPressureDebug: () => getStrategyPressureDebug,
|
|
4995
5094
|
hasRegisteredNavigator: () => hasRegisteredNavigator,
|
|
4996
5095
|
initializeNavigatorRegistry: () => initializeNavigatorRegistry,
|
|
4997
5096
|
isFilter: () => isFilter,
|
|
@@ -5074,6 +5173,7 @@ var init_navigators = __esm({
|
|
|
5074
5173
|
init_diversityRerank();
|
|
5075
5174
|
init_PipelineDebugger();
|
|
5076
5175
|
init_SrsDebugger();
|
|
5176
|
+
init_StrategyPressureDebugger();
|
|
5077
5177
|
init_logger();
|
|
5078
5178
|
init_();
|
|
5079
5179
|
init_2();
|
|
@@ -6648,6 +6748,15 @@ var init_userOutcome = __esm({
|
|
|
6648
6748
|
}
|
|
6649
6749
|
});
|
|
6650
6750
|
|
|
6751
|
+
// src/core/types/hydration.ts
|
|
6752
|
+
var HYDRATION_MARKER_ID;
|
|
6753
|
+
var init_hydration = __esm({
|
|
6754
|
+
"src/core/types/hydration.ts"() {
|
|
6755
|
+
"use strict";
|
|
6756
|
+
HYDRATION_MARKER_ID = "_local/hydration";
|
|
6757
|
+
}
|
|
6758
|
+
});
|
|
6759
|
+
|
|
6651
6760
|
// src/core/util/index.ts
|
|
6652
6761
|
function getCardHistoryID(courseID, cardID) {
|
|
6653
6762
|
return `${DocTypePrefixes["CARDRECORD" /* CARDRECORD */]}-${courseID}-${cardID}`;
|
|
@@ -7145,6 +7254,7 @@ var init_core = __esm({
|
|
|
7145
7254
|
init_user();
|
|
7146
7255
|
init_strategyState();
|
|
7147
7256
|
init_userOutcome();
|
|
7257
|
+
init_hydration();
|
|
7148
7258
|
init_Loggable();
|
|
7149
7259
|
init_util();
|
|
7150
7260
|
init_navigators();
|
|
@@ -7218,6 +7328,19 @@ var init_user_course_relDB = __esm({
|
|
|
7218
7328
|
});
|
|
7219
7329
|
|
|
7220
7330
|
// src/impl/common/BaseUserDB.ts
|
|
7331
|
+
function withTimeout(p, ms, label) {
|
|
7332
|
+
let timer;
|
|
7333
|
+
return Promise.race([
|
|
7334
|
+
p,
|
|
7335
|
+
new Promise((_resolve, reject) => {
|
|
7336
|
+
timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
|
|
7337
|
+
})
|
|
7338
|
+
]).finally(() => {
|
|
7339
|
+
if (timer !== void 0) {
|
|
7340
|
+
clearTimeout(timer);
|
|
7341
|
+
}
|
|
7342
|
+
});
|
|
7343
|
+
}
|
|
7221
7344
|
function accomodateGuest() {
|
|
7222
7345
|
logger.log("[funnel] accomodateGuest() called");
|
|
7223
7346
|
if (typeof localStorage === "undefined") {
|
|
@@ -7391,7 +7514,7 @@ async function dropUserFromClassroom(user, classID) {
|
|
|
7391
7514
|
async function getUserClassrooms(user) {
|
|
7392
7515
|
return getOrCreateClassroomRegistrationsDoc(user);
|
|
7393
7516
|
}
|
|
7394
|
-
var import_common13, import_moment5, log3, BaseUser, userCoursesDoc, userClassroomsDoc;
|
|
7517
|
+
var import_common13, import_moment5, log3, HYDRATION_TIMEOUT_MS, BaseUser, userCoursesDoc, userClassroomsDoc;
|
|
7395
7518
|
var init_BaseUserDB = __esm({
|
|
7396
7519
|
"src/impl/common/BaseUserDB.ts"() {
|
|
7397
7520
|
"use strict";
|
|
@@ -7408,6 +7531,7 @@ var init_BaseUserDB = __esm({
|
|
|
7408
7531
|
log3 = (s) => {
|
|
7409
7532
|
logger.info(s);
|
|
7410
7533
|
};
|
|
7534
|
+
HYDRATION_TIMEOUT_MS = 15e3;
|
|
7411
7535
|
BaseUser = class _BaseUser {
|
|
7412
7536
|
static _instance;
|
|
7413
7537
|
static _initialized = false;
|
|
@@ -7436,6 +7560,29 @@ var init_BaseUserDB = __esm({
|
|
|
7436
7560
|
writeDB;
|
|
7437
7561
|
// Database to use for write operations (local-first approach)
|
|
7438
7562
|
updateQueue;
|
|
7563
|
+
_hydration = { state: "not-required" };
|
|
7564
|
+
/**
|
|
7565
|
+
* How far the local mirror can be trusted. See {@link UserHydrationStatus}.
|
|
7566
|
+
*
|
|
7567
|
+
* Consumers that would otherwise read a 404 as "new user" — onboarding
|
|
7568
|
+
* gates, first-run experiences, progress dashboards — should check for
|
|
7569
|
+
* `failed` and present a retry affordance rather than an empty state.
|
|
7570
|
+
*/
|
|
7571
|
+
hydrationStatus() {
|
|
7572
|
+
return { ...this._hydration };
|
|
7573
|
+
}
|
|
7574
|
+
/**
|
|
7575
|
+
* Whether a missing document may be treated as genuinely absent, allowing
|
|
7576
|
+
* callers to create it with defaults.
|
|
7577
|
+
*
|
|
7578
|
+
* False only when hydration failed or is still running: a 404 then may just
|
|
7579
|
+
* mean "not pulled down yet", and materializing a default would both lie to
|
|
7580
|
+
* the user and, once live sync starts, push a rev-1 document that loses to
|
|
7581
|
+
* the real one — silently discarding it.
|
|
7582
|
+
*/
|
|
7583
|
+
canMaterializeDefaults() {
|
|
7584
|
+
return this._hydration.state !== "failed" && this._hydration.state !== "hydrating";
|
|
7585
|
+
}
|
|
7439
7586
|
async createAccount(username, password) {
|
|
7440
7587
|
if (!this.syncStrategy.canCreateAccount()) {
|
|
7441
7588
|
throw new Error("Account creation not supported by current sync strategy");
|
|
@@ -7553,6 +7700,11 @@ Currently logged-in as ${this._username}.`
|
|
|
7553
7700
|
} catch (e) {
|
|
7554
7701
|
const err = e;
|
|
7555
7702
|
if (err.status === 404) {
|
|
7703
|
+
if (!this.canMaterializeDefaults()) {
|
|
7704
|
+
throw new Error(
|
|
7705
|
+
`Cannot read course registrations for ${this._username}: the local mirror is unavailable (hydration state '${this._hydration.state}'). Refusing to create an empty registration doc \u2014 that would report an existing user as unregistered, and then lose to their real document once sync resumes.`
|
|
7706
|
+
);
|
|
7707
|
+
}
|
|
7556
7708
|
await this.localDB.put({
|
|
7557
7709
|
_id: _BaseUser.DOC_IDS.COURSE_REGISTRATIONS,
|
|
7558
7710
|
courses: [],
|
|
@@ -7795,6 +7947,11 @@ Currently logged-in as ${this._username}.`
|
|
|
7795
7947
|
} catch (e) {
|
|
7796
7948
|
const err = e;
|
|
7797
7949
|
if (err.name && err.name === "not_found") {
|
|
7950
|
+
if (!this.canMaterializeDefaults()) {
|
|
7951
|
+
throw new Error(
|
|
7952
|
+
`Cannot read config for ${this._username}: the local mirror is unavailable (hydration state '${this._hydration.state}'). Refusing to write a default config over what may be an existing one.`
|
|
7953
|
+
);
|
|
7954
|
+
}
|
|
7798
7955
|
await this.localDB.put(defaultConfig);
|
|
7799
7956
|
return this.getConfig();
|
|
7800
7957
|
} else {
|
|
@@ -7868,7 +8025,9 @@ Currently logged-in as ${this._username}.`
|
|
|
7868
8025
|
_BaseUser._initialized = true;
|
|
7869
8026
|
return;
|
|
7870
8027
|
}
|
|
8028
|
+
this.syncStrategy.stopSync?.();
|
|
7871
8029
|
this.setDBandQ();
|
|
8030
|
+
await this.hydrateLocalMirror();
|
|
7872
8031
|
this.syncStrategy.startSync(this.localDB, this.remoteDB);
|
|
7873
8032
|
this.applyDesignDocs().catch((error) => {
|
|
7874
8033
|
log3(`Error in applyDesignDocs background task: ${error}`);
|
|
@@ -7884,6 +8043,85 @@ Currently logged-in as ${this._username}.`
|
|
|
7884
8043
|
});
|
|
7885
8044
|
_BaseUser._initialized = true;
|
|
7886
8045
|
}
|
|
8046
|
+
/**
|
|
8047
|
+
* Pull this account's documents into the local mirror, if that hasn't
|
|
8048
|
+
* happened on this device before.
|
|
8049
|
+
*
|
|
8050
|
+
* Runs between setDBandQ() and startSync() — after the handles exist, before
|
|
8051
|
+
* anything can observe an empty local DB or replicate one upward.
|
|
8052
|
+
*
|
|
8053
|
+
* Never throws: a failure is recorded as `failed` state and reported through
|
|
8054
|
+
* hydrationStatus(), because throwing here would abort init() and leave
|
|
8055
|
+
* BaseUser._initialized false forever (BaseUser.instance() polls it with no
|
|
8056
|
+
* ceiling). Callers decide what a failure means for them.
|
|
8057
|
+
*/
|
|
8058
|
+
async hydrateLocalMirror() {
|
|
8059
|
+
if (this.localDB.name === this.remoteDB.name || !this.syncStrategy.hydrate) {
|
|
8060
|
+
this._hydration = { state: "not-required" };
|
|
8061
|
+
return;
|
|
8062
|
+
}
|
|
8063
|
+
if (await this.hasHydrationMarker()) {
|
|
8064
|
+
this._hydration = { state: "stale" };
|
|
8065
|
+
return;
|
|
8066
|
+
}
|
|
8067
|
+
this._hydration = { state: "hydrating" };
|
|
8068
|
+
const start = Date.now();
|
|
8069
|
+
try {
|
|
8070
|
+
const { docsWritten } = await withTimeout(
|
|
8071
|
+
this.syncStrategy.hydrate(this.localDB, this.remoteDB),
|
|
8072
|
+
HYDRATION_TIMEOUT_MS,
|
|
8073
|
+
`Hydration of local mirror for ${this._username}`
|
|
8074
|
+
);
|
|
8075
|
+
await this.writeHydrationMarker();
|
|
8076
|
+
this._hydration = {
|
|
8077
|
+
state: "hydrated",
|
|
8078
|
+
docsWritten,
|
|
8079
|
+
durationMs: Date.now() - start
|
|
8080
|
+
};
|
|
8081
|
+
log3(
|
|
8082
|
+
`Hydrated local mirror for ${this._username}: ${docsWritten} docs in ${this._hydration.durationMs}ms`
|
|
8083
|
+
);
|
|
8084
|
+
} catch (e) {
|
|
8085
|
+
this.syncStrategy.stopSync?.();
|
|
8086
|
+
this._hydration = {
|
|
8087
|
+
state: "failed",
|
|
8088
|
+
durationMs: Date.now() - start,
|
|
8089
|
+
error: e instanceof Error ? e.message : String(e)
|
|
8090
|
+
};
|
|
8091
|
+
logger.error(`Failed to hydrate local mirror for ${this._username}:`, e);
|
|
8092
|
+
}
|
|
8093
|
+
}
|
|
8094
|
+
/**
|
|
8095
|
+
* Has a full pull completed on this device for the CURRENT account?
|
|
8096
|
+
*
|
|
8097
|
+
* The marker is a `_local/` document, which never replicates — so its
|
|
8098
|
+
* presence describes this device's mirror and cannot arrive from elsewhere.
|
|
8099
|
+
*/
|
|
8100
|
+
async hasHydrationMarker() {
|
|
8101
|
+
try {
|
|
8102
|
+
const marker = await this.localDB.get(HYDRATION_MARKER_ID);
|
|
8103
|
+
return marker.username === this._username;
|
|
8104
|
+
} catch {
|
|
8105
|
+
return false;
|
|
8106
|
+
}
|
|
8107
|
+
}
|
|
8108
|
+
async writeHydrationMarker() {
|
|
8109
|
+
try {
|
|
8110
|
+
let existingRev;
|
|
8111
|
+
try {
|
|
8112
|
+
existingRev = (await this.localDB.get(HYDRATION_MARKER_ID))._rev;
|
|
8113
|
+
} catch {
|
|
8114
|
+
}
|
|
8115
|
+
await this.localDB.put({
|
|
8116
|
+
_id: HYDRATION_MARKER_ID,
|
|
8117
|
+
_rev: existingRev,
|
|
8118
|
+
username: this._username,
|
|
8119
|
+
hydratedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
8120
|
+
});
|
|
8121
|
+
} catch (e) {
|
|
8122
|
+
logger.warn(`Could not write hydration marker for ${this._username}: ${e}`);
|
|
8123
|
+
}
|
|
8124
|
+
}
|
|
7887
8125
|
static designDocs = [
|
|
7888
8126
|
{
|
|
7889
8127
|
_id: "_design/reviewCards",
|
|
@@ -8220,6 +8458,11 @@ Currently logged-in as ${this._username}.`
|
|
|
8220
8458
|
} catch (e) {
|
|
8221
8459
|
const err = e;
|
|
8222
8460
|
if (err.status === 404) {
|
|
8461
|
+
if (!this.canMaterializeDefaults()) {
|
|
8462
|
+
throw new Error(
|
|
8463
|
+
`Cannot read strategy state '${strategyKey}' for ${this._username}: the local mirror is unavailable (hydration state '${this._hydration.state}').`
|
|
8464
|
+
);
|
|
8465
|
+
}
|
|
8223
8466
|
return null;
|
|
8224
8467
|
}
|
|
8225
8468
|
throw e;
|
|
@@ -8744,6 +8987,8 @@ var init_CouchDBSyncStrategy = __esm({
|
|
|
8744
8987
|
CouchDBSyncStrategy = class {
|
|
8745
8988
|
syncHandle;
|
|
8746
8989
|
// Handle to cancel sync if needed
|
|
8990
|
+
/** In-flight one-shot hydration pull, so a timed-out pull can be abandoned. */
|
|
8991
|
+
hydrationHandle;
|
|
8747
8992
|
setupRemoteDB(username) {
|
|
8748
8993
|
if (username === GuestUsername || username.startsWith(GuestUsername)) {
|
|
8749
8994
|
return getLocalUserDB(username);
|
|
@@ -8758,8 +9003,26 @@ var init_CouchDBSyncStrategy = __esm({
|
|
|
8758
9003
|
return this.getUserDB(username);
|
|
8759
9004
|
}
|
|
8760
9005
|
}
|
|
9006
|
+
/**
|
|
9007
|
+
* One-shot remote → local pull. Resolves once the local mirror is caught up.
|
|
9008
|
+
*
|
|
9009
|
+
* Pull only: pushing here would upload whatever the local DB happens to hold
|
|
9010
|
+
* before we know what the remote already has, which is the conflict-leaf
|
|
9011
|
+
* problem hydration exists to avoid. Local changes go up when startSync()
|
|
9012
|
+
* takes over.
|
|
9013
|
+
*/
|
|
9014
|
+
async hydrate(localDB, remoteDB) {
|
|
9015
|
+
const replication = pouchdb_setup_default.replicate(remoteDB, localDB, {});
|
|
9016
|
+
this.hydrationHandle = replication;
|
|
9017
|
+
try {
|
|
9018
|
+
const info = await replication;
|
|
9019
|
+
return { docsWritten: info.docs_written };
|
|
9020
|
+
} finally {
|
|
9021
|
+
this.hydrationHandle = void 0;
|
|
9022
|
+
}
|
|
9023
|
+
}
|
|
8761
9024
|
startSync(localDB, remoteDB) {
|
|
8762
|
-
if (localDB !== remoteDB) {
|
|
9025
|
+
if (localDB.name !== remoteDB.name) {
|
|
8763
9026
|
this.syncHandle = pouchdb_setup_default.sync(localDB, remoteDB, {
|
|
8764
9027
|
live: true,
|
|
8765
9028
|
retry: true
|
|
@@ -8767,8 +9030,20 @@ var init_CouchDBSyncStrategy = __esm({
|
|
|
8767
9030
|
}
|
|
8768
9031
|
}
|
|
8769
9032
|
stopSync() {
|
|
9033
|
+
if (this.hydrationHandle) {
|
|
9034
|
+
try {
|
|
9035
|
+
this.hydrationHandle.cancel();
|
|
9036
|
+
} catch (e) {
|
|
9037
|
+
logger.warn(`Failed to cancel hydration pull (continuing anyway): ${e}`);
|
|
9038
|
+
}
|
|
9039
|
+
this.hydrationHandle = void 0;
|
|
9040
|
+
}
|
|
8770
9041
|
if (this.syncHandle) {
|
|
8771
|
-
|
|
9042
|
+
try {
|
|
9043
|
+
this.syncHandle.cancel();
|
|
9044
|
+
} catch (e) {
|
|
9045
|
+
logger.warn(`Failed to cancel user sync (continuing anyway): ${e}`);
|
|
9046
|
+
}
|
|
8772
9047
|
this.syncHandle = void 0;
|
|
8773
9048
|
}
|
|
8774
9049
|
}
|