@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
|
@@ -1446,6 +1446,30 @@ var init_SrsDebugger = __esm({
|
|
|
1446
1446
|
}
|
|
1447
1447
|
});
|
|
1448
1448
|
|
|
1449
|
+
// src/core/navigators/StrategyPressureDebugger.ts
|
|
1450
|
+
var StrategyPressureDebugger_exports = {};
|
|
1451
|
+
__export(StrategyPressureDebugger_exports, {
|
|
1452
|
+
captureStrategyPressure: () => captureStrategyPressure,
|
|
1453
|
+
clearStrategyPressureDebug: () => clearStrategyPressureDebug,
|
|
1454
|
+
getStrategyPressureDebug: () => getStrategyPressureDebug
|
|
1455
|
+
});
|
|
1456
|
+
function captureStrategyPressure(snapshot) {
|
|
1457
|
+
snapshots2.set(`${snapshot.source}:${snapshot.courseId}`, snapshot);
|
|
1458
|
+
}
|
|
1459
|
+
function getStrategyPressureDebug() {
|
|
1460
|
+
return [...snapshots2.values()].sort((a, b) => b.timestamp - a.timestamp);
|
|
1461
|
+
}
|
|
1462
|
+
function clearStrategyPressureDebug() {
|
|
1463
|
+
snapshots2.clear();
|
|
1464
|
+
}
|
|
1465
|
+
var snapshots2;
|
|
1466
|
+
var init_StrategyPressureDebugger = __esm({
|
|
1467
|
+
"src/core/navigators/StrategyPressureDebugger.ts"() {
|
|
1468
|
+
"use strict";
|
|
1469
|
+
snapshots2 = /* @__PURE__ */ new Map();
|
|
1470
|
+
}
|
|
1471
|
+
});
|
|
1472
|
+
|
|
1449
1473
|
// src/core/navigators/generators/CompositeGenerator.ts
|
|
1450
1474
|
var CompositeGenerator_exports = {};
|
|
1451
1475
|
__export(CompositeGenerator_exports, {
|
|
@@ -1810,6 +1834,13 @@ function shuffleInPlace(arr) {
|
|
|
1810
1834
|
[arr[i], arr[j]] = [arr[j], arr[i]];
|
|
1811
1835
|
}
|
|
1812
1836
|
}
|
|
1837
|
+
function formatAge(ms) {
|
|
1838
|
+
const minutes = Math.max(0, Math.round(ms / 6e4));
|
|
1839
|
+
if (minutes < 60) return `${minutes}m`;
|
|
1840
|
+
const hours = Math.round(minutes / 60);
|
|
1841
|
+
if (hours < 24) return `${hours}h`;
|
|
1842
|
+
return `${Math.round(hours / 24)}d`;
|
|
1843
|
+
}
|
|
1813
1844
|
function pickTopByScore(cards, limit) {
|
|
1814
1845
|
return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
|
|
1815
1846
|
}
|
|
@@ -1818,6 +1849,7 @@ var init_prescribed = __esm({
|
|
|
1818
1849
|
"src/core/navigators/generators/prescribed.ts"() {
|
|
1819
1850
|
"use strict";
|
|
1820
1851
|
init_navigators();
|
|
1852
|
+
init_StrategyPressureDebugger();
|
|
1821
1853
|
init_logger();
|
|
1822
1854
|
DEFAULT_FRESHNESS_WINDOW = 3;
|
|
1823
1855
|
DEFAULT_MAX_DIRECT_PER_RUN = 3;
|
|
@@ -1894,6 +1926,7 @@ var init_prescribed = __esm({
|
|
|
1894
1926
|
const groupRuntimes = [];
|
|
1895
1927
|
const priorPracticeDebt = progress.practiceDebt ?? {};
|
|
1896
1928
|
const nextPracticeDebt = {};
|
|
1929
|
+
const practiceDebtsByGroup = /* @__PURE__ */ new Map();
|
|
1897
1930
|
for (const group of this.config.groups) {
|
|
1898
1931
|
const runtime = this.buildGroupRuntimeState({
|
|
1899
1932
|
group,
|
|
@@ -1942,7 +1975,7 @@ var init_prescribed = __esm({
|
|
|
1942
1975
|
courseId,
|
|
1943
1976
|
emittedIds
|
|
1944
1977
|
);
|
|
1945
|
-
const
|
|
1978
|
+
const practice = this.buildPracticeCards({
|
|
1946
1979
|
group,
|
|
1947
1980
|
courseId,
|
|
1948
1981
|
emittedIds,
|
|
@@ -1955,7 +1988,8 @@ var init_prescribed = __esm({
|
|
|
1955
1988
|
priorPracticeDebt,
|
|
1956
1989
|
nextPracticeDebt
|
|
1957
1990
|
});
|
|
1958
|
-
|
|
1991
|
+
practiceDebtsByGroup.set(group.id, practice.debts);
|
|
1992
|
+
emitted.push(...directCards, ...supportCards, ...discoveredSupportCards, ...practice.cards);
|
|
1959
1993
|
}
|
|
1960
1994
|
nextState.practiceDebt = nextPracticeDebt;
|
|
1961
1995
|
const hintSummary = this.buildSupportHintSummary(groupRuntimes);
|
|
@@ -1971,6 +2005,7 @@ var init_prescribed = __esm({
|
|
|
1971
2005
|
} else {
|
|
1972
2006
|
logger.info("[Prescribed] No hints to emit (no blocked targets or no support tags)");
|
|
1973
2007
|
}
|
|
2008
|
+
this.capturePressureSnapshot(courseId, groupRuntimes, practiceDebtsByGroup, emitted, hints);
|
|
1974
2009
|
if (emitted.length === 0) {
|
|
1975
2010
|
logger.info(
|
|
1976
2011
|
"[Prescribed] 0 cards emitted (all targets blocked, authored/discovered support candidates exhausted)" + (hints ? " \u2014 boost hints emitted but may not survive filters" : "")
|
|
@@ -2032,6 +2067,60 @@ var init_prescribed = __esm({
|
|
|
2032
2067
|
supportTags: [...supportTags].sort()
|
|
2033
2068
|
};
|
|
2034
2069
|
}
|
|
2070
|
+
/**
|
|
2071
|
+
* Translate this run's per-group runtimes and practice debts into gauges on
|
|
2072
|
+
* the generic strategy-pressure debug channel (rendered by the live session
|
|
2073
|
+
* overlay's "strategy backpressure" section).
|
|
2074
|
+
*/
|
|
2075
|
+
capturePressureSnapshot(courseId, groupRuntimes, practiceDebtsByGroup, emitted, hints) {
|
|
2076
|
+
const now = Date.now();
|
|
2077
|
+
const gauges = [];
|
|
2078
|
+
for (const runtime of groupRuntimes) {
|
|
2079
|
+
const group = runtime.group;
|
|
2080
|
+
const window2 = group.freshnessWindowSessions ?? DEFAULT_FRESHNESS_WINDOW;
|
|
2081
|
+
gauges.push({
|
|
2082
|
+
id: `group:${group.id}:target`,
|
|
2083
|
+
label: `${group.id} targets`,
|
|
2084
|
+
multiplier: runtime.pressureMultiplier,
|
|
2085
|
+
max: MAX_TARGET_MULTIPLIER,
|
|
2086
|
+
detail: `${runtime.pendingTargets.length} pending (${runtime.surfaceableTargets.length} surfaceable, ${runtime.blockedTargets.length} blocked) \xB7 sinceSurfaced ${runtime.sessionsSinceSurfaced}/${window2}`,
|
|
2087
|
+
items: runtime.blockedTargets.map((cardId) => ({ label: cardId, value: "blocked" }))
|
|
2088
|
+
});
|
|
2089
|
+
if (runtime.blockedTargets.length > 0) {
|
|
2090
|
+
const mode = runtime.supportCandidates.length > 0 ? "direct-support" : runtime.discoveredSupportCandidates.length > 0 ? "inserted-support" : "boost-only";
|
|
2091
|
+
gauges.push({
|
|
2092
|
+
id: `group:${group.id}:support`,
|
|
2093
|
+
label: `${group.id} support`,
|
|
2094
|
+
multiplier: runtime.supportMultiplier,
|
|
2095
|
+
max: MAX_SUPPORT_MULTIPLIER,
|
|
2096
|
+
detail: `mode=${mode} \xB7 ${runtime.supportTags.length} support tag(s)`,
|
|
2097
|
+
items: runtime.supportTags.map((tag) => ({ label: tag }))
|
|
2098
|
+
});
|
|
2099
|
+
}
|
|
2100
|
+
const debts = practiceDebtsByGroup.get(group.id) ?? [];
|
|
2101
|
+
if (debts.length > 0) {
|
|
2102
|
+
gauges.push({
|
|
2103
|
+
id: `group:${group.id}:practice-debt`,
|
|
2104
|
+
label: `${group.id} practice debt`,
|
|
2105
|
+
multiplier: Math.max(...debts.map((d) => d.multiplier)),
|
|
2106
|
+
max: MAX_PRACTICE_MULTIPLIER,
|
|
2107
|
+
detail: `${debts.length} under-practiced skill(s)`,
|
|
2108
|
+
items: debts.map((d) => ({
|
|
2109
|
+
label: d.tag,
|
|
2110
|
+
value: `\xD7${d.multiplier.toFixed(2)} \xB7 open ${formatAge(now - new Date(d.firstOwedAt).getTime())}`
|
|
2111
|
+
}))
|
|
2112
|
+
});
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
captureStrategyPressure({
|
|
2116
|
+
source: "prescribed",
|
|
2117
|
+
courseId,
|
|
2118
|
+
gauges,
|
|
2119
|
+
topScore: emitted.length > 0 ? Math.max(...emitted.map((c) => c.score)) : null,
|
|
2120
|
+
hintsLabel: hints?._label,
|
|
2121
|
+
timestamp: now
|
|
2122
|
+
});
|
|
2123
|
+
}
|
|
2035
2124
|
parseConfig(serializedData) {
|
|
2036
2125
|
try {
|
|
2037
2126
|
const parsed = JSON.parse(serializedData);
|
|
@@ -2188,6 +2277,7 @@ var init_prescribed = __esm({
|
|
|
2188
2277
|
supportTags: [...supportTags],
|
|
2189
2278
|
pressureMultiplier,
|
|
2190
2279
|
supportMultiplier,
|
|
2280
|
+
sessionsSinceSurfaced,
|
|
2191
2281
|
debugVersion: PRESCRIBED_DEBUG_VERSION
|
|
2192
2282
|
};
|
|
2193
2283
|
}
|
|
@@ -2320,13 +2410,13 @@ var init_prescribed = __esm({
|
|
|
2320
2410
|
nextPracticeDebt
|
|
2321
2411
|
} = args;
|
|
2322
2412
|
const patterns = group.practiceTagPatterns ?? [];
|
|
2323
|
-
if (patterns.length === 0) return [];
|
|
2413
|
+
if (patterns.length === 0) return { cards: [], debts: [] };
|
|
2324
2414
|
const practiceMinCount = group.practiceMinCount ?? DEFAULT_PRACTICE_MIN_COUNT;
|
|
2325
2415
|
const maxPractice = group.maxPracticeCardsPerRun ?? DEFAULT_MAX_PRACTICE_PER_RUN;
|
|
2326
2416
|
const practiceTags = [...cardsByTag.keys()].filter(
|
|
2327
2417
|
(tag) => patterns.some((p) => matchesTagPattern(tag, p)) && this.isUnlockedGatedSkill(tag, hierarchyConfigs, userTagElo, userGlobalElo) && (userTagElo[tag]?.count ?? 0) < practiceMinCount
|
|
2328
2418
|
);
|
|
2329
|
-
if (practiceTags.length === 0) return [];
|
|
2419
|
+
if (practiceTags.length === 0) return { cards: [], debts: [] };
|
|
2330
2420
|
const now = Date.now();
|
|
2331
2421
|
const DAY_MS = 24 * 60 * 60 * 1e3;
|
|
2332
2422
|
const tagMultiplier = /* @__PURE__ */ new Map();
|
|
@@ -2341,6 +2431,11 @@ var init_prescribed = __esm({
|
|
|
2341
2431
|
);
|
|
2342
2432
|
tagMultiplier.set(tag, mult);
|
|
2343
2433
|
}
|
|
2434
|
+
const debts = practiceTags.map((tag) => ({
|
|
2435
|
+
tag,
|
|
2436
|
+
multiplier: tagMultiplier.get(tag) ?? PRACTICE_BASE_MULT,
|
|
2437
|
+
firstOwedAt: nextPracticeDebt[tag]
|
|
2438
|
+
})).sort((a, b) => b.multiplier - a.multiplier || a.tag.localeCompare(b.tag));
|
|
2344
2439
|
const practiceCardIds = this.findDiscoveredSupportCards({
|
|
2345
2440
|
supportTags: practiceTags,
|
|
2346
2441
|
cardsByTag,
|
|
@@ -2349,7 +2444,7 @@ var init_prescribed = __esm({
|
|
|
2349
2444
|
excludedIds: emittedIds,
|
|
2350
2445
|
limit: maxPractice
|
|
2351
2446
|
});
|
|
2352
|
-
if (practiceCardIds.length === 0) return [];
|
|
2447
|
+
if (practiceCardIds.length === 0) return { cards: [], debts };
|
|
2353
2448
|
logger.info(
|
|
2354
2449
|
`[Prescribed] Group '${group.id}' practice: ${practiceTags.length} unlocked under-practiced skill(s), emitting ${practiceCardIds.length} drill card(s)`
|
|
2355
2450
|
);
|
|
@@ -2379,7 +2474,7 @@ var init_prescribed = __esm({
|
|
|
2379
2474
|
]
|
|
2380
2475
|
});
|
|
2381
2476
|
}
|
|
2382
|
-
return cards;
|
|
2477
|
+
return { cards, debts };
|
|
2383
2478
|
}
|
|
2384
2479
|
/**
|
|
2385
2480
|
* True for a skill that was *gated and is now reached*: it has at least one
|
|
@@ -4810,6 +4905,7 @@ var init_3 = __esm({
|
|
|
4810
4905
|
"./PipelineAssembler.ts": () => Promise.resolve().then(() => (init_PipelineAssembler(), PipelineAssembler_exports)),
|
|
4811
4906
|
"./PipelineDebugger.ts": () => Promise.resolve().then(() => (init_PipelineDebugger(), PipelineDebugger_exports)),
|
|
4812
4907
|
"./SrsDebugger.ts": () => Promise.resolve().then(() => (init_SrsDebugger(), SrsDebugger_exports)),
|
|
4908
|
+
"./StrategyPressureDebugger.ts": () => Promise.resolve().then(() => (init_StrategyPressureDebugger(), StrategyPressureDebugger_exports)),
|
|
4813
4909
|
"./defaults.ts": () => Promise.resolve().then(() => (init_defaults(), defaults_exports)),
|
|
4814
4910
|
"./diversityRerank.ts": () => Promise.resolve().then(() => (init_diversityRerank(), diversityRerank_exports)),
|
|
4815
4911
|
"./filters/WeightedFilter.ts": () => Promise.resolve().then(() => (init_WeightedFilter(), WeightedFilter_exports)),
|
|
@@ -4842,7 +4938,9 @@ __export(navigators_exports, {
|
|
|
4842
4938
|
NavigatorRole: () => NavigatorRole,
|
|
4843
4939
|
NavigatorRoles: () => NavigatorRoles,
|
|
4844
4940
|
Navigators: () => Navigators,
|
|
4941
|
+
captureStrategyPressure: () => captureStrategyPressure,
|
|
4845
4942
|
clearSrsBacklogDebug: () => clearSrsBacklogDebug,
|
|
4943
|
+
clearStrategyPressureDebug: () => clearStrategyPressureDebug,
|
|
4846
4944
|
diversityRerank: () => diversityRerank,
|
|
4847
4945
|
getActivePipeline: () => getActivePipeline,
|
|
4848
4946
|
getCardOrigin: () => getCardOrigin,
|
|
@@ -4850,6 +4948,7 @@ __export(navigators_exports, {
|
|
|
4850
4948
|
getRegisteredNavigatorNames: () => getRegisteredNavigatorNames,
|
|
4851
4949
|
getRegisteredNavigatorRole: () => getRegisteredNavigatorRole,
|
|
4852
4950
|
getSrsBacklogDebug: () => getSrsBacklogDebug,
|
|
4951
|
+
getStrategyPressureDebug: () => getStrategyPressureDebug,
|
|
4853
4952
|
hasRegisteredNavigator: () => hasRegisteredNavigator,
|
|
4854
4953
|
initializeNavigatorRegistry: () => initializeNavigatorRegistry,
|
|
4855
4954
|
isFilter: () => isFilter,
|
|
@@ -4932,6 +5031,7 @@ var init_navigators = __esm({
|
|
|
4932
5031
|
init_diversityRerank();
|
|
4933
5032
|
init_PipelineDebugger();
|
|
4934
5033
|
init_SrsDebugger();
|
|
5034
|
+
init_StrategyPressureDebugger();
|
|
4935
5035
|
init_logger();
|
|
4936
5036
|
init_();
|
|
4937
5037
|
init_2();
|
|
@@ -5270,6 +5370,19 @@ var init_couch = __esm({
|
|
|
5270
5370
|
// src/impl/common/BaseUserDB.ts
|
|
5271
5371
|
import { Status as Status3 } from "@vue-skuilder/common";
|
|
5272
5372
|
import moment6 from "moment";
|
|
5373
|
+
function withTimeout(p, ms, label) {
|
|
5374
|
+
let timer;
|
|
5375
|
+
return Promise.race([
|
|
5376
|
+
p,
|
|
5377
|
+
new Promise((_resolve, reject) => {
|
|
5378
|
+
timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
|
|
5379
|
+
})
|
|
5380
|
+
]).finally(() => {
|
|
5381
|
+
if (timer !== void 0) {
|
|
5382
|
+
clearTimeout(timer);
|
|
5383
|
+
}
|
|
5384
|
+
});
|
|
5385
|
+
}
|
|
5273
5386
|
function accomodateGuest() {
|
|
5274
5387
|
logger.log("[funnel] accomodateGuest() called");
|
|
5275
5388
|
if (typeof localStorage === "undefined") {
|
|
@@ -5443,7 +5556,7 @@ async function dropUserFromClassroom(user, classID) {
|
|
|
5443
5556
|
async function getUserClassrooms(user) {
|
|
5444
5557
|
return getOrCreateClassroomRegistrationsDoc(user);
|
|
5445
5558
|
}
|
|
5446
|
-
var log3, BaseUser, userCoursesDoc, userClassroomsDoc;
|
|
5559
|
+
var log3, HYDRATION_TIMEOUT_MS, BaseUser, userCoursesDoc, userClassroomsDoc;
|
|
5447
5560
|
var init_BaseUserDB = __esm({
|
|
5448
5561
|
"src/impl/common/BaseUserDB.ts"() {
|
|
5449
5562
|
"use strict";
|
|
@@ -5458,6 +5571,7 @@ var init_BaseUserDB = __esm({
|
|
|
5458
5571
|
log3 = (s) => {
|
|
5459
5572
|
logger.info(s);
|
|
5460
5573
|
};
|
|
5574
|
+
HYDRATION_TIMEOUT_MS = 15e3;
|
|
5461
5575
|
BaseUser = class _BaseUser {
|
|
5462
5576
|
static _instance;
|
|
5463
5577
|
static _initialized = false;
|
|
@@ -5486,6 +5600,29 @@ var init_BaseUserDB = __esm({
|
|
|
5486
5600
|
writeDB;
|
|
5487
5601
|
// Database to use for write operations (local-first approach)
|
|
5488
5602
|
updateQueue;
|
|
5603
|
+
_hydration = { state: "not-required" };
|
|
5604
|
+
/**
|
|
5605
|
+
* How far the local mirror can be trusted. See {@link UserHydrationStatus}.
|
|
5606
|
+
*
|
|
5607
|
+
* Consumers that would otherwise read a 404 as "new user" — onboarding
|
|
5608
|
+
* gates, first-run experiences, progress dashboards — should check for
|
|
5609
|
+
* `failed` and present a retry affordance rather than an empty state.
|
|
5610
|
+
*/
|
|
5611
|
+
hydrationStatus() {
|
|
5612
|
+
return { ...this._hydration };
|
|
5613
|
+
}
|
|
5614
|
+
/**
|
|
5615
|
+
* Whether a missing document may be treated as genuinely absent, allowing
|
|
5616
|
+
* callers to create it with defaults.
|
|
5617
|
+
*
|
|
5618
|
+
* False only when hydration failed or is still running: a 404 then may just
|
|
5619
|
+
* mean "not pulled down yet", and materializing a default would both lie to
|
|
5620
|
+
* the user and, once live sync starts, push a rev-1 document that loses to
|
|
5621
|
+
* the real one — silently discarding it.
|
|
5622
|
+
*/
|
|
5623
|
+
canMaterializeDefaults() {
|
|
5624
|
+
return this._hydration.state !== "failed" && this._hydration.state !== "hydrating";
|
|
5625
|
+
}
|
|
5489
5626
|
async createAccount(username, password) {
|
|
5490
5627
|
if (!this.syncStrategy.canCreateAccount()) {
|
|
5491
5628
|
throw new Error("Account creation not supported by current sync strategy");
|
|
@@ -5603,6 +5740,11 @@ Currently logged-in as ${this._username}.`
|
|
|
5603
5740
|
} catch (e) {
|
|
5604
5741
|
const err = e;
|
|
5605
5742
|
if (err.status === 404) {
|
|
5743
|
+
if (!this.canMaterializeDefaults()) {
|
|
5744
|
+
throw new Error(
|
|
5745
|
+
`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.`
|
|
5746
|
+
);
|
|
5747
|
+
}
|
|
5606
5748
|
await this.localDB.put({
|
|
5607
5749
|
_id: _BaseUser.DOC_IDS.COURSE_REGISTRATIONS,
|
|
5608
5750
|
courses: [],
|
|
@@ -5845,6 +5987,11 @@ Currently logged-in as ${this._username}.`
|
|
|
5845
5987
|
} catch (e) {
|
|
5846
5988
|
const err = e;
|
|
5847
5989
|
if (err.name && err.name === "not_found") {
|
|
5990
|
+
if (!this.canMaterializeDefaults()) {
|
|
5991
|
+
throw new Error(
|
|
5992
|
+
`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.`
|
|
5993
|
+
);
|
|
5994
|
+
}
|
|
5848
5995
|
await this.localDB.put(defaultConfig);
|
|
5849
5996
|
return this.getConfig();
|
|
5850
5997
|
} else {
|
|
@@ -5918,7 +6065,9 @@ Currently logged-in as ${this._username}.`
|
|
|
5918
6065
|
_BaseUser._initialized = true;
|
|
5919
6066
|
return;
|
|
5920
6067
|
}
|
|
6068
|
+
this.syncStrategy.stopSync?.();
|
|
5921
6069
|
this.setDBandQ();
|
|
6070
|
+
await this.hydrateLocalMirror();
|
|
5922
6071
|
this.syncStrategy.startSync(this.localDB, this.remoteDB);
|
|
5923
6072
|
this.applyDesignDocs().catch((error) => {
|
|
5924
6073
|
log3(`Error in applyDesignDocs background task: ${error}`);
|
|
@@ -5934,6 +6083,85 @@ Currently logged-in as ${this._username}.`
|
|
|
5934
6083
|
});
|
|
5935
6084
|
_BaseUser._initialized = true;
|
|
5936
6085
|
}
|
|
6086
|
+
/**
|
|
6087
|
+
* Pull this account's documents into the local mirror, if that hasn't
|
|
6088
|
+
* happened on this device before.
|
|
6089
|
+
*
|
|
6090
|
+
* Runs between setDBandQ() and startSync() — after the handles exist, before
|
|
6091
|
+
* anything can observe an empty local DB or replicate one upward.
|
|
6092
|
+
*
|
|
6093
|
+
* Never throws: a failure is recorded as `failed` state and reported through
|
|
6094
|
+
* hydrationStatus(), because throwing here would abort init() and leave
|
|
6095
|
+
* BaseUser._initialized false forever (BaseUser.instance() polls it with no
|
|
6096
|
+
* ceiling). Callers decide what a failure means for them.
|
|
6097
|
+
*/
|
|
6098
|
+
async hydrateLocalMirror() {
|
|
6099
|
+
if (this.localDB.name === this.remoteDB.name || !this.syncStrategy.hydrate) {
|
|
6100
|
+
this._hydration = { state: "not-required" };
|
|
6101
|
+
return;
|
|
6102
|
+
}
|
|
6103
|
+
if (await this.hasHydrationMarker()) {
|
|
6104
|
+
this._hydration = { state: "stale" };
|
|
6105
|
+
return;
|
|
6106
|
+
}
|
|
6107
|
+
this._hydration = { state: "hydrating" };
|
|
6108
|
+
const start = Date.now();
|
|
6109
|
+
try {
|
|
6110
|
+
const { docsWritten } = await withTimeout(
|
|
6111
|
+
this.syncStrategy.hydrate(this.localDB, this.remoteDB),
|
|
6112
|
+
HYDRATION_TIMEOUT_MS,
|
|
6113
|
+
`Hydration of local mirror for ${this._username}`
|
|
6114
|
+
);
|
|
6115
|
+
await this.writeHydrationMarker();
|
|
6116
|
+
this._hydration = {
|
|
6117
|
+
state: "hydrated",
|
|
6118
|
+
docsWritten,
|
|
6119
|
+
durationMs: Date.now() - start
|
|
6120
|
+
};
|
|
6121
|
+
log3(
|
|
6122
|
+
`Hydrated local mirror for ${this._username}: ${docsWritten} docs in ${this._hydration.durationMs}ms`
|
|
6123
|
+
);
|
|
6124
|
+
} catch (e) {
|
|
6125
|
+
this.syncStrategy.stopSync?.();
|
|
6126
|
+
this._hydration = {
|
|
6127
|
+
state: "failed",
|
|
6128
|
+
durationMs: Date.now() - start,
|
|
6129
|
+
error: e instanceof Error ? e.message : String(e)
|
|
6130
|
+
};
|
|
6131
|
+
logger.error(`Failed to hydrate local mirror for ${this._username}:`, e);
|
|
6132
|
+
}
|
|
6133
|
+
}
|
|
6134
|
+
/**
|
|
6135
|
+
* Has a full pull completed on this device for the CURRENT account?
|
|
6136
|
+
*
|
|
6137
|
+
* The marker is a `_local/` document, which never replicates — so its
|
|
6138
|
+
* presence describes this device's mirror and cannot arrive from elsewhere.
|
|
6139
|
+
*/
|
|
6140
|
+
async hasHydrationMarker() {
|
|
6141
|
+
try {
|
|
6142
|
+
const marker = await this.localDB.get(HYDRATION_MARKER_ID);
|
|
6143
|
+
return marker.username === this._username;
|
|
6144
|
+
} catch {
|
|
6145
|
+
return false;
|
|
6146
|
+
}
|
|
6147
|
+
}
|
|
6148
|
+
async writeHydrationMarker() {
|
|
6149
|
+
try {
|
|
6150
|
+
let existingRev;
|
|
6151
|
+
try {
|
|
6152
|
+
existingRev = (await this.localDB.get(HYDRATION_MARKER_ID))._rev;
|
|
6153
|
+
} catch {
|
|
6154
|
+
}
|
|
6155
|
+
await this.localDB.put({
|
|
6156
|
+
_id: HYDRATION_MARKER_ID,
|
|
6157
|
+
_rev: existingRev,
|
|
6158
|
+
username: this._username,
|
|
6159
|
+
hydratedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6160
|
+
});
|
|
6161
|
+
} catch (e) {
|
|
6162
|
+
logger.warn(`Could not write hydration marker for ${this._username}: ${e}`);
|
|
6163
|
+
}
|
|
6164
|
+
}
|
|
5937
6165
|
static designDocs = [
|
|
5938
6166
|
{
|
|
5939
6167
|
_id: "_design/reviewCards",
|
|
@@ -6270,6 +6498,11 @@ Currently logged-in as ${this._username}.`
|
|
|
6270
6498
|
} catch (e) {
|
|
6271
6499
|
const err = e;
|
|
6272
6500
|
if (err.status === 404) {
|
|
6501
|
+
if (!this.canMaterializeDefaults()) {
|
|
6502
|
+
throw new Error(
|
|
6503
|
+
`Cannot read strategy state '${strategyKey}' for ${this._username}: the local mirror is unavailable (hydration state '${this._hydration.state}').`
|
|
6504
|
+
);
|
|
6505
|
+
}
|
|
6273
6506
|
return null;
|
|
6274
6507
|
}
|
|
6275
6508
|
throw e;
|
|
@@ -6443,6 +6676,15 @@ var init_userOutcome = __esm({
|
|
|
6443
6676
|
}
|
|
6444
6677
|
});
|
|
6445
6678
|
|
|
6679
|
+
// src/core/types/hydration.ts
|
|
6680
|
+
var HYDRATION_MARKER_ID;
|
|
6681
|
+
var init_hydration = __esm({
|
|
6682
|
+
"src/core/types/hydration.ts"() {
|
|
6683
|
+
"use strict";
|
|
6684
|
+
HYDRATION_MARKER_ID = "_local/hydration";
|
|
6685
|
+
}
|
|
6686
|
+
});
|
|
6687
|
+
|
|
6446
6688
|
// src/core/bulkImport/cardProcessor.ts
|
|
6447
6689
|
import { Status as Status4 } from "@vue-skuilder/common";
|
|
6448
6690
|
var init_cardProcessor = __esm({
|
|
@@ -6811,6 +7053,7 @@ var init_core = __esm({
|
|
|
6811
7053
|
init_user();
|
|
6812
7054
|
init_strategyState();
|
|
6813
7055
|
init_userOutcome();
|
|
7056
|
+
init_hydration();
|
|
6814
7057
|
init_Loggable();
|
|
6815
7058
|
init_util();
|
|
6816
7059
|
init_navigators();
|