@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,9 +1,9 @@
|
|
|
1
|
-
import { U as UserDBInterface, C as CourseDBInterface, b as CoursesDBInterface, c as ClassroomDBInterface, A as AdminDBInterface, a as UserDBReader, d as CourseInfo, S as StudySessionItem, D as DataLayerResult, e as ContentNavigationStrategyData, f as ContentNavigator, R as ReplanHints, G as GeneratorResult } from '../../contentSource-
|
|
2
|
-
import { D as DataLayerProvider } from '../../dataLayerProvider-
|
|
1
|
+
import { U as UserDBInterface, C as CourseDBInterface, b as CoursesDBInterface, c as ClassroomDBInterface, A as AdminDBInterface, a as UserDBReader, d as CourseInfo, S as StudySessionItem, D as DataLayerResult, e as ContentNavigationStrategyData, f as ContentNavigator, R as ReplanHints, G as GeneratorResult } from '../../contentSource-CBqZCoGU.cjs';
|
|
2
|
+
import { D as DataLayerProvider } from '../../dataLayerProvider-BBA8tJNx.cjs';
|
|
3
3
|
import { S as StaticCourseManifest } from '../../types-BFUa1pa3.cjs';
|
|
4
4
|
import { CourseConfig, CourseElo, DataShape } from '@vue-skuilder/common';
|
|
5
5
|
import { S as SkuilderCourseData, Q as QualifiedCardID, T as TagStub, a as Tag } from '../../types-legacy-4tlwHnXo.cjs';
|
|
6
|
-
import { S as SyncStrategy, A as AccountCreationResult, a as AuthenticationResult } from '../../SyncStrategy-
|
|
6
|
+
import { S as SyncStrategy, A as AccountCreationResult, a as AuthenticationResult } from '../../SyncStrategy-BJMZq3WO.cjs';
|
|
7
7
|
import 'moment';
|
|
8
8
|
|
|
9
9
|
interface SkuilderManifest {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { U as UserDBInterface, C as CourseDBInterface, b as CoursesDBInterface, c as ClassroomDBInterface, A as AdminDBInterface, a as UserDBReader, d as CourseInfo, S as StudySessionItem, D as DataLayerResult, e as ContentNavigationStrategyData, f as ContentNavigator, R as ReplanHints, G as GeneratorResult } from '../../contentSource-
|
|
2
|
-
import { D as DataLayerProvider } from '../../dataLayerProvider-
|
|
1
|
+
import { U as UserDBInterface, C as CourseDBInterface, b as CoursesDBInterface, c as ClassroomDBInterface, A as AdminDBInterface, a as UserDBReader, d as CourseInfo, S as StudySessionItem, D as DataLayerResult, e as ContentNavigationStrategyData, f as ContentNavigator, R as ReplanHints, G as GeneratorResult } from '../../contentSource-CudEz5Tm.js';
|
|
2
|
+
import { D as DataLayerProvider } from '../../dataLayerProvider-C9WgkBzR.js';
|
|
3
3
|
import { S as StaticCourseManifest } from '../../types-CHgpWQAY.js';
|
|
4
4
|
import { CourseConfig, CourseElo, DataShape } from '@vue-skuilder/common';
|
|
5
5
|
import { S as SkuilderCourseData, Q as QualifiedCardID, T as TagStub, a as Tag } from '../../types-legacy-4tlwHnXo.js';
|
|
6
|
-
import { S as SyncStrategy, A as AccountCreationResult, a as AuthenticationResult } from '../../SyncStrategy-
|
|
6
|
+
import { S as SyncStrategy, A as AccountCreationResult, a as AuthenticationResult } from '../../SyncStrategy-BJMZq3WO.js';
|
|
7
7
|
import 'moment';
|
|
8
8
|
|
|
9
9
|
interface SkuilderManifest {
|
|
@@ -1470,6 +1470,30 @@ var init_SrsDebugger = __esm({
|
|
|
1470
1470
|
}
|
|
1471
1471
|
});
|
|
1472
1472
|
|
|
1473
|
+
// src/core/navigators/StrategyPressureDebugger.ts
|
|
1474
|
+
var StrategyPressureDebugger_exports = {};
|
|
1475
|
+
__export(StrategyPressureDebugger_exports, {
|
|
1476
|
+
captureStrategyPressure: () => captureStrategyPressure,
|
|
1477
|
+
clearStrategyPressureDebug: () => clearStrategyPressureDebug,
|
|
1478
|
+
getStrategyPressureDebug: () => getStrategyPressureDebug
|
|
1479
|
+
});
|
|
1480
|
+
function captureStrategyPressure(snapshot) {
|
|
1481
|
+
snapshots2.set(`${snapshot.source}:${snapshot.courseId}`, snapshot);
|
|
1482
|
+
}
|
|
1483
|
+
function getStrategyPressureDebug() {
|
|
1484
|
+
return [...snapshots2.values()].sort((a, b) => b.timestamp - a.timestamp);
|
|
1485
|
+
}
|
|
1486
|
+
function clearStrategyPressureDebug() {
|
|
1487
|
+
snapshots2.clear();
|
|
1488
|
+
}
|
|
1489
|
+
var snapshots2;
|
|
1490
|
+
var init_StrategyPressureDebugger = __esm({
|
|
1491
|
+
"src/core/navigators/StrategyPressureDebugger.ts"() {
|
|
1492
|
+
"use strict";
|
|
1493
|
+
snapshots2 = /* @__PURE__ */ new Map();
|
|
1494
|
+
}
|
|
1495
|
+
});
|
|
1496
|
+
|
|
1473
1497
|
// src/core/navigators/generators/CompositeGenerator.ts
|
|
1474
1498
|
var CompositeGenerator_exports = {};
|
|
1475
1499
|
__export(CompositeGenerator_exports, {
|
|
@@ -1834,6 +1858,13 @@ function shuffleInPlace(arr) {
|
|
|
1834
1858
|
[arr[i], arr[j]] = [arr[j], arr[i]];
|
|
1835
1859
|
}
|
|
1836
1860
|
}
|
|
1861
|
+
function formatAge(ms) {
|
|
1862
|
+
const minutes = Math.max(0, Math.round(ms / 6e4));
|
|
1863
|
+
if (minutes < 60) return `${minutes}m`;
|
|
1864
|
+
const hours = Math.round(minutes / 60);
|
|
1865
|
+
if (hours < 24) return `${hours}h`;
|
|
1866
|
+
return `${Math.round(hours / 24)}d`;
|
|
1867
|
+
}
|
|
1837
1868
|
function pickTopByScore(cards, limit) {
|
|
1838
1869
|
return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
|
|
1839
1870
|
}
|
|
@@ -1842,6 +1873,7 @@ var init_prescribed = __esm({
|
|
|
1842
1873
|
"src/core/navigators/generators/prescribed.ts"() {
|
|
1843
1874
|
"use strict";
|
|
1844
1875
|
init_navigators();
|
|
1876
|
+
init_StrategyPressureDebugger();
|
|
1845
1877
|
init_logger();
|
|
1846
1878
|
DEFAULT_FRESHNESS_WINDOW = 3;
|
|
1847
1879
|
DEFAULT_MAX_DIRECT_PER_RUN = 3;
|
|
@@ -1918,6 +1950,7 @@ var init_prescribed = __esm({
|
|
|
1918
1950
|
const groupRuntimes = [];
|
|
1919
1951
|
const priorPracticeDebt = progress.practiceDebt ?? {};
|
|
1920
1952
|
const nextPracticeDebt = {};
|
|
1953
|
+
const practiceDebtsByGroup = /* @__PURE__ */ new Map();
|
|
1921
1954
|
for (const group of this.config.groups) {
|
|
1922
1955
|
const runtime = this.buildGroupRuntimeState({
|
|
1923
1956
|
group,
|
|
@@ -1966,7 +1999,7 @@ var init_prescribed = __esm({
|
|
|
1966
1999
|
courseId,
|
|
1967
2000
|
emittedIds
|
|
1968
2001
|
);
|
|
1969
|
-
const
|
|
2002
|
+
const practice = this.buildPracticeCards({
|
|
1970
2003
|
group,
|
|
1971
2004
|
courseId,
|
|
1972
2005
|
emittedIds,
|
|
@@ -1979,7 +2012,8 @@ var init_prescribed = __esm({
|
|
|
1979
2012
|
priorPracticeDebt,
|
|
1980
2013
|
nextPracticeDebt
|
|
1981
2014
|
});
|
|
1982
|
-
|
|
2015
|
+
practiceDebtsByGroup.set(group.id, practice.debts);
|
|
2016
|
+
emitted.push(...directCards, ...supportCards, ...discoveredSupportCards, ...practice.cards);
|
|
1983
2017
|
}
|
|
1984
2018
|
nextState.practiceDebt = nextPracticeDebt;
|
|
1985
2019
|
const hintSummary = this.buildSupportHintSummary(groupRuntimes);
|
|
@@ -1995,6 +2029,7 @@ var init_prescribed = __esm({
|
|
|
1995
2029
|
} else {
|
|
1996
2030
|
logger.info("[Prescribed] No hints to emit (no blocked targets or no support tags)");
|
|
1997
2031
|
}
|
|
2032
|
+
this.capturePressureSnapshot(courseId, groupRuntimes, practiceDebtsByGroup, emitted, hints);
|
|
1998
2033
|
if (emitted.length === 0) {
|
|
1999
2034
|
logger.info(
|
|
2000
2035
|
"[Prescribed] 0 cards emitted (all targets blocked, authored/discovered support candidates exhausted)" + (hints ? " \u2014 boost hints emitted but may not survive filters" : "")
|
|
@@ -2056,6 +2091,60 @@ var init_prescribed = __esm({
|
|
|
2056
2091
|
supportTags: [...supportTags].sort()
|
|
2057
2092
|
};
|
|
2058
2093
|
}
|
|
2094
|
+
/**
|
|
2095
|
+
* Translate this run's per-group runtimes and practice debts into gauges on
|
|
2096
|
+
* the generic strategy-pressure debug channel (rendered by the live session
|
|
2097
|
+
* overlay's "strategy backpressure" section).
|
|
2098
|
+
*/
|
|
2099
|
+
capturePressureSnapshot(courseId, groupRuntimes, practiceDebtsByGroup, emitted, hints) {
|
|
2100
|
+
const now = Date.now();
|
|
2101
|
+
const gauges = [];
|
|
2102
|
+
for (const runtime of groupRuntimes) {
|
|
2103
|
+
const group = runtime.group;
|
|
2104
|
+
const window2 = group.freshnessWindowSessions ?? DEFAULT_FRESHNESS_WINDOW;
|
|
2105
|
+
gauges.push({
|
|
2106
|
+
id: `group:${group.id}:target`,
|
|
2107
|
+
label: `${group.id} targets`,
|
|
2108
|
+
multiplier: runtime.pressureMultiplier,
|
|
2109
|
+
max: MAX_TARGET_MULTIPLIER,
|
|
2110
|
+
detail: `${runtime.pendingTargets.length} pending (${runtime.surfaceableTargets.length} surfaceable, ${runtime.blockedTargets.length} blocked) \xB7 sinceSurfaced ${runtime.sessionsSinceSurfaced}/${window2}`,
|
|
2111
|
+
items: runtime.blockedTargets.map((cardId) => ({ label: cardId, value: "blocked" }))
|
|
2112
|
+
});
|
|
2113
|
+
if (runtime.blockedTargets.length > 0) {
|
|
2114
|
+
const mode = runtime.supportCandidates.length > 0 ? "direct-support" : runtime.discoveredSupportCandidates.length > 0 ? "inserted-support" : "boost-only";
|
|
2115
|
+
gauges.push({
|
|
2116
|
+
id: `group:${group.id}:support`,
|
|
2117
|
+
label: `${group.id} support`,
|
|
2118
|
+
multiplier: runtime.supportMultiplier,
|
|
2119
|
+
max: MAX_SUPPORT_MULTIPLIER,
|
|
2120
|
+
detail: `mode=${mode} \xB7 ${runtime.supportTags.length} support tag(s)`,
|
|
2121
|
+
items: runtime.supportTags.map((tag) => ({ label: tag }))
|
|
2122
|
+
});
|
|
2123
|
+
}
|
|
2124
|
+
const debts = practiceDebtsByGroup.get(group.id) ?? [];
|
|
2125
|
+
if (debts.length > 0) {
|
|
2126
|
+
gauges.push({
|
|
2127
|
+
id: `group:${group.id}:practice-debt`,
|
|
2128
|
+
label: `${group.id} practice debt`,
|
|
2129
|
+
multiplier: Math.max(...debts.map((d) => d.multiplier)),
|
|
2130
|
+
max: MAX_PRACTICE_MULTIPLIER,
|
|
2131
|
+
detail: `${debts.length} under-practiced skill(s)`,
|
|
2132
|
+
items: debts.map((d) => ({
|
|
2133
|
+
label: d.tag,
|
|
2134
|
+
value: `\xD7${d.multiplier.toFixed(2)} \xB7 open ${formatAge(now - new Date(d.firstOwedAt).getTime())}`
|
|
2135
|
+
}))
|
|
2136
|
+
});
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
captureStrategyPressure({
|
|
2140
|
+
source: "prescribed",
|
|
2141
|
+
courseId,
|
|
2142
|
+
gauges,
|
|
2143
|
+
topScore: emitted.length > 0 ? Math.max(...emitted.map((c) => c.score)) : null,
|
|
2144
|
+
hintsLabel: hints?._label,
|
|
2145
|
+
timestamp: now
|
|
2146
|
+
});
|
|
2147
|
+
}
|
|
2059
2148
|
parseConfig(serializedData) {
|
|
2060
2149
|
try {
|
|
2061
2150
|
const parsed = JSON.parse(serializedData);
|
|
@@ -2212,6 +2301,7 @@ var init_prescribed = __esm({
|
|
|
2212
2301
|
supportTags: [...supportTags],
|
|
2213
2302
|
pressureMultiplier,
|
|
2214
2303
|
supportMultiplier,
|
|
2304
|
+
sessionsSinceSurfaced,
|
|
2215
2305
|
debugVersion: PRESCRIBED_DEBUG_VERSION
|
|
2216
2306
|
};
|
|
2217
2307
|
}
|
|
@@ -2344,13 +2434,13 @@ var init_prescribed = __esm({
|
|
|
2344
2434
|
nextPracticeDebt
|
|
2345
2435
|
} = args;
|
|
2346
2436
|
const patterns = group.practiceTagPatterns ?? [];
|
|
2347
|
-
if (patterns.length === 0) return [];
|
|
2437
|
+
if (patterns.length === 0) return { cards: [], debts: [] };
|
|
2348
2438
|
const practiceMinCount = group.practiceMinCount ?? DEFAULT_PRACTICE_MIN_COUNT;
|
|
2349
2439
|
const maxPractice = group.maxPracticeCardsPerRun ?? DEFAULT_MAX_PRACTICE_PER_RUN;
|
|
2350
2440
|
const practiceTags = [...cardsByTag.keys()].filter(
|
|
2351
2441
|
(tag) => patterns.some((p) => matchesTagPattern(tag, p)) && this.isUnlockedGatedSkill(tag, hierarchyConfigs, userTagElo, userGlobalElo) && (userTagElo[tag]?.count ?? 0) < practiceMinCount
|
|
2352
2442
|
);
|
|
2353
|
-
if (practiceTags.length === 0) return [];
|
|
2443
|
+
if (practiceTags.length === 0) return { cards: [], debts: [] };
|
|
2354
2444
|
const now = Date.now();
|
|
2355
2445
|
const DAY_MS = 24 * 60 * 60 * 1e3;
|
|
2356
2446
|
const tagMultiplier = /* @__PURE__ */ new Map();
|
|
@@ -2365,6 +2455,11 @@ var init_prescribed = __esm({
|
|
|
2365
2455
|
);
|
|
2366
2456
|
tagMultiplier.set(tag, mult);
|
|
2367
2457
|
}
|
|
2458
|
+
const debts = practiceTags.map((tag) => ({
|
|
2459
|
+
tag,
|
|
2460
|
+
multiplier: tagMultiplier.get(tag) ?? PRACTICE_BASE_MULT,
|
|
2461
|
+
firstOwedAt: nextPracticeDebt[tag]
|
|
2462
|
+
})).sort((a, b) => b.multiplier - a.multiplier || a.tag.localeCompare(b.tag));
|
|
2368
2463
|
const practiceCardIds = this.findDiscoveredSupportCards({
|
|
2369
2464
|
supportTags: practiceTags,
|
|
2370
2465
|
cardsByTag,
|
|
@@ -2373,7 +2468,7 @@ var init_prescribed = __esm({
|
|
|
2373
2468
|
excludedIds: emittedIds,
|
|
2374
2469
|
limit: maxPractice
|
|
2375
2470
|
});
|
|
2376
|
-
if (practiceCardIds.length === 0) return [];
|
|
2471
|
+
if (practiceCardIds.length === 0) return { cards: [], debts };
|
|
2377
2472
|
logger.info(
|
|
2378
2473
|
`[Prescribed] Group '${group.id}' practice: ${practiceTags.length} unlocked under-practiced skill(s), emitting ${practiceCardIds.length} drill card(s)`
|
|
2379
2474
|
);
|
|
@@ -2403,7 +2498,7 @@ var init_prescribed = __esm({
|
|
|
2403
2498
|
]
|
|
2404
2499
|
});
|
|
2405
2500
|
}
|
|
2406
|
-
return cards;
|
|
2501
|
+
return { cards, debts };
|
|
2407
2502
|
}
|
|
2408
2503
|
/**
|
|
2409
2504
|
* True for a skill that was *gated and is now reached*: it has at least one
|
|
@@ -4834,6 +4929,7 @@ var init_3 = __esm({
|
|
|
4834
4929
|
"./PipelineAssembler.ts": () => Promise.resolve().then(() => (init_PipelineAssembler(), PipelineAssembler_exports)),
|
|
4835
4930
|
"./PipelineDebugger.ts": () => Promise.resolve().then(() => (init_PipelineDebugger(), PipelineDebugger_exports)),
|
|
4836
4931
|
"./SrsDebugger.ts": () => Promise.resolve().then(() => (init_SrsDebugger(), SrsDebugger_exports)),
|
|
4932
|
+
"./StrategyPressureDebugger.ts": () => Promise.resolve().then(() => (init_StrategyPressureDebugger(), StrategyPressureDebugger_exports)),
|
|
4837
4933
|
"./defaults.ts": () => Promise.resolve().then(() => (init_defaults(), defaults_exports)),
|
|
4838
4934
|
"./diversityRerank.ts": () => Promise.resolve().then(() => (init_diversityRerank(), diversityRerank_exports)),
|
|
4839
4935
|
"./filters/WeightedFilter.ts": () => Promise.resolve().then(() => (init_WeightedFilter(), WeightedFilter_exports)),
|
|
@@ -4866,7 +4962,9 @@ __export(navigators_exports, {
|
|
|
4866
4962
|
NavigatorRole: () => NavigatorRole,
|
|
4867
4963
|
NavigatorRoles: () => NavigatorRoles,
|
|
4868
4964
|
Navigators: () => Navigators,
|
|
4965
|
+
captureStrategyPressure: () => captureStrategyPressure,
|
|
4869
4966
|
clearSrsBacklogDebug: () => clearSrsBacklogDebug,
|
|
4967
|
+
clearStrategyPressureDebug: () => clearStrategyPressureDebug,
|
|
4870
4968
|
diversityRerank: () => diversityRerank,
|
|
4871
4969
|
getActivePipeline: () => getActivePipeline,
|
|
4872
4970
|
getCardOrigin: () => getCardOrigin,
|
|
@@ -4874,6 +4972,7 @@ __export(navigators_exports, {
|
|
|
4874
4972
|
getRegisteredNavigatorNames: () => getRegisteredNavigatorNames,
|
|
4875
4973
|
getRegisteredNavigatorRole: () => getRegisteredNavigatorRole,
|
|
4876
4974
|
getSrsBacklogDebug: () => getSrsBacklogDebug,
|
|
4975
|
+
getStrategyPressureDebug: () => getStrategyPressureDebug,
|
|
4877
4976
|
hasRegisteredNavigator: () => hasRegisteredNavigator,
|
|
4878
4977
|
initializeNavigatorRegistry: () => initializeNavigatorRegistry,
|
|
4879
4978
|
isFilter: () => isFilter,
|
|
@@ -4956,6 +5055,7 @@ var init_navigators = __esm({
|
|
|
4956
5055
|
init_diversityRerank();
|
|
4957
5056
|
init_PipelineDebugger();
|
|
4958
5057
|
init_SrsDebugger();
|
|
5058
|
+
init_StrategyPressureDebugger();
|
|
4959
5059
|
init_logger();
|
|
4960
5060
|
init_();
|
|
4961
5061
|
init_2();
|
|
@@ -5291,6 +5391,19 @@ var init_couch = __esm({
|
|
|
5291
5391
|
});
|
|
5292
5392
|
|
|
5293
5393
|
// src/impl/common/BaseUserDB.ts
|
|
5394
|
+
function withTimeout(p, ms, label) {
|
|
5395
|
+
let timer;
|
|
5396
|
+
return Promise.race([
|
|
5397
|
+
p,
|
|
5398
|
+
new Promise((_resolve, reject) => {
|
|
5399
|
+
timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
|
|
5400
|
+
})
|
|
5401
|
+
]).finally(() => {
|
|
5402
|
+
if (timer !== void 0) {
|
|
5403
|
+
clearTimeout(timer);
|
|
5404
|
+
}
|
|
5405
|
+
});
|
|
5406
|
+
}
|
|
5294
5407
|
function accomodateGuest() {
|
|
5295
5408
|
logger.log("[funnel] accomodateGuest() called");
|
|
5296
5409
|
if (typeof localStorage === "undefined") {
|
|
@@ -5464,7 +5577,7 @@ async function dropUserFromClassroom(user, classID) {
|
|
|
5464
5577
|
async function getUserClassrooms(user) {
|
|
5465
5578
|
return getOrCreateClassroomRegistrationsDoc(user);
|
|
5466
5579
|
}
|
|
5467
|
-
var import_common12, import_moment6, log3, BaseUser, userCoursesDoc, userClassroomsDoc;
|
|
5580
|
+
var import_common12, import_moment6, log3, HYDRATION_TIMEOUT_MS, BaseUser, userCoursesDoc, userClassroomsDoc;
|
|
5468
5581
|
var init_BaseUserDB = __esm({
|
|
5469
5582
|
"src/impl/common/BaseUserDB.ts"() {
|
|
5470
5583
|
"use strict";
|
|
@@ -5481,6 +5594,7 @@ var init_BaseUserDB = __esm({
|
|
|
5481
5594
|
log3 = (s) => {
|
|
5482
5595
|
logger.info(s);
|
|
5483
5596
|
};
|
|
5597
|
+
HYDRATION_TIMEOUT_MS = 15e3;
|
|
5484
5598
|
BaseUser = class _BaseUser {
|
|
5485
5599
|
static _instance;
|
|
5486
5600
|
static _initialized = false;
|
|
@@ -5509,6 +5623,29 @@ var init_BaseUserDB = __esm({
|
|
|
5509
5623
|
writeDB;
|
|
5510
5624
|
// Database to use for write operations (local-first approach)
|
|
5511
5625
|
updateQueue;
|
|
5626
|
+
_hydration = { state: "not-required" };
|
|
5627
|
+
/**
|
|
5628
|
+
* How far the local mirror can be trusted. See {@link UserHydrationStatus}.
|
|
5629
|
+
*
|
|
5630
|
+
* Consumers that would otherwise read a 404 as "new user" — onboarding
|
|
5631
|
+
* gates, first-run experiences, progress dashboards — should check for
|
|
5632
|
+
* `failed` and present a retry affordance rather than an empty state.
|
|
5633
|
+
*/
|
|
5634
|
+
hydrationStatus() {
|
|
5635
|
+
return { ...this._hydration };
|
|
5636
|
+
}
|
|
5637
|
+
/**
|
|
5638
|
+
* Whether a missing document may be treated as genuinely absent, allowing
|
|
5639
|
+
* callers to create it with defaults.
|
|
5640
|
+
*
|
|
5641
|
+
* False only when hydration failed or is still running: a 404 then may just
|
|
5642
|
+
* mean "not pulled down yet", and materializing a default would both lie to
|
|
5643
|
+
* the user and, once live sync starts, push a rev-1 document that loses to
|
|
5644
|
+
* the real one — silently discarding it.
|
|
5645
|
+
*/
|
|
5646
|
+
canMaterializeDefaults() {
|
|
5647
|
+
return this._hydration.state !== "failed" && this._hydration.state !== "hydrating";
|
|
5648
|
+
}
|
|
5512
5649
|
async createAccount(username, password) {
|
|
5513
5650
|
if (!this.syncStrategy.canCreateAccount()) {
|
|
5514
5651
|
throw new Error("Account creation not supported by current sync strategy");
|
|
@@ -5626,6 +5763,11 @@ Currently logged-in as ${this._username}.`
|
|
|
5626
5763
|
} catch (e) {
|
|
5627
5764
|
const err = e;
|
|
5628
5765
|
if (err.status === 404) {
|
|
5766
|
+
if (!this.canMaterializeDefaults()) {
|
|
5767
|
+
throw new Error(
|
|
5768
|
+
`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.`
|
|
5769
|
+
);
|
|
5770
|
+
}
|
|
5629
5771
|
await this.localDB.put({
|
|
5630
5772
|
_id: _BaseUser.DOC_IDS.COURSE_REGISTRATIONS,
|
|
5631
5773
|
courses: [],
|
|
@@ -5868,6 +6010,11 @@ Currently logged-in as ${this._username}.`
|
|
|
5868
6010
|
} catch (e) {
|
|
5869
6011
|
const err = e;
|
|
5870
6012
|
if (err.name && err.name === "not_found") {
|
|
6013
|
+
if (!this.canMaterializeDefaults()) {
|
|
6014
|
+
throw new Error(
|
|
6015
|
+
`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.`
|
|
6016
|
+
);
|
|
6017
|
+
}
|
|
5871
6018
|
await this.localDB.put(defaultConfig);
|
|
5872
6019
|
return this.getConfig();
|
|
5873
6020
|
} else {
|
|
@@ -5941,7 +6088,9 @@ Currently logged-in as ${this._username}.`
|
|
|
5941
6088
|
_BaseUser._initialized = true;
|
|
5942
6089
|
return;
|
|
5943
6090
|
}
|
|
6091
|
+
this.syncStrategy.stopSync?.();
|
|
5944
6092
|
this.setDBandQ();
|
|
6093
|
+
await this.hydrateLocalMirror();
|
|
5945
6094
|
this.syncStrategy.startSync(this.localDB, this.remoteDB);
|
|
5946
6095
|
this.applyDesignDocs().catch((error) => {
|
|
5947
6096
|
log3(`Error in applyDesignDocs background task: ${error}`);
|
|
@@ -5957,6 +6106,85 @@ Currently logged-in as ${this._username}.`
|
|
|
5957
6106
|
});
|
|
5958
6107
|
_BaseUser._initialized = true;
|
|
5959
6108
|
}
|
|
6109
|
+
/**
|
|
6110
|
+
* Pull this account's documents into the local mirror, if that hasn't
|
|
6111
|
+
* happened on this device before.
|
|
6112
|
+
*
|
|
6113
|
+
* Runs between setDBandQ() and startSync() — after the handles exist, before
|
|
6114
|
+
* anything can observe an empty local DB or replicate one upward.
|
|
6115
|
+
*
|
|
6116
|
+
* Never throws: a failure is recorded as `failed` state and reported through
|
|
6117
|
+
* hydrationStatus(), because throwing here would abort init() and leave
|
|
6118
|
+
* BaseUser._initialized false forever (BaseUser.instance() polls it with no
|
|
6119
|
+
* ceiling). Callers decide what a failure means for them.
|
|
6120
|
+
*/
|
|
6121
|
+
async hydrateLocalMirror() {
|
|
6122
|
+
if (this.localDB.name === this.remoteDB.name || !this.syncStrategy.hydrate) {
|
|
6123
|
+
this._hydration = { state: "not-required" };
|
|
6124
|
+
return;
|
|
6125
|
+
}
|
|
6126
|
+
if (await this.hasHydrationMarker()) {
|
|
6127
|
+
this._hydration = { state: "stale" };
|
|
6128
|
+
return;
|
|
6129
|
+
}
|
|
6130
|
+
this._hydration = { state: "hydrating" };
|
|
6131
|
+
const start = Date.now();
|
|
6132
|
+
try {
|
|
6133
|
+
const { docsWritten } = await withTimeout(
|
|
6134
|
+
this.syncStrategy.hydrate(this.localDB, this.remoteDB),
|
|
6135
|
+
HYDRATION_TIMEOUT_MS,
|
|
6136
|
+
`Hydration of local mirror for ${this._username}`
|
|
6137
|
+
);
|
|
6138
|
+
await this.writeHydrationMarker();
|
|
6139
|
+
this._hydration = {
|
|
6140
|
+
state: "hydrated",
|
|
6141
|
+
docsWritten,
|
|
6142
|
+
durationMs: Date.now() - start
|
|
6143
|
+
};
|
|
6144
|
+
log3(
|
|
6145
|
+
`Hydrated local mirror for ${this._username}: ${docsWritten} docs in ${this._hydration.durationMs}ms`
|
|
6146
|
+
);
|
|
6147
|
+
} catch (e) {
|
|
6148
|
+
this.syncStrategy.stopSync?.();
|
|
6149
|
+
this._hydration = {
|
|
6150
|
+
state: "failed",
|
|
6151
|
+
durationMs: Date.now() - start,
|
|
6152
|
+
error: e instanceof Error ? e.message : String(e)
|
|
6153
|
+
};
|
|
6154
|
+
logger.error(`Failed to hydrate local mirror for ${this._username}:`, e);
|
|
6155
|
+
}
|
|
6156
|
+
}
|
|
6157
|
+
/**
|
|
6158
|
+
* Has a full pull completed on this device for the CURRENT account?
|
|
6159
|
+
*
|
|
6160
|
+
* The marker is a `_local/` document, which never replicates — so its
|
|
6161
|
+
* presence describes this device's mirror and cannot arrive from elsewhere.
|
|
6162
|
+
*/
|
|
6163
|
+
async hasHydrationMarker() {
|
|
6164
|
+
try {
|
|
6165
|
+
const marker = await this.localDB.get(HYDRATION_MARKER_ID);
|
|
6166
|
+
return marker.username === this._username;
|
|
6167
|
+
} catch {
|
|
6168
|
+
return false;
|
|
6169
|
+
}
|
|
6170
|
+
}
|
|
6171
|
+
async writeHydrationMarker() {
|
|
6172
|
+
try {
|
|
6173
|
+
let existingRev;
|
|
6174
|
+
try {
|
|
6175
|
+
existingRev = (await this.localDB.get(HYDRATION_MARKER_ID))._rev;
|
|
6176
|
+
} catch {
|
|
6177
|
+
}
|
|
6178
|
+
await this.localDB.put({
|
|
6179
|
+
_id: HYDRATION_MARKER_ID,
|
|
6180
|
+
_rev: existingRev,
|
|
6181
|
+
username: this._username,
|
|
6182
|
+
hydratedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6183
|
+
});
|
|
6184
|
+
} catch (e) {
|
|
6185
|
+
logger.warn(`Could not write hydration marker for ${this._username}: ${e}`);
|
|
6186
|
+
}
|
|
6187
|
+
}
|
|
5960
6188
|
static designDocs = [
|
|
5961
6189
|
{
|
|
5962
6190
|
_id: "_design/reviewCards",
|
|
@@ -6293,6 +6521,11 @@ Currently logged-in as ${this._username}.`
|
|
|
6293
6521
|
} catch (e) {
|
|
6294
6522
|
const err = e;
|
|
6295
6523
|
if (err.status === 404) {
|
|
6524
|
+
if (!this.canMaterializeDefaults()) {
|
|
6525
|
+
throw new Error(
|
|
6526
|
+
`Cannot read strategy state '${strategyKey}' for ${this._username}: the local mirror is unavailable (hydration state '${this._hydration.state}').`
|
|
6527
|
+
);
|
|
6528
|
+
}
|
|
6296
6529
|
return null;
|
|
6297
6530
|
}
|
|
6298
6531
|
throw e;
|
|
@@ -6468,6 +6701,15 @@ var init_userOutcome = __esm({
|
|
|
6468
6701
|
}
|
|
6469
6702
|
});
|
|
6470
6703
|
|
|
6704
|
+
// src/core/types/hydration.ts
|
|
6705
|
+
var HYDRATION_MARKER_ID;
|
|
6706
|
+
var init_hydration = __esm({
|
|
6707
|
+
"src/core/types/hydration.ts"() {
|
|
6708
|
+
"use strict";
|
|
6709
|
+
HYDRATION_MARKER_ID = "_local/hydration";
|
|
6710
|
+
}
|
|
6711
|
+
});
|
|
6712
|
+
|
|
6471
6713
|
// src/core/bulkImport/cardProcessor.ts
|
|
6472
6714
|
var import_common16;
|
|
6473
6715
|
var init_cardProcessor = __esm({
|
|
@@ -6837,6 +7079,7 @@ var init_core = __esm({
|
|
|
6837
7079
|
init_user();
|
|
6838
7080
|
init_strategyState();
|
|
6839
7081
|
init_userOutcome();
|
|
7082
|
+
init_hydration();
|
|
6840
7083
|
init_Loggable();
|
|
6841
7084
|
init_util();
|
|
6842
7085
|
init_navigators();
|