@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.mjs
CHANGED
|
@@ -1644,6 +1644,30 @@ var init_SrsDebugger = __esm({
|
|
|
1644
1644
|
}
|
|
1645
1645
|
});
|
|
1646
1646
|
|
|
1647
|
+
// src/core/navigators/StrategyPressureDebugger.ts
|
|
1648
|
+
var StrategyPressureDebugger_exports = {};
|
|
1649
|
+
__export(StrategyPressureDebugger_exports, {
|
|
1650
|
+
captureStrategyPressure: () => captureStrategyPressure,
|
|
1651
|
+
clearStrategyPressureDebug: () => clearStrategyPressureDebug,
|
|
1652
|
+
getStrategyPressureDebug: () => getStrategyPressureDebug
|
|
1653
|
+
});
|
|
1654
|
+
function captureStrategyPressure(snapshot) {
|
|
1655
|
+
snapshots2.set(`${snapshot.source}:${snapshot.courseId}`, snapshot);
|
|
1656
|
+
}
|
|
1657
|
+
function getStrategyPressureDebug() {
|
|
1658
|
+
return [...snapshots2.values()].sort((a, b) => b.timestamp - a.timestamp);
|
|
1659
|
+
}
|
|
1660
|
+
function clearStrategyPressureDebug() {
|
|
1661
|
+
snapshots2.clear();
|
|
1662
|
+
}
|
|
1663
|
+
var snapshots2;
|
|
1664
|
+
var init_StrategyPressureDebugger = __esm({
|
|
1665
|
+
"src/core/navigators/StrategyPressureDebugger.ts"() {
|
|
1666
|
+
"use strict";
|
|
1667
|
+
snapshots2 = /* @__PURE__ */ new Map();
|
|
1668
|
+
}
|
|
1669
|
+
});
|
|
1670
|
+
|
|
1647
1671
|
// src/core/navigators/generators/CompositeGenerator.ts
|
|
1648
1672
|
var CompositeGenerator_exports = {};
|
|
1649
1673
|
__export(CompositeGenerator_exports, {
|
|
@@ -2008,6 +2032,13 @@ function shuffleInPlace(arr) {
|
|
|
2008
2032
|
[arr[i], arr[j]] = [arr[j], arr[i]];
|
|
2009
2033
|
}
|
|
2010
2034
|
}
|
|
2035
|
+
function formatAge(ms) {
|
|
2036
|
+
const minutes = Math.max(0, Math.round(ms / 6e4));
|
|
2037
|
+
if (minutes < 60) return `${minutes}m`;
|
|
2038
|
+
const hours = Math.round(minutes / 60);
|
|
2039
|
+
if (hours < 24) return `${hours}h`;
|
|
2040
|
+
return `${Math.round(hours / 24)}d`;
|
|
2041
|
+
}
|
|
2011
2042
|
function pickTopByScore(cards, limit) {
|
|
2012
2043
|
return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
|
|
2013
2044
|
}
|
|
@@ -2016,6 +2047,7 @@ var init_prescribed = __esm({
|
|
|
2016
2047
|
"src/core/navigators/generators/prescribed.ts"() {
|
|
2017
2048
|
"use strict";
|
|
2018
2049
|
init_navigators();
|
|
2050
|
+
init_StrategyPressureDebugger();
|
|
2019
2051
|
init_logger();
|
|
2020
2052
|
DEFAULT_FRESHNESS_WINDOW = 3;
|
|
2021
2053
|
DEFAULT_MAX_DIRECT_PER_RUN = 3;
|
|
@@ -2092,6 +2124,7 @@ var init_prescribed = __esm({
|
|
|
2092
2124
|
const groupRuntimes = [];
|
|
2093
2125
|
const priorPracticeDebt = progress.practiceDebt ?? {};
|
|
2094
2126
|
const nextPracticeDebt = {};
|
|
2127
|
+
const practiceDebtsByGroup = /* @__PURE__ */ new Map();
|
|
2095
2128
|
for (const group of this.config.groups) {
|
|
2096
2129
|
const runtime = this.buildGroupRuntimeState({
|
|
2097
2130
|
group,
|
|
@@ -2140,7 +2173,7 @@ var init_prescribed = __esm({
|
|
|
2140
2173
|
courseId,
|
|
2141
2174
|
emittedIds
|
|
2142
2175
|
);
|
|
2143
|
-
const
|
|
2176
|
+
const practice = this.buildPracticeCards({
|
|
2144
2177
|
group,
|
|
2145
2178
|
courseId,
|
|
2146
2179
|
emittedIds,
|
|
@@ -2153,7 +2186,8 @@ var init_prescribed = __esm({
|
|
|
2153
2186
|
priorPracticeDebt,
|
|
2154
2187
|
nextPracticeDebt
|
|
2155
2188
|
});
|
|
2156
|
-
|
|
2189
|
+
practiceDebtsByGroup.set(group.id, practice.debts);
|
|
2190
|
+
emitted.push(...directCards, ...supportCards, ...discoveredSupportCards, ...practice.cards);
|
|
2157
2191
|
}
|
|
2158
2192
|
nextState.practiceDebt = nextPracticeDebt;
|
|
2159
2193
|
const hintSummary = this.buildSupportHintSummary(groupRuntimes);
|
|
@@ -2169,6 +2203,7 @@ var init_prescribed = __esm({
|
|
|
2169
2203
|
} else {
|
|
2170
2204
|
logger.info("[Prescribed] No hints to emit (no blocked targets or no support tags)");
|
|
2171
2205
|
}
|
|
2206
|
+
this.capturePressureSnapshot(courseId, groupRuntimes, practiceDebtsByGroup, emitted, hints);
|
|
2172
2207
|
if (emitted.length === 0) {
|
|
2173
2208
|
logger.info(
|
|
2174
2209
|
"[Prescribed] 0 cards emitted (all targets blocked, authored/discovered support candidates exhausted)" + (hints ? " \u2014 boost hints emitted but may not survive filters" : "")
|
|
@@ -2230,6 +2265,60 @@ var init_prescribed = __esm({
|
|
|
2230
2265
|
supportTags: [...supportTags].sort()
|
|
2231
2266
|
};
|
|
2232
2267
|
}
|
|
2268
|
+
/**
|
|
2269
|
+
* Translate this run's per-group runtimes and practice debts into gauges on
|
|
2270
|
+
* the generic strategy-pressure debug channel (rendered by the live session
|
|
2271
|
+
* overlay's "strategy backpressure" section).
|
|
2272
|
+
*/
|
|
2273
|
+
capturePressureSnapshot(courseId, groupRuntimes, practiceDebtsByGroup, emitted, hints) {
|
|
2274
|
+
const now = Date.now();
|
|
2275
|
+
const gauges = [];
|
|
2276
|
+
for (const runtime of groupRuntimes) {
|
|
2277
|
+
const group = runtime.group;
|
|
2278
|
+
const window2 = group.freshnessWindowSessions ?? DEFAULT_FRESHNESS_WINDOW;
|
|
2279
|
+
gauges.push({
|
|
2280
|
+
id: `group:${group.id}:target`,
|
|
2281
|
+
label: `${group.id} targets`,
|
|
2282
|
+
multiplier: runtime.pressureMultiplier,
|
|
2283
|
+
max: MAX_TARGET_MULTIPLIER,
|
|
2284
|
+
detail: `${runtime.pendingTargets.length} pending (${runtime.surfaceableTargets.length} surfaceable, ${runtime.blockedTargets.length} blocked) \xB7 sinceSurfaced ${runtime.sessionsSinceSurfaced}/${window2}`,
|
|
2285
|
+
items: runtime.blockedTargets.map((cardId) => ({ label: cardId, value: "blocked" }))
|
|
2286
|
+
});
|
|
2287
|
+
if (runtime.blockedTargets.length > 0) {
|
|
2288
|
+
const mode = runtime.supportCandidates.length > 0 ? "direct-support" : runtime.discoveredSupportCandidates.length > 0 ? "inserted-support" : "boost-only";
|
|
2289
|
+
gauges.push({
|
|
2290
|
+
id: `group:${group.id}:support`,
|
|
2291
|
+
label: `${group.id} support`,
|
|
2292
|
+
multiplier: runtime.supportMultiplier,
|
|
2293
|
+
max: MAX_SUPPORT_MULTIPLIER,
|
|
2294
|
+
detail: `mode=${mode} \xB7 ${runtime.supportTags.length} support tag(s)`,
|
|
2295
|
+
items: runtime.supportTags.map((tag) => ({ label: tag }))
|
|
2296
|
+
});
|
|
2297
|
+
}
|
|
2298
|
+
const debts = practiceDebtsByGroup.get(group.id) ?? [];
|
|
2299
|
+
if (debts.length > 0) {
|
|
2300
|
+
gauges.push({
|
|
2301
|
+
id: `group:${group.id}:practice-debt`,
|
|
2302
|
+
label: `${group.id} practice debt`,
|
|
2303
|
+
multiplier: Math.max(...debts.map((d) => d.multiplier)),
|
|
2304
|
+
max: MAX_PRACTICE_MULTIPLIER,
|
|
2305
|
+
detail: `${debts.length} under-practiced skill(s)`,
|
|
2306
|
+
items: debts.map((d) => ({
|
|
2307
|
+
label: d.tag,
|
|
2308
|
+
value: `\xD7${d.multiplier.toFixed(2)} \xB7 open ${formatAge(now - new Date(d.firstOwedAt).getTime())}`
|
|
2309
|
+
}))
|
|
2310
|
+
});
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
captureStrategyPressure({
|
|
2314
|
+
source: "prescribed",
|
|
2315
|
+
courseId,
|
|
2316
|
+
gauges,
|
|
2317
|
+
topScore: emitted.length > 0 ? Math.max(...emitted.map((c) => c.score)) : null,
|
|
2318
|
+
hintsLabel: hints?._label,
|
|
2319
|
+
timestamp: now
|
|
2320
|
+
});
|
|
2321
|
+
}
|
|
2233
2322
|
parseConfig(serializedData) {
|
|
2234
2323
|
try {
|
|
2235
2324
|
const parsed = JSON.parse(serializedData);
|
|
@@ -2386,6 +2475,7 @@ var init_prescribed = __esm({
|
|
|
2386
2475
|
supportTags: [...supportTags],
|
|
2387
2476
|
pressureMultiplier,
|
|
2388
2477
|
supportMultiplier,
|
|
2478
|
+
sessionsSinceSurfaced,
|
|
2389
2479
|
debugVersion: PRESCRIBED_DEBUG_VERSION
|
|
2390
2480
|
};
|
|
2391
2481
|
}
|
|
@@ -2518,13 +2608,13 @@ var init_prescribed = __esm({
|
|
|
2518
2608
|
nextPracticeDebt
|
|
2519
2609
|
} = args;
|
|
2520
2610
|
const patterns = group.practiceTagPatterns ?? [];
|
|
2521
|
-
if (patterns.length === 0) return [];
|
|
2611
|
+
if (patterns.length === 0) return { cards: [], debts: [] };
|
|
2522
2612
|
const practiceMinCount = group.practiceMinCount ?? DEFAULT_PRACTICE_MIN_COUNT;
|
|
2523
2613
|
const maxPractice = group.maxPracticeCardsPerRun ?? DEFAULT_MAX_PRACTICE_PER_RUN;
|
|
2524
2614
|
const practiceTags = [...cardsByTag.keys()].filter(
|
|
2525
2615
|
(tag) => patterns.some((p) => matchesTagPattern(tag, p)) && this.isUnlockedGatedSkill(tag, hierarchyConfigs, userTagElo, userGlobalElo) && (userTagElo[tag]?.count ?? 0) < practiceMinCount
|
|
2526
2616
|
);
|
|
2527
|
-
if (practiceTags.length === 0) return [];
|
|
2617
|
+
if (practiceTags.length === 0) return { cards: [], debts: [] };
|
|
2528
2618
|
const now = Date.now();
|
|
2529
2619
|
const DAY_MS = 24 * 60 * 60 * 1e3;
|
|
2530
2620
|
const tagMultiplier = /* @__PURE__ */ new Map();
|
|
@@ -2539,6 +2629,11 @@ var init_prescribed = __esm({
|
|
|
2539
2629
|
);
|
|
2540
2630
|
tagMultiplier.set(tag, mult);
|
|
2541
2631
|
}
|
|
2632
|
+
const debts = practiceTags.map((tag) => ({
|
|
2633
|
+
tag,
|
|
2634
|
+
multiplier: tagMultiplier.get(tag) ?? PRACTICE_BASE_MULT,
|
|
2635
|
+
firstOwedAt: nextPracticeDebt[tag]
|
|
2636
|
+
})).sort((a, b) => b.multiplier - a.multiplier || a.tag.localeCompare(b.tag));
|
|
2542
2637
|
const practiceCardIds = this.findDiscoveredSupportCards({
|
|
2543
2638
|
supportTags: practiceTags,
|
|
2544
2639
|
cardsByTag,
|
|
@@ -2547,7 +2642,7 @@ var init_prescribed = __esm({
|
|
|
2547
2642
|
excludedIds: emittedIds,
|
|
2548
2643
|
limit: maxPractice
|
|
2549
2644
|
});
|
|
2550
|
-
if (practiceCardIds.length === 0) return [];
|
|
2645
|
+
if (practiceCardIds.length === 0) return { cards: [], debts };
|
|
2551
2646
|
logger.info(
|
|
2552
2647
|
`[Prescribed] Group '${group.id}' practice: ${practiceTags.length} unlocked under-practiced skill(s), emitting ${practiceCardIds.length} drill card(s)`
|
|
2553
2648
|
);
|
|
@@ -2577,7 +2672,7 @@ var init_prescribed = __esm({
|
|
|
2577
2672
|
]
|
|
2578
2673
|
});
|
|
2579
2674
|
}
|
|
2580
|
-
return cards;
|
|
2675
|
+
return { cards, debts };
|
|
2581
2676
|
}
|
|
2582
2677
|
/**
|
|
2583
2678
|
* True for a skill that was *gated and is now reached*: it has at least one
|
|
@@ -5254,6 +5349,7 @@ var init_3 = __esm({
|
|
|
5254
5349
|
"./PipelineAssembler.ts": () => Promise.resolve().then(() => (init_PipelineAssembler(), PipelineAssembler_exports)),
|
|
5255
5350
|
"./PipelineDebugger.ts": () => Promise.resolve().then(() => (init_PipelineDebugger(), PipelineDebugger_exports)),
|
|
5256
5351
|
"./SrsDebugger.ts": () => Promise.resolve().then(() => (init_SrsDebugger(), SrsDebugger_exports)),
|
|
5352
|
+
"./StrategyPressureDebugger.ts": () => Promise.resolve().then(() => (init_StrategyPressureDebugger(), StrategyPressureDebugger_exports)),
|
|
5257
5353
|
"./defaults.ts": () => Promise.resolve().then(() => (init_defaults(), defaults_exports)),
|
|
5258
5354
|
"./diversityRerank.ts": () => Promise.resolve().then(() => (init_diversityRerank(), diversityRerank_exports)),
|
|
5259
5355
|
"./filters/WeightedFilter.ts": () => Promise.resolve().then(() => (init_WeightedFilter(), WeightedFilter_exports)),
|
|
@@ -5286,7 +5382,9 @@ __export(navigators_exports, {
|
|
|
5286
5382
|
NavigatorRole: () => NavigatorRole,
|
|
5287
5383
|
NavigatorRoles: () => NavigatorRoles,
|
|
5288
5384
|
Navigators: () => Navigators,
|
|
5385
|
+
captureStrategyPressure: () => captureStrategyPressure,
|
|
5289
5386
|
clearSrsBacklogDebug: () => clearSrsBacklogDebug,
|
|
5387
|
+
clearStrategyPressureDebug: () => clearStrategyPressureDebug,
|
|
5290
5388
|
diversityRerank: () => diversityRerank,
|
|
5291
5389
|
getActivePipeline: () => getActivePipeline,
|
|
5292
5390
|
getCardOrigin: () => getCardOrigin,
|
|
@@ -5294,6 +5392,7 @@ __export(navigators_exports, {
|
|
|
5294
5392
|
getRegisteredNavigatorNames: () => getRegisteredNavigatorNames,
|
|
5295
5393
|
getRegisteredNavigatorRole: () => getRegisteredNavigatorRole,
|
|
5296
5394
|
getSrsBacklogDebug: () => getSrsBacklogDebug,
|
|
5395
|
+
getStrategyPressureDebug: () => getStrategyPressureDebug,
|
|
5297
5396
|
hasRegisteredNavigator: () => hasRegisteredNavigator,
|
|
5298
5397
|
initializeNavigatorRegistry: () => initializeNavigatorRegistry,
|
|
5299
5398
|
isFilter: () => isFilter,
|
|
@@ -5376,6 +5475,7 @@ var init_navigators = __esm({
|
|
|
5376
5475
|
init_diversityRerank();
|
|
5377
5476
|
init_PipelineDebugger();
|
|
5378
5477
|
init_SrsDebugger();
|
|
5478
|
+
init_StrategyPressureDebugger();
|
|
5379
5479
|
init_logger();
|
|
5380
5480
|
init_();
|
|
5381
5481
|
init_2();
|
|
@@ -6649,6 +6749,19 @@ var init_couch = __esm({
|
|
|
6649
6749
|
// src/impl/common/BaseUserDB.ts
|
|
6650
6750
|
import { Status as Status3 } from "@vue-skuilder/common";
|
|
6651
6751
|
import moment6 from "moment";
|
|
6752
|
+
function withTimeout(p, ms, label) {
|
|
6753
|
+
let timer;
|
|
6754
|
+
return Promise.race([
|
|
6755
|
+
p,
|
|
6756
|
+
new Promise((_resolve, reject) => {
|
|
6757
|
+
timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
|
|
6758
|
+
})
|
|
6759
|
+
]).finally(() => {
|
|
6760
|
+
if (timer !== void 0) {
|
|
6761
|
+
clearTimeout(timer);
|
|
6762
|
+
}
|
|
6763
|
+
});
|
|
6764
|
+
}
|
|
6652
6765
|
async function getOrCreateClassroomRegistrationsDoc(user) {
|
|
6653
6766
|
let ret;
|
|
6654
6767
|
try {
|
|
@@ -6741,7 +6854,7 @@ async function dropUserFromClassroom(user, classID) {
|
|
|
6741
6854
|
async function getUserClassrooms(user) {
|
|
6742
6855
|
return getOrCreateClassroomRegistrationsDoc(user);
|
|
6743
6856
|
}
|
|
6744
|
-
var log3, BaseUser, userCoursesDoc, userClassroomsDoc;
|
|
6857
|
+
var log3, HYDRATION_TIMEOUT_MS, BaseUser, userCoursesDoc, userClassroomsDoc;
|
|
6745
6858
|
var init_BaseUserDB = __esm({
|
|
6746
6859
|
"src/impl/common/BaseUserDB.ts"() {
|
|
6747
6860
|
"use strict";
|
|
@@ -6756,6 +6869,7 @@ var init_BaseUserDB = __esm({
|
|
|
6756
6869
|
log3 = (s) => {
|
|
6757
6870
|
logger.info(s);
|
|
6758
6871
|
};
|
|
6872
|
+
HYDRATION_TIMEOUT_MS = 15e3;
|
|
6759
6873
|
BaseUser = class _BaseUser {
|
|
6760
6874
|
static _instance;
|
|
6761
6875
|
static _initialized = false;
|
|
@@ -6784,6 +6898,29 @@ var init_BaseUserDB = __esm({
|
|
|
6784
6898
|
writeDB;
|
|
6785
6899
|
// Database to use for write operations (local-first approach)
|
|
6786
6900
|
updateQueue;
|
|
6901
|
+
_hydration = { state: "not-required" };
|
|
6902
|
+
/**
|
|
6903
|
+
* How far the local mirror can be trusted. See {@link UserHydrationStatus}.
|
|
6904
|
+
*
|
|
6905
|
+
* Consumers that would otherwise read a 404 as "new user" — onboarding
|
|
6906
|
+
* gates, first-run experiences, progress dashboards — should check for
|
|
6907
|
+
* `failed` and present a retry affordance rather than an empty state.
|
|
6908
|
+
*/
|
|
6909
|
+
hydrationStatus() {
|
|
6910
|
+
return { ...this._hydration };
|
|
6911
|
+
}
|
|
6912
|
+
/**
|
|
6913
|
+
* Whether a missing document may be treated as genuinely absent, allowing
|
|
6914
|
+
* callers to create it with defaults.
|
|
6915
|
+
*
|
|
6916
|
+
* False only when hydration failed or is still running: a 404 then may just
|
|
6917
|
+
* mean "not pulled down yet", and materializing a default would both lie to
|
|
6918
|
+
* the user and, once live sync starts, push a rev-1 document that loses to
|
|
6919
|
+
* the real one — silently discarding it.
|
|
6920
|
+
*/
|
|
6921
|
+
canMaterializeDefaults() {
|
|
6922
|
+
return this._hydration.state !== "failed" && this._hydration.state !== "hydrating";
|
|
6923
|
+
}
|
|
6787
6924
|
async createAccount(username, password) {
|
|
6788
6925
|
if (!this.syncStrategy.canCreateAccount()) {
|
|
6789
6926
|
throw new Error("Account creation not supported by current sync strategy");
|
|
@@ -6901,6 +7038,11 @@ Currently logged-in as ${this._username}.`
|
|
|
6901
7038
|
} catch (e) {
|
|
6902
7039
|
const err = e;
|
|
6903
7040
|
if (err.status === 404) {
|
|
7041
|
+
if (!this.canMaterializeDefaults()) {
|
|
7042
|
+
throw new Error(
|
|
7043
|
+
`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.`
|
|
7044
|
+
);
|
|
7045
|
+
}
|
|
6904
7046
|
await this.localDB.put({
|
|
6905
7047
|
_id: _BaseUser.DOC_IDS.COURSE_REGISTRATIONS,
|
|
6906
7048
|
courses: [],
|
|
@@ -7143,6 +7285,11 @@ Currently logged-in as ${this._username}.`
|
|
|
7143
7285
|
} catch (e) {
|
|
7144
7286
|
const err = e;
|
|
7145
7287
|
if (err.name && err.name === "not_found") {
|
|
7288
|
+
if (!this.canMaterializeDefaults()) {
|
|
7289
|
+
throw new Error(
|
|
7290
|
+
`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.`
|
|
7291
|
+
);
|
|
7292
|
+
}
|
|
7146
7293
|
await this.localDB.put(defaultConfig);
|
|
7147
7294
|
return this.getConfig();
|
|
7148
7295
|
} else {
|
|
@@ -7216,7 +7363,9 @@ Currently logged-in as ${this._username}.`
|
|
|
7216
7363
|
_BaseUser._initialized = true;
|
|
7217
7364
|
return;
|
|
7218
7365
|
}
|
|
7366
|
+
this.syncStrategy.stopSync?.();
|
|
7219
7367
|
this.setDBandQ();
|
|
7368
|
+
await this.hydrateLocalMirror();
|
|
7220
7369
|
this.syncStrategy.startSync(this.localDB, this.remoteDB);
|
|
7221
7370
|
this.applyDesignDocs().catch((error) => {
|
|
7222
7371
|
log3(`Error in applyDesignDocs background task: ${error}`);
|
|
@@ -7232,6 +7381,85 @@ Currently logged-in as ${this._username}.`
|
|
|
7232
7381
|
});
|
|
7233
7382
|
_BaseUser._initialized = true;
|
|
7234
7383
|
}
|
|
7384
|
+
/**
|
|
7385
|
+
* Pull this account's documents into the local mirror, if that hasn't
|
|
7386
|
+
* happened on this device before.
|
|
7387
|
+
*
|
|
7388
|
+
* Runs between setDBandQ() and startSync() — after the handles exist, before
|
|
7389
|
+
* anything can observe an empty local DB or replicate one upward.
|
|
7390
|
+
*
|
|
7391
|
+
* Never throws: a failure is recorded as `failed` state and reported through
|
|
7392
|
+
* hydrationStatus(), because throwing here would abort init() and leave
|
|
7393
|
+
* BaseUser._initialized false forever (BaseUser.instance() polls it with no
|
|
7394
|
+
* ceiling). Callers decide what a failure means for them.
|
|
7395
|
+
*/
|
|
7396
|
+
async hydrateLocalMirror() {
|
|
7397
|
+
if (this.localDB.name === this.remoteDB.name || !this.syncStrategy.hydrate) {
|
|
7398
|
+
this._hydration = { state: "not-required" };
|
|
7399
|
+
return;
|
|
7400
|
+
}
|
|
7401
|
+
if (await this.hasHydrationMarker()) {
|
|
7402
|
+
this._hydration = { state: "stale" };
|
|
7403
|
+
return;
|
|
7404
|
+
}
|
|
7405
|
+
this._hydration = { state: "hydrating" };
|
|
7406
|
+
const start = Date.now();
|
|
7407
|
+
try {
|
|
7408
|
+
const { docsWritten } = await withTimeout(
|
|
7409
|
+
this.syncStrategy.hydrate(this.localDB, this.remoteDB),
|
|
7410
|
+
HYDRATION_TIMEOUT_MS,
|
|
7411
|
+
`Hydration of local mirror for ${this._username}`
|
|
7412
|
+
);
|
|
7413
|
+
await this.writeHydrationMarker();
|
|
7414
|
+
this._hydration = {
|
|
7415
|
+
state: "hydrated",
|
|
7416
|
+
docsWritten,
|
|
7417
|
+
durationMs: Date.now() - start
|
|
7418
|
+
};
|
|
7419
|
+
log3(
|
|
7420
|
+
`Hydrated local mirror for ${this._username}: ${docsWritten} docs in ${this._hydration.durationMs}ms`
|
|
7421
|
+
);
|
|
7422
|
+
} catch (e) {
|
|
7423
|
+
this.syncStrategy.stopSync?.();
|
|
7424
|
+
this._hydration = {
|
|
7425
|
+
state: "failed",
|
|
7426
|
+
durationMs: Date.now() - start,
|
|
7427
|
+
error: e instanceof Error ? e.message : String(e)
|
|
7428
|
+
};
|
|
7429
|
+
logger.error(`Failed to hydrate local mirror for ${this._username}:`, e);
|
|
7430
|
+
}
|
|
7431
|
+
}
|
|
7432
|
+
/**
|
|
7433
|
+
* Has a full pull completed on this device for the CURRENT account?
|
|
7434
|
+
*
|
|
7435
|
+
* The marker is a `_local/` document, which never replicates — so its
|
|
7436
|
+
* presence describes this device's mirror and cannot arrive from elsewhere.
|
|
7437
|
+
*/
|
|
7438
|
+
async hasHydrationMarker() {
|
|
7439
|
+
try {
|
|
7440
|
+
const marker = await this.localDB.get(HYDRATION_MARKER_ID);
|
|
7441
|
+
return marker.username === this._username;
|
|
7442
|
+
} catch {
|
|
7443
|
+
return false;
|
|
7444
|
+
}
|
|
7445
|
+
}
|
|
7446
|
+
async writeHydrationMarker() {
|
|
7447
|
+
try {
|
|
7448
|
+
let existingRev;
|
|
7449
|
+
try {
|
|
7450
|
+
existingRev = (await this.localDB.get(HYDRATION_MARKER_ID))._rev;
|
|
7451
|
+
} catch {
|
|
7452
|
+
}
|
|
7453
|
+
await this.localDB.put({
|
|
7454
|
+
_id: HYDRATION_MARKER_ID,
|
|
7455
|
+
_rev: existingRev,
|
|
7456
|
+
username: this._username,
|
|
7457
|
+
hydratedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
7458
|
+
});
|
|
7459
|
+
} catch (e) {
|
|
7460
|
+
logger.warn(`Could not write hydration marker for ${this._username}: ${e}`);
|
|
7461
|
+
}
|
|
7462
|
+
}
|
|
7235
7463
|
static designDocs = [
|
|
7236
7464
|
{
|
|
7237
7465
|
_id: "_design/reviewCards",
|
|
@@ -7568,6 +7796,11 @@ Currently logged-in as ${this._username}.`
|
|
|
7568
7796
|
} catch (e) {
|
|
7569
7797
|
const err = e;
|
|
7570
7798
|
if (err.status === 404) {
|
|
7799
|
+
if (!this.canMaterializeDefaults()) {
|
|
7800
|
+
throw new Error(
|
|
7801
|
+
`Cannot read strategy state '${strategyKey}' for ${this._username}: the local mirror is unavailable (hydration state '${this._hydration.state}').`
|
|
7802
|
+
);
|
|
7803
|
+
}
|
|
7571
7804
|
return null;
|
|
7572
7805
|
}
|
|
7573
7806
|
throw e;
|
|
@@ -7915,6 +8148,15 @@ var init_userOutcome = __esm({
|
|
|
7915
8148
|
}
|
|
7916
8149
|
});
|
|
7917
8150
|
|
|
8151
|
+
// src/core/types/hydration.ts
|
|
8152
|
+
var HYDRATION_MARKER_ID;
|
|
8153
|
+
var init_hydration = __esm({
|
|
8154
|
+
"src/core/types/hydration.ts"() {
|
|
8155
|
+
"use strict";
|
|
8156
|
+
HYDRATION_MARKER_ID = "_local/hydration";
|
|
8157
|
+
}
|
|
8158
|
+
});
|
|
8159
|
+
|
|
7918
8160
|
// src/core/bulkImport/cardProcessor.ts
|
|
7919
8161
|
import { Status as Status4 } from "@vue-skuilder/common";
|
|
7920
8162
|
async function importParsedCards(parsedCards, courseDB, config) {
|
|
@@ -8393,6 +8635,7 @@ var init_core = __esm({
|
|
|
8393
8635
|
init_user();
|
|
8394
8636
|
init_strategyState();
|
|
8395
8637
|
init_userOutcome();
|
|
8638
|
+
init_hydration();
|
|
8396
8639
|
init_Loggable();
|
|
8397
8640
|
init_util();
|
|
8398
8641
|
init_navigators();
|
|
@@ -8409,6 +8652,7 @@ export {
|
|
|
8409
8652
|
DocType,
|
|
8410
8653
|
DocTypePrefixes,
|
|
8411
8654
|
GuestUsername,
|
|
8655
|
+
HYDRATION_MARKER_ID,
|
|
8412
8656
|
Loggable,
|
|
8413
8657
|
NavigatorRole,
|
|
8414
8658
|
NavigatorRoles,
|
|
@@ -8416,7 +8660,9 @@ export {
|
|
|
8416
8660
|
aggregateOutcomesForGradient,
|
|
8417
8661
|
areQuestionRecords,
|
|
8418
8662
|
buildStrategyStateId,
|
|
8663
|
+
captureStrategyPressure,
|
|
8419
8664
|
clearSrsBacklogDebug,
|
|
8665
|
+
clearStrategyPressureDebug,
|
|
8420
8666
|
computeDeviation,
|
|
8421
8667
|
computeEffectiveWeight,
|
|
8422
8668
|
computeOutcomeSignal,
|
|
@@ -8433,6 +8679,7 @@ export {
|
|
|
8433
8679
|
getRegisteredNavigatorNames,
|
|
8434
8680
|
getRegisteredNavigatorRole,
|
|
8435
8681
|
getSrsBacklogDebug,
|
|
8682
|
+
getStrategyPressureDebug,
|
|
8436
8683
|
getStudySource,
|
|
8437
8684
|
hasRegisteredNavigator,
|
|
8438
8685
|
importParsedCards,
|