@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
package/dist/core/index.js
CHANGED
|
@@ -1667,6 +1667,30 @@ var init_SrsDebugger = __esm({
|
|
|
1667
1667
|
}
|
|
1668
1668
|
});
|
|
1669
1669
|
|
|
1670
|
+
// src/core/navigators/StrategyPressureDebugger.ts
|
|
1671
|
+
var StrategyPressureDebugger_exports = {};
|
|
1672
|
+
__export(StrategyPressureDebugger_exports, {
|
|
1673
|
+
captureStrategyPressure: () => captureStrategyPressure,
|
|
1674
|
+
clearStrategyPressureDebug: () => clearStrategyPressureDebug,
|
|
1675
|
+
getStrategyPressureDebug: () => getStrategyPressureDebug
|
|
1676
|
+
});
|
|
1677
|
+
function captureStrategyPressure(snapshot) {
|
|
1678
|
+
snapshots2.set(`${snapshot.source}:${snapshot.courseId}`, snapshot);
|
|
1679
|
+
}
|
|
1680
|
+
function getStrategyPressureDebug() {
|
|
1681
|
+
return [...snapshots2.values()].sort((a, b) => b.timestamp - a.timestamp);
|
|
1682
|
+
}
|
|
1683
|
+
function clearStrategyPressureDebug() {
|
|
1684
|
+
snapshots2.clear();
|
|
1685
|
+
}
|
|
1686
|
+
var snapshots2;
|
|
1687
|
+
var init_StrategyPressureDebugger = __esm({
|
|
1688
|
+
"src/core/navigators/StrategyPressureDebugger.ts"() {
|
|
1689
|
+
"use strict";
|
|
1690
|
+
snapshots2 = /* @__PURE__ */ new Map();
|
|
1691
|
+
}
|
|
1692
|
+
});
|
|
1693
|
+
|
|
1670
1694
|
// src/core/navigators/generators/CompositeGenerator.ts
|
|
1671
1695
|
var CompositeGenerator_exports = {};
|
|
1672
1696
|
__export(CompositeGenerator_exports, {
|
|
@@ -2031,6 +2055,13 @@ function shuffleInPlace(arr) {
|
|
|
2031
2055
|
[arr[i], arr[j]] = [arr[j], arr[i]];
|
|
2032
2056
|
}
|
|
2033
2057
|
}
|
|
2058
|
+
function formatAge(ms) {
|
|
2059
|
+
const minutes = Math.max(0, Math.round(ms / 6e4));
|
|
2060
|
+
if (minutes < 60) return `${minutes}m`;
|
|
2061
|
+
const hours = Math.round(minutes / 60);
|
|
2062
|
+
if (hours < 24) return `${hours}h`;
|
|
2063
|
+
return `${Math.round(hours / 24)}d`;
|
|
2064
|
+
}
|
|
2034
2065
|
function pickTopByScore(cards, limit) {
|
|
2035
2066
|
return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
|
|
2036
2067
|
}
|
|
@@ -2039,6 +2070,7 @@ var init_prescribed = __esm({
|
|
|
2039
2070
|
"src/core/navigators/generators/prescribed.ts"() {
|
|
2040
2071
|
"use strict";
|
|
2041
2072
|
init_navigators();
|
|
2073
|
+
init_StrategyPressureDebugger();
|
|
2042
2074
|
init_logger();
|
|
2043
2075
|
DEFAULT_FRESHNESS_WINDOW = 3;
|
|
2044
2076
|
DEFAULT_MAX_DIRECT_PER_RUN = 3;
|
|
@@ -2115,6 +2147,7 @@ var init_prescribed = __esm({
|
|
|
2115
2147
|
const groupRuntimes = [];
|
|
2116
2148
|
const priorPracticeDebt = progress.practiceDebt ?? {};
|
|
2117
2149
|
const nextPracticeDebt = {};
|
|
2150
|
+
const practiceDebtsByGroup = /* @__PURE__ */ new Map();
|
|
2118
2151
|
for (const group of this.config.groups) {
|
|
2119
2152
|
const runtime = this.buildGroupRuntimeState({
|
|
2120
2153
|
group,
|
|
@@ -2163,7 +2196,7 @@ var init_prescribed = __esm({
|
|
|
2163
2196
|
courseId,
|
|
2164
2197
|
emittedIds
|
|
2165
2198
|
);
|
|
2166
|
-
const
|
|
2199
|
+
const practice = this.buildPracticeCards({
|
|
2167
2200
|
group,
|
|
2168
2201
|
courseId,
|
|
2169
2202
|
emittedIds,
|
|
@@ -2176,7 +2209,8 @@ var init_prescribed = __esm({
|
|
|
2176
2209
|
priorPracticeDebt,
|
|
2177
2210
|
nextPracticeDebt
|
|
2178
2211
|
});
|
|
2179
|
-
|
|
2212
|
+
practiceDebtsByGroup.set(group.id, practice.debts);
|
|
2213
|
+
emitted.push(...directCards, ...supportCards, ...discoveredSupportCards, ...practice.cards);
|
|
2180
2214
|
}
|
|
2181
2215
|
nextState.practiceDebt = nextPracticeDebt;
|
|
2182
2216
|
const hintSummary = this.buildSupportHintSummary(groupRuntimes);
|
|
@@ -2192,6 +2226,7 @@ var init_prescribed = __esm({
|
|
|
2192
2226
|
} else {
|
|
2193
2227
|
logger.info("[Prescribed] No hints to emit (no blocked targets or no support tags)");
|
|
2194
2228
|
}
|
|
2229
|
+
this.capturePressureSnapshot(courseId, groupRuntimes, practiceDebtsByGroup, emitted, hints);
|
|
2195
2230
|
if (emitted.length === 0) {
|
|
2196
2231
|
logger.info(
|
|
2197
2232
|
"[Prescribed] 0 cards emitted (all targets blocked, authored/discovered support candidates exhausted)" + (hints ? " \u2014 boost hints emitted but may not survive filters" : "")
|
|
@@ -2253,6 +2288,60 @@ var init_prescribed = __esm({
|
|
|
2253
2288
|
supportTags: [...supportTags].sort()
|
|
2254
2289
|
};
|
|
2255
2290
|
}
|
|
2291
|
+
/**
|
|
2292
|
+
* Translate this run's per-group runtimes and practice debts into gauges on
|
|
2293
|
+
* the generic strategy-pressure debug channel (rendered by the live session
|
|
2294
|
+
* overlay's "strategy backpressure" section).
|
|
2295
|
+
*/
|
|
2296
|
+
capturePressureSnapshot(courseId, groupRuntimes, practiceDebtsByGroup, emitted, hints) {
|
|
2297
|
+
const now = Date.now();
|
|
2298
|
+
const gauges = [];
|
|
2299
|
+
for (const runtime of groupRuntimes) {
|
|
2300
|
+
const group = runtime.group;
|
|
2301
|
+
const window2 = group.freshnessWindowSessions ?? DEFAULT_FRESHNESS_WINDOW;
|
|
2302
|
+
gauges.push({
|
|
2303
|
+
id: `group:${group.id}:target`,
|
|
2304
|
+
label: `${group.id} targets`,
|
|
2305
|
+
multiplier: runtime.pressureMultiplier,
|
|
2306
|
+
max: MAX_TARGET_MULTIPLIER,
|
|
2307
|
+
detail: `${runtime.pendingTargets.length} pending (${runtime.surfaceableTargets.length} surfaceable, ${runtime.blockedTargets.length} blocked) \xB7 sinceSurfaced ${runtime.sessionsSinceSurfaced}/${window2}`,
|
|
2308
|
+
items: runtime.blockedTargets.map((cardId) => ({ label: cardId, value: "blocked" }))
|
|
2309
|
+
});
|
|
2310
|
+
if (runtime.blockedTargets.length > 0) {
|
|
2311
|
+
const mode = runtime.supportCandidates.length > 0 ? "direct-support" : runtime.discoveredSupportCandidates.length > 0 ? "inserted-support" : "boost-only";
|
|
2312
|
+
gauges.push({
|
|
2313
|
+
id: `group:${group.id}:support`,
|
|
2314
|
+
label: `${group.id} support`,
|
|
2315
|
+
multiplier: runtime.supportMultiplier,
|
|
2316
|
+
max: MAX_SUPPORT_MULTIPLIER,
|
|
2317
|
+
detail: `mode=${mode} \xB7 ${runtime.supportTags.length} support tag(s)`,
|
|
2318
|
+
items: runtime.supportTags.map((tag) => ({ label: tag }))
|
|
2319
|
+
});
|
|
2320
|
+
}
|
|
2321
|
+
const debts = practiceDebtsByGroup.get(group.id) ?? [];
|
|
2322
|
+
if (debts.length > 0) {
|
|
2323
|
+
gauges.push({
|
|
2324
|
+
id: `group:${group.id}:practice-debt`,
|
|
2325
|
+
label: `${group.id} practice debt`,
|
|
2326
|
+
multiplier: Math.max(...debts.map((d) => d.multiplier)),
|
|
2327
|
+
max: MAX_PRACTICE_MULTIPLIER,
|
|
2328
|
+
detail: `${debts.length} under-practiced skill(s)`,
|
|
2329
|
+
items: debts.map((d) => ({
|
|
2330
|
+
label: d.tag,
|
|
2331
|
+
value: `\xD7${d.multiplier.toFixed(2)} \xB7 open ${formatAge(now - new Date(d.firstOwedAt).getTime())}`
|
|
2332
|
+
}))
|
|
2333
|
+
});
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
captureStrategyPressure({
|
|
2337
|
+
source: "prescribed",
|
|
2338
|
+
courseId,
|
|
2339
|
+
gauges,
|
|
2340
|
+
topScore: emitted.length > 0 ? Math.max(...emitted.map((c) => c.score)) : null,
|
|
2341
|
+
hintsLabel: hints?._label,
|
|
2342
|
+
timestamp: now
|
|
2343
|
+
});
|
|
2344
|
+
}
|
|
2256
2345
|
parseConfig(serializedData) {
|
|
2257
2346
|
try {
|
|
2258
2347
|
const parsed = JSON.parse(serializedData);
|
|
@@ -2409,6 +2498,7 @@ var init_prescribed = __esm({
|
|
|
2409
2498
|
supportTags: [...supportTags],
|
|
2410
2499
|
pressureMultiplier,
|
|
2411
2500
|
supportMultiplier,
|
|
2501
|
+
sessionsSinceSurfaced,
|
|
2412
2502
|
debugVersion: PRESCRIBED_DEBUG_VERSION
|
|
2413
2503
|
};
|
|
2414
2504
|
}
|
|
@@ -2541,13 +2631,13 @@ var init_prescribed = __esm({
|
|
|
2541
2631
|
nextPracticeDebt
|
|
2542
2632
|
} = args;
|
|
2543
2633
|
const patterns = group.practiceTagPatterns ?? [];
|
|
2544
|
-
if (patterns.length === 0) return [];
|
|
2634
|
+
if (patterns.length === 0) return { cards: [], debts: [] };
|
|
2545
2635
|
const practiceMinCount = group.practiceMinCount ?? DEFAULT_PRACTICE_MIN_COUNT;
|
|
2546
2636
|
const maxPractice = group.maxPracticeCardsPerRun ?? DEFAULT_MAX_PRACTICE_PER_RUN;
|
|
2547
2637
|
const practiceTags = [...cardsByTag.keys()].filter(
|
|
2548
2638
|
(tag) => patterns.some((p) => matchesTagPattern(tag, p)) && this.isUnlockedGatedSkill(tag, hierarchyConfigs, userTagElo, userGlobalElo) && (userTagElo[tag]?.count ?? 0) < practiceMinCount
|
|
2549
2639
|
);
|
|
2550
|
-
if (practiceTags.length === 0) return [];
|
|
2640
|
+
if (practiceTags.length === 0) return { cards: [], debts: [] };
|
|
2551
2641
|
const now = Date.now();
|
|
2552
2642
|
const DAY_MS = 24 * 60 * 60 * 1e3;
|
|
2553
2643
|
const tagMultiplier = /* @__PURE__ */ new Map();
|
|
@@ -2562,6 +2652,11 @@ var init_prescribed = __esm({
|
|
|
2562
2652
|
);
|
|
2563
2653
|
tagMultiplier.set(tag, mult);
|
|
2564
2654
|
}
|
|
2655
|
+
const debts = practiceTags.map((tag) => ({
|
|
2656
|
+
tag,
|
|
2657
|
+
multiplier: tagMultiplier.get(tag) ?? PRACTICE_BASE_MULT,
|
|
2658
|
+
firstOwedAt: nextPracticeDebt[tag]
|
|
2659
|
+
})).sort((a, b) => b.multiplier - a.multiplier || a.tag.localeCompare(b.tag));
|
|
2565
2660
|
const practiceCardIds = this.findDiscoveredSupportCards({
|
|
2566
2661
|
supportTags: practiceTags,
|
|
2567
2662
|
cardsByTag,
|
|
@@ -2570,7 +2665,7 @@ var init_prescribed = __esm({
|
|
|
2570
2665
|
excludedIds: emittedIds,
|
|
2571
2666
|
limit: maxPractice
|
|
2572
2667
|
});
|
|
2573
|
-
if (practiceCardIds.length === 0) return [];
|
|
2668
|
+
if (practiceCardIds.length === 0) return { cards: [], debts };
|
|
2574
2669
|
logger.info(
|
|
2575
2670
|
`[Prescribed] Group '${group.id}' practice: ${practiceTags.length} unlocked under-practiced skill(s), emitting ${practiceCardIds.length} drill card(s)`
|
|
2576
2671
|
);
|
|
@@ -2600,7 +2695,7 @@ var init_prescribed = __esm({
|
|
|
2600
2695
|
]
|
|
2601
2696
|
});
|
|
2602
2697
|
}
|
|
2603
|
-
return cards;
|
|
2698
|
+
return { cards, debts };
|
|
2604
2699
|
}
|
|
2605
2700
|
/**
|
|
2606
2701
|
* True for a skill that was *gated and is now reached*: it has at least one
|
|
@@ -5277,6 +5372,7 @@ var init_3 = __esm({
|
|
|
5277
5372
|
"./PipelineAssembler.ts": () => Promise.resolve().then(() => (init_PipelineAssembler(), PipelineAssembler_exports)),
|
|
5278
5373
|
"./PipelineDebugger.ts": () => Promise.resolve().then(() => (init_PipelineDebugger(), PipelineDebugger_exports)),
|
|
5279
5374
|
"./SrsDebugger.ts": () => Promise.resolve().then(() => (init_SrsDebugger(), SrsDebugger_exports)),
|
|
5375
|
+
"./StrategyPressureDebugger.ts": () => Promise.resolve().then(() => (init_StrategyPressureDebugger(), StrategyPressureDebugger_exports)),
|
|
5280
5376
|
"./defaults.ts": () => Promise.resolve().then(() => (init_defaults(), defaults_exports)),
|
|
5281
5377
|
"./diversityRerank.ts": () => Promise.resolve().then(() => (init_diversityRerank(), diversityRerank_exports)),
|
|
5282
5378
|
"./filters/WeightedFilter.ts": () => Promise.resolve().then(() => (init_WeightedFilter(), WeightedFilter_exports)),
|
|
@@ -5309,7 +5405,9 @@ __export(navigators_exports, {
|
|
|
5309
5405
|
NavigatorRole: () => NavigatorRole,
|
|
5310
5406
|
NavigatorRoles: () => NavigatorRoles,
|
|
5311
5407
|
Navigators: () => Navigators,
|
|
5408
|
+
captureStrategyPressure: () => captureStrategyPressure,
|
|
5312
5409
|
clearSrsBacklogDebug: () => clearSrsBacklogDebug,
|
|
5410
|
+
clearStrategyPressureDebug: () => clearStrategyPressureDebug,
|
|
5313
5411
|
diversityRerank: () => diversityRerank,
|
|
5314
5412
|
getActivePipeline: () => getActivePipeline,
|
|
5315
5413
|
getCardOrigin: () => getCardOrigin,
|
|
@@ -5317,6 +5415,7 @@ __export(navigators_exports, {
|
|
|
5317
5415
|
getRegisteredNavigatorNames: () => getRegisteredNavigatorNames,
|
|
5318
5416
|
getRegisteredNavigatorRole: () => getRegisteredNavigatorRole,
|
|
5319
5417
|
getSrsBacklogDebug: () => getSrsBacklogDebug,
|
|
5418
|
+
getStrategyPressureDebug: () => getStrategyPressureDebug,
|
|
5320
5419
|
hasRegisteredNavigator: () => hasRegisteredNavigator,
|
|
5321
5420
|
initializeNavigatorRegistry: () => initializeNavigatorRegistry,
|
|
5322
5421
|
isFilter: () => isFilter,
|
|
@@ -5399,6 +5498,7 @@ var init_navigators = __esm({
|
|
|
5399
5498
|
init_diversityRerank();
|
|
5400
5499
|
init_PipelineDebugger();
|
|
5401
5500
|
init_SrsDebugger();
|
|
5501
|
+
init_StrategyPressureDebugger();
|
|
5402
5502
|
init_logger();
|
|
5403
5503
|
init_();
|
|
5404
5504
|
init_2();
|
|
@@ -6667,6 +6767,19 @@ var init_couch = __esm({
|
|
|
6667
6767
|
});
|
|
6668
6768
|
|
|
6669
6769
|
// src/impl/common/BaseUserDB.ts
|
|
6770
|
+
function withTimeout(p, ms, label) {
|
|
6771
|
+
let timer;
|
|
6772
|
+
return Promise.race([
|
|
6773
|
+
p,
|
|
6774
|
+
new Promise((_resolve, reject) => {
|
|
6775
|
+
timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
|
|
6776
|
+
})
|
|
6777
|
+
]).finally(() => {
|
|
6778
|
+
if (timer !== void 0) {
|
|
6779
|
+
clearTimeout(timer);
|
|
6780
|
+
}
|
|
6781
|
+
});
|
|
6782
|
+
}
|
|
6670
6783
|
async function getOrCreateClassroomRegistrationsDoc(user) {
|
|
6671
6784
|
let ret;
|
|
6672
6785
|
try {
|
|
@@ -6759,7 +6872,7 @@ async function dropUserFromClassroom(user, classID) {
|
|
|
6759
6872
|
async function getUserClassrooms(user) {
|
|
6760
6873
|
return getOrCreateClassroomRegistrationsDoc(user);
|
|
6761
6874
|
}
|
|
6762
|
-
var import_common12, import_moment6, log3, BaseUser, userCoursesDoc, userClassroomsDoc;
|
|
6875
|
+
var import_common12, import_moment6, log3, HYDRATION_TIMEOUT_MS, BaseUser, userCoursesDoc, userClassroomsDoc;
|
|
6763
6876
|
var init_BaseUserDB = __esm({
|
|
6764
6877
|
"src/impl/common/BaseUserDB.ts"() {
|
|
6765
6878
|
"use strict";
|
|
@@ -6776,6 +6889,7 @@ var init_BaseUserDB = __esm({
|
|
|
6776
6889
|
log3 = (s) => {
|
|
6777
6890
|
logger.info(s);
|
|
6778
6891
|
};
|
|
6892
|
+
HYDRATION_TIMEOUT_MS = 15e3;
|
|
6779
6893
|
BaseUser = class _BaseUser {
|
|
6780
6894
|
static _instance;
|
|
6781
6895
|
static _initialized = false;
|
|
@@ -6804,6 +6918,29 @@ var init_BaseUserDB = __esm({
|
|
|
6804
6918
|
writeDB;
|
|
6805
6919
|
// Database to use for write operations (local-first approach)
|
|
6806
6920
|
updateQueue;
|
|
6921
|
+
_hydration = { state: "not-required" };
|
|
6922
|
+
/**
|
|
6923
|
+
* How far the local mirror can be trusted. See {@link UserHydrationStatus}.
|
|
6924
|
+
*
|
|
6925
|
+
* Consumers that would otherwise read a 404 as "new user" — onboarding
|
|
6926
|
+
* gates, first-run experiences, progress dashboards — should check for
|
|
6927
|
+
* `failed` and present a retry affordance rather than an empty state.
|
|
6928
|
+
*/
|
|
6929
|
+
hydrationStatus() {
|
|
6930
|
+
return { ...this._hydration };
|
|
6931
|
+
}
|
|
6932
|
+
/**
|
|
6933
|
+
* Whether a missing document may be treated as genuinely absent, allowing
|
|
6934
|
+
* callers to create it with defaults.
|
|
6935
|
+
*
|
|
6936
|
+
* False only when hydration failed or is still running: a 404 then may just
|
|
6937
|
+
* mean "not pulled down yet", and materializing a default would both lie to
|
|
6938
|
+
* the user and, once live sync starts, push a rev-1 document that loses to
|
|
6939
|
+
* the real one — silently discarding it.
|
|
6940
|
+
*/
|
|
6941
|
+
canMaterializeDefaults() {
|
|
6942
|
+
return this._hydration.state !== "failed" && this._hydration.state !== "hydrating";
|
|
6943
|
+
}
|
|
6807
6944
|
async createAccount(username, password) {
|
|
6808
6945
|
if (!this.syncStrategy.canCreateAccount()) {
|
|
6809
6946
|
throw new Error("Account creation not supported by current sync strategy");
|
|
@@ -6921,6 +7058,11 @@ Currently logged-in as ${this._username}.`
|
|
|
6921
7058
|
} catch (e) {
|
|
6922
7059
|
const err = e;
|
|
6923
7060
|
if (err.status === 404) {
|
|
7061
|
+
if (!this.canMaterializeDefaults()) {
|
|
7062
|
+
throw new Error(
|
|
7063
|
+
`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.`
|
|
7064
|
+
);
|
|
7065
|
+
}
|
|
6924
7066
|
await this.localDB.put({
|
|
6925
7067
|
_id: _BaseUser.DOC_IDS.COURSE_REGISTRATIONS,
|
|
6926
7068
|
courses: [],
|
|
@@ -7163,6 +7305,11 @@ Currently logged-in as ${this._username}.`
|
|
|
7163
7305
|
} catch (e) {
|
|
7164
7306
|
const err = e;
|
|
7165
7307
|
if (err.name && err.name === "not_found") {
|
|
7308
|
+
if (!this.canMaterializeDefaults()) {
|
|
7309
|
+
throw new Error(
|
|
7310
|
+
`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.`
|
|
7311
|
+
);
|
|
7312
|
+
}
|
|
7166
7313
|
await this.localDB.put(defaultConfig);
|
|
7167
7314
|
return this.getConfig();
|
|
7168
7315
|
} else {
|
|
@@ -7236,7 +7383,9 @@ Currently logged-in as ${this._username}.`
|
|
|
7236
7383
|
_BaseUser._initialized = true;
|
|
7237
7384
|
return;
|
|
7238
7385
|
}
|
|
7386
|
+
this.syncStrategy.stopSync?.();
|
|
7239
7387
|
this.setDBandQ();
|
|
7388
|
+
await this.hydrateLocalMirror();
|
|
7240
7389
|
this.syncStrategy.startSync(this.localDB, this.remoteDB);
|
|
7241
7390
|
this.applyDesignDocs().catch((error) => {
|
|
7242
7391
|
log3(`Error in applyDesignDocs background task: ${error}`);
|
|
@@ -7252,6 +7401,85 @@ Currently logged-in as ${this._username}.`
|
|
|
7252
7401
|
});
|
|
7253
7402
|
_BaseUser._initialized = true;
|
|
7254
7403
|
}
|
|
7404
|
+
/**
|
|
7405
|
+
* Pull this account's documents into the local mirror, if that hasn't
|
|
7406
|
+
* happened on this device before.
|
|
7407
|
+
*
|
|
7408
|
+
* Runs between setDBandQ() and startSync() — after the handles exist, before
|
|
7409
|
+
* anything can observe an empty local DB or replicate one upward.
|
|
7410
|
+
*
|
|
7411
|
+
* Never throws: a failure is recorded as `failed` state and reported through
|
|
7412
|
+
* hydrationStatus(), because throwing here would abort init() and leave
|
|
7413
|
+
* BaseUser._initialized false forever (BaseUser.instance() polls it with no
|
|
7414
|
+
* ceiling). Callers decide what a failure means for them.
|
|
7415
|
+
*/
|
|
7416
|
+
async hydrateLocalMirror() {
|
|
7417
|
+
if (this.localDB.name === this.remoteDB.name || !this.syncStrategy.hydrate) {
|
|
7418
|
+
this._hydration = { state: "not-required" };
|
|
7419
|
+
return;
|
|
7420
|
+
}
|
|
7421
|
+
if (await this.hasHydrationMarker()) {
|
|
7422
|
+
this._hydration = { state: "stale" };
|
|
7423
|
+
return;
|
|
7424
|
+
}
|
|
7425
|
+
this._hydration = { state: "hydrating" };
|
|
7426
|
+
const start = Date.now();
|
|
7427
|
+
try {
|
|
7428
|
+
const { docsWritten } = await withTimeout(
|
|
7429
|
+
this.syncStrategy.hydrate(this.localDB, this.remoteDB),
|
|
7430
|
+
HYDRATION_TIMEOUT_MS,
|
|
7431
|
+
`Hydration of local mirror for ${this._username}`
|
|
7432
|
+
);
|
|
7433
|
+
await this.writeHydrationMarker();
|
|
7434
|
+
this._hydration = {
|
|
7435
|
+
state: "hydrated",
|
|
7436
|
+
docsWritten,
|
|
7437
|
+
durationMs: Date.now() - start
|
|
7438
|
+
};
|
|
7439
|
+
log3(
|
|
7440
|
+
`Hydrated local mirror for ${this._username}: ${docsWritten} docs in ${this._hydration.durationMs}ms`
|
|
7441
|
+
);
|
|
7442
|
+
} catch (e) {
|
|
7443
|
+
this.syncStrategy.stopSync?.();
|
|
7444
|
+
this._hydration = {
|
|
7445
|
+
state: "failed",
|
|
7446
|
+
durationMs: Date.now() - start,
|
|
7447
|
+
error: e instanceof Error ? e.message : String(e)
|
|
7448
|
+
};
|
|
7449
|
+
logger.error(`Failed to hydrate local mirror for ${this._username}:`, e);
|
|
7450
|
+
}
|
|
7451
|
+
}
|
|
7452
|
+
/**
|
|
7453
|
+
* Has a full pull completed on this device for the CURRENT account?
|
|
7454
|
+
*
|
|
7455
|
+
* The marker is a `_local/` document, which never replicates — so its
|
|
7456
|
+
* presence describes this device's mirror and cannot arrive from elsewhere.
|
|
7457
|
+
*/
|
|
7458
|
+
async hasHydrationMarker() {
|
|
7459
|
+
try {
|
|
7460
|
+
const marker = await this.localDB.get(HYDRATION_MARKER_ID);
|
|
7461
|
+
return marker.username === this._username;
|
|
7462
|
+
} catch {
|
|
7463
|
+
return false;
|
|
7464
|
+
}
|
|
7465
|
+
}
|
|
7466
|
+
async writeHydrationMarker() {
|
|
7467
|
+
try {
|
|
7468
|
+
let existingRev;
|
|
7469
|
+
try {
|
|
7470
|
+
existingRev = (await this.localDB.get(HYDRATION_MARKER_ID))._rev;
|
|
7471
|
+
} catch {
|
|
7472
|
+
}
|
|
7473
|
+
await this.localDB.put({
|
|
7474
|
+
_id: HYDRATION_MARKER_ID,
|
|
7475
|
+
_rev: existingRev,
|
|
7476
|
+
username: this._username,
|
|
7477
|
+
hydratedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
7478
|
+
});
|
|
7479
|
+
} catch (e) {
|
|
7480
|
+
logger.warn(`Could not write hydration marker for ${this._username}: ${e}`);
|
|
7481
|
+
}
|
|
7482
|
+
}
|
|
7255
7483
|
static designDocs = [
|
|
7256
7484
|
{
|
|
7257
7485
|
_id: "_design/reviewCards",
|
|
@@ -7588,6 +7816,11 @@ Currently logged-in as ${this._username}.`
|
|
|
7588
7816
|
} catch (e) {
|
|
7589
7817
|
const err = e;
|
|
7590
7818
|
if (err.status === 404) {
|
|
7819
|
+
if (!this.canMaterializeDefaults()) {
|
|
7820
|
+
throw new Error(
|
|
7821
|
+
`Cannot read strategy state '${strategyKey}' for ${this._username}: the local mirror is unavailable (hydration state '${this._hydration.state}').`
|
|
7822
|
+
);
|
|
7823
|
+
}
|
|
7591
7824
|
return null;
|
|
7592
7825
|
}
|
|
7593
7826
|
throw e;
|
|
@@ -7936,6 +8169,15 @@ var init_userOutcome = __esm({
|
|
|
7936
8169
|
}
|
|
7937
8170
|
});
|
|
7938
8171
|
|
|
8172
|
+
// src/core/types/hydration.ts
|
|
8173
|
+
var HYDRATION_MARKER_ID;
|
|
8174
|
+
var init_hydration = __esm({
|
|
8175
|
+
"src/core/types/hydration.ts"() {
|
|
8176
|
+
"use strict";
|
|
8177
|
+
HYDRATION_MARKER_ID = "_local/hydration";
|
|
8178
|
+
}
|
|
8179
|
+
});
|
|
8180
|
+
|
|
7939
8181
|
// src/core/bulkImport/cardProcessor.ts
|
|
7940
8182
|
async function importParsedCards(parsedCards, courseDB, config) {
|
|
7941
8183
|
const results = [];
|
|
@@ -8416,6 +8658,7 @@ __export(core_exports, {
|
|
|
8416
8658
|
DocType: () => DocType,
|
|
8417
8659
|
DocTypePrefixes: () => DocTypePrefixes,
|
|
8418
8660
|
GuestUsername: () => GuestUsername,
|
|
8661
|
+
HYDRATION_MARKER_ID: () => HYDRATION_MARKER_ID,
|
|
8419
8662
|
Loggable: () => Loggable,
|
|
8420
8663
|
NavigatorRole: () => NavigatorRole,
|
|
8421
8664
|
NavigatorRoles: () => NavigatorRoles,
|
|
@@ -8423,7 +8666,9 @@ __export(core_exports, {
|
|
|
8423
8666
|
aggregateOutcomesForGradient: () => aggregateOutcomesForGradient,
|
|
8424
8667
|
areQuestionRecords: () => areQuestionRecords,
|
|
8425
8668
|
buildStrategyStateId: () => buildStrategyStateId,
|
|
8669
|
+
captureStrategyPressure: () => captureStrategyPressure,
|
|
8426
8670
|
clearSrsBacklogDebug: () => clearSrsBacklogDebug,
|
|
8671
|
+
clearStrategyPressureDebug: () => clearStrategyPressureDebug,
|
|
8427
8672
|
computeDeviation: () => computeDeviation,
|
|
8428
8673
|
computeEffectiveWeight: () => computeEffectiveWeight,
|
|
8429
8674
|
computeOutcomeSignal: () => computeOutcomeSignal,
|
|
@@ -8440,6 +8685,7 @@ __export(core_exports, {
|
|
|
8440
8685
|
getRegisteredNavigatorNames: () => getRegisteredNavigatorNames,
|
|
8441
8686
|
getRegisteredNavigatorRole: () => getRegisteredNavigatorRole,
|
|
8442
8687
|
getSrsBacklogDebug: () => getSrsBacklogDebug,
|
|
8688
|
+
getStrategyPressureDebug: () => getStrategyPressureDebug,
|
|
8443
8689
|
getStudySource: () => getStudySource,
|
|
8444
8690
|
hasRegisteredNavigator: () => hasRegisteredNavigator,
|
|
8445
8691
|
importParsedCards: () => importParsedCards,
|
|
@@ -8470,6 +8716,7 @@ var init_core = __esm({
|
|
|
8470
8716
|
init_user();
|
|
8471
8717
|
init_strategyState();
|
|
8472
8718
|
init_userOutcome();
|
|
8719
|
+
init_hydration();
|
|
8473
8720
|
init_Loggable();
|
|
8474
8721
|
init_util();
|
|
8475
8722
|
init_navigators();
|
|
@@ -8487,6 +8734,7 @@ init_core();
|
|
|
8487
8734
|
DocType,
|
|
8488
8735
|
DocTypePrefixes,
|
|
8489
8736
|
GuestUsername,
|
|
8737
|
+
HYDRATION_MARKER_ID,
|
|
8490
8738
|
Loggable,
|
|
8491
8739
|
NavigatorRole,
|
|
8492
8740
|
NavigatorRoles,
|
|
@@ -8494,7 +8742,9 @@ init_core();
|
|
|
8494
8742
|
aggregateOutcomesForGradient,
|
|
8495
8743
|
areQuestionRecords,
|
|
8496
8744
|
buildStrategyStateId,
|
|
8745
|
+
captureStrategyPressure,
|
|
8497
8746
|
clearSrsBacklogDebug,
|
|
8747
|
+
clearStrategyPressureDebug,
|
|
8498
8748
|
computeDeviation,
|
|
8499
8749
|
computeEffectiveWeight,
|
|
8500
8750
|
computeOutcomeSignal,
|
|
@@ -8511,6 +8761,7 @@ init_core();
|
|
|
8511
8761
|
getRegisteredNavigatorNames,
|
|
8512
8762
|
getRegisteredNavigatorRole,
|
|
8513
8763
|
getSrsBacklogDebug,
|
|
8764
|
+
getStrategyPressureDebug,
|
|
8514
8765
|
getStudySource,
|
|
8515
8766
|
hasRegisteredNavigator,
|
|
8516
8767
|
importParsedCards,
|