@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/index.js
CHANGED
|
@@ -1820,6 +1820,30 @@ var init_SrsDebugger = __esm({
|
|
|
1820
1820
|
}
|
|
1821
1821
|
});
|
|
1822
1822
|
|
|
1823
|
+
// src/core/navigators/StrategyPressureDebugger.ts
|
|
1824
|
+
var StrategyPressureDebugger_exports = {};
|
|
1825
|
+
__export(StrategyPressureDebugger_exports, {
|
|
1826
|
+
captureStrategyPressure: () => captureStrategyPressure,
|
|
1827
|
+
clearStrategyPressureDebug: () => clearStrategyPressureDebug,
|
|
1828
|
+
getStrategyPressureDebug: () => getStrategyPressureDebug
|
|
1829
|
+
});
|
|
1830
|
+
function captureStrategyPressure(snapshot) {
|
|
1831
|
+
snapshots2.set(`${snapshot.source}:${snapshot.courseId}`, snapshot);
|
|
1832
|
+
}
|
|
1833
|
+
function getStrategyPressureDebug() {
|
|
1834
|
+
return [...snapshots2.values()].sort((a, b) => b.timestamp - a.timestamp);
|
|
1835
|
+
}
|
|
1836
|
+
function clearStrategyPressureDebug() {
|
|
1837
|
+
snapshots2.clear();
|
|
1838
|
+
}
|
|
1839
|
+
var snapshots2;
|
|
1840
|
+
var init_StrategyPressureDebugger = __esm({
|
|
1841
|
+
"src/core/navigators/StrategyPressureDebugger.ts"() {
|
|
1842
|
+
"use strict";
|
|
1843
|
+
snapshots2 = /* @__PURE__ */ new Map();
|
|
1844
|
+
}
|
|
1845
|
+
});
|
|
1846
|
+
|
|
1823
1847
|
// src/core/navigators/generators/CompositeGenerator.ts
|
|
1824
1848
|
var CompositeGenerator_exports = {};
|
|
1825
1849
|
__export(CompositeGenerator_exports, {
|
|
@@ -2184,6 +2208,13 @@ function shuffleInPlace(arr) {
|
|
|
2184
2208
|
[arr[i], arr[j]] = [arr[j], arr[i]];
|
|
2185
2209
|
}
|
|
2186
2210
|
}
|
|
2211
|
+
function formatAge(ms) {
|
|
2212
|
+
const minutes = Math.max(0, Math.round(ms / 6e4));
|
|
2213
|
+
if (minutes < 60) return `${minutes}m`;
|
|
2214
|
+
const hours = Math.round(minutes / 60);
|
|
2215
|
+
if (hours < 24) return `${hours}h`;
|
|
2216
|
+
return `${Math.round(hours / 24)}d`;
|
|
2217
|
+
}
|
|
2187
2218
|
function pickTopByScore(cards, limit) {
|
|
2188
2219
|
return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
|
|
2189
2220
|
}
|
|
@@ -2192,6 +2223,7 @@ var init_prescribed = __esm({
|
|
|
2192
2223
|
"src/core/navigators/generators/prescribed.ts"() {
|
|
2193
2224
|
"use strict";
|
|
2194
2225
|
init_navigators();
|
|
2226
|
+
init_StrategyPressureDebugger();
|
|
2195
2227
|
init_logger();
|
|
2196
2228
|
DEFAULT_FRESHNESS_WINDOW = 3;
|
|
2197
2229
|
DEFAULT_MAX_DIRECT_PER_RUN = 3;
|
|
@@ -2268,6 +2300,7 @@ var init_prescribed = __esm({
|
|
|
2268
2300
|
const groupRuntimes = [];
|
|
2269
2301
|
const priorPracticeDebt = progress.practiceDebt ?? {};
|
|
2270
2302
|
const nextPracticeDebt = {};
|
|
2303
|
+
const practiceDebtsByGroup = /* @__PURE__ */ new Map();
|
|
2271
2304
|
for (const group of this.config.groups) {
|
|
2272
2305
|
const runtime = this.buildGroupRuntimeState({
|
|
2273
2306
|
group,
|
|
@@ -2316,7 +2349,7 @@ var init_prescribed = __esm({
|
|
|
2316
2349
|
courseId,
|
|
2317
2350
|
emittedIds
|
|
2318
2351
|
);
|
|
2319
|
-
const
|
|
2352
|
+
const practice = this.buildPracticeCards({
|
|
2320
2353
|
group,
|
|
2321
2354
|
courseId,
|
|
2322
2355
|
emittedIds,
|
|
@@ -2329,7 +2362,8 @@ var init_prescribed = __esm({
|
|
|
2329
2362
|
priorPracticeDebt,
|
|
2330
2363
|
nextPracticeDebt
|
|
2331
2364
|
});
|
|
2332
|
-
|
|
2365
|
+
practiceDebtsByGroup.set(group.id, practice.debts);
|
|
2366
|
+
emitted.push(...directCards, ...supportCards, ...discoveredSupportCards, ...practice.cards);
|
|
2333
2367
|
}
|
|
2334
2368
|
nextState.practiceDebt = nextPracticeDebt;
|
|
2335
2369
|
const hintSummary = this.buildSupportHintSummary(groupRuntimes);
|
|
@@ -2345,6 +2379,7 @@ var init_prescribed = __esm({
|
|
|
2345
2379
|
} else {
|
|
2346
2380
|
logger.info("[Prescribed] No hints to emit (no blocked targets or no support tags)");
|
|
2347
2381
|
}
|
|
2382
|
+
this.capturePressureSnapshot(courseId, groupRuntimes, practiceDebtsByGroup, emitted, hints);
|
|
2348
2383
|
if (emitted.length === 0) {
|
|
2349
2384
|
logger.info(
|
|
2350
2385
|
"[Prescribed] 0 cards emitted (all targets blocked, authored/discovered support candidates exhausted)" + (hints ? " \u2014 boost hints emitted but may not survive filters" : "")
|
|
@@ -2406,6 +2441,60 @@ var init_prescribed = __esm({
|
|
|
2406
2441
|
supportTags: [...supportTags].sort()
|
|
2407
2442
|
};
|
|
2408
2443
|
}
|
|
2444
|
+
/**
|
|
2445
|
+
* Translate this run's per-group runtimes and practice debts into gauges on
|
|
2446
|
+
* the generic strategy-pressure debug channel (rendered by the live session
|
|
2447
|
+
* overlay's "strategy backpressure" section).
|
|
2448
|
+
*/
|
|
2449
|
+
capturePressureSnapshot(courseId, groupRuntimes, practiceDebtsByGroup, emitted, hints) {
|
|
2450
|
+
const now = Date.now();
|
|
2451
|
+
const gauges = [];
|
|
2452
|
+
for (const runtime of groupRuntimes) {
|
|
2453
|
+
const group = runtime.group;
|
|
2454
|
+
const window2 = group.freshnessWindowSessions ?? DEFAULT_FRESHNESS_WINDOW;
|
|
2455
|
+
gauges.push({
|
|
2456
|
+
id: `group:${group.id}:target`,
|
|
2457
|
+
label: `${group.id} targets`,
|
|
2458
|
+
multiplier: runtime.pressureMultiplier,
|
|
2459
|
+
max: MAX_TARGET_MULTIPLIER,
|
|
2460
|
+
detail: `${runtime.pendingTargets.length} pending (${runtime.surfaceableTargets.length} surfaceable, ${runtime.blockedTargets.length} blocked) \xB7 sinceSurfaced ${runtime.sessionsSinceSurfaced}/${window2}`,
|
|
2461
|
+
items: runtime.blockedTargets.map((cardId) => ({ label: cardId, value: "blocked" }))
|
|
2462
|
+
});
|
|
2463
|
+
if (runtime.blockedTargets.length > 0) {
|
|
2464
|
+
const mode = runtime.supportCandidates.length > 0 ? "direct-support" : runtime.discoveredSupportCandidates.length > 0 ? "inserted-support" : "boost-only";
|
|
2465
|
+
gauges.push({
|
|
2466
|
+
id: `group:${group.id}:support`,
|
|
2467
|
+
label: `${group.id} support`,
|
|
2468
|
+
multiplier: runtime.supportMultiplier,
|
|
2469
|
+
max: MAX_SUPPORT_MULTIPLIER,
|
|
2470
|
+
detail: `mode=${mode} \xB7 ${runtime.supportTags.length} support tag(s)`,
|
|
2471
|
+
items: runtime.supportTags.map((tag) => ({ label: tag }))
|
|
2472
|
+
});
|
|
2473
|
+
}
|
|
2474
|
+
const debts = practiceDebtsByGroup.get(group.id) ?? [];
|
|
2475
|
+
if (debts.length > 0) {
|
|
2476
|
+
gauges.push({
|
|
2477
|
+
id: `group:${group.id}:practice-debt`,
|
|
2478
|
+
label: `${group.id} practice debt`,
|
|
2479
|
+
multiplier: Math.max(...debts.map((d) => d.multiplier)),
|
|
2480
|
+
max: MAX_PRACTICE_MULTIPLIER,
|
|
2481
|
+
detail: `${debts.length} under-practiced skill(s)`,
|
|
2482
|
+
items: debts.map((d) => ({
|
|
2483
|
+
label: d.tag,
|
|
2484
|
+
value: `\xD7${d.multiplier.toFixed(2)} \xB7 open ${formatAge(now - new Date(d.firstOwedAt).getTime())}`
|
|
2485
|
+
}))
|
|
2486
|
+
});
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
captureStrategyPressure({
|
|
2490
|
+
source: "prescribed",
|
|
2491
|
+
courseId,
|
|
2492
|
+
gauges,
|
|
2493
|
+
topScore: emitted.length > 0 ? Math.max(...emitted.map((c) => c.score)) : null,
|
|
2494
|
+
hintsLabel: hints?._label,
|
|
2495
|
+
timestamp: now
|
|
2496
|
+
});
|
|
2497
|
+
}
|
|
2409
2498
|
parseConfig(serializedData) {
|
|
2410
2499
|
try {
|
|
2411
2500
|
const parsed = JSON.parse(serializedData);
|
|
@@ -2562,6 +2651,7 @@ var init_prescribed = __esm({
|
|
|
2562
2651
|
supportTags: [...supportTags],
|
|
2563
2652
|
pressureMultiplier,
|
|
2564
2653
|
supportMultiplier,
|
|
2654
|
+
sessionsSinceSurfaced,
|
|
2565
2655
|
debugVersion: PRESCRIBED_DEBUG_VERSION
|
|
2566
2656
|
};
|
|
2567
2657
|
}
|
|
@@ -2694,13 +2784,13 @@ var init_prescribed = __esm({
|
|
|
2694
2784
|
nextPracticeDebt
|
|
2695
2785
|
} = args;
|
|
2696
2786
|
const patterns = group.practiceTagPatterns ?? [];
|
|
2697
|
-
if (patterns.length === 0) return [];
|
|
2787
|
+
if (patterns.length === 0) return { cards: [], debts: [] };
|
|
2698
2788
|
const practiceMinCount = group.practiceMinCount ?? DEFAULT_PRACTICE_MIN_COUNT;
|
|
2699
2789
|
const maxPractice = group.maxPracticeCardsPerRun ?? DEFAULT_MAX_PRACTICE_PER_RUN;
|
|
2700
2790
|
const practiceTags = [...cardsByTag.keys()].filter(
|
|
2701
2791
|
(tag) => patterns.some((p) => matchesTagPattern(tag, p)) && this.isUnlockedGatedSkill(tag, hierarchyConfigs, userTagElo, userGlobalElo) && (userTagElo[tag]?.count ?? 0) < practiceMinCount
|
|
2702
2792
|
);
|
|
2703
|
-
if (practiceTags.length === 0) return [];
|
|
2793
|
+
if (practiceTags.length === 0) return { cards: [], debts: [] };
|
|
2704
2794
|
const now = Date.now();
|
|
2705
2795
|
const DAY_MS = 24 * 60 * 60 * 1e3;
|
|
2706
2796
|
const tagMultiplier = /* @__PURE__ */ new Map();
|
|
@@ -2715,6 +2805,11 @@ var init_prescribed = __esm({
|
|
|
2715
2805
|
);
|
|
2716
2806
|
tagMultiplier.set(tag, mult);
|
|
2717
2807
|
}
|
|
2808
|
+
const debts = practiceTags.map((tag) => ({
|
|
2809
|
+
tag,
|
|
2810
|
+
multiplier: tagMultiplier.get(tag) ?? PRACTICE_BASE_MULT,
|
|
2811
|
+
firstOwedAt: nextPracticeDebt[tag]
|
|
2812
|
+
})).sort((a, b) => b.multiplier - a.multiplier || a.tag.localeCompare(b.tag));
|
|
2718
2813
|
const practiceCardIds = this.findDiscoveredSupportCards({
|
|
2719
2814
|
supportTags: practiceTags,
|
|
2720
2815
|
cardsByTag,
|
|
@@ -2723,7 +2818,7 @@ var init_prescribed = __esm({
|
|
|
2723
2818
|
excludedIds: emittedIds,
|
|
2724
2819
|
limit: maxPractice
|
|
2725
2820
|
});
|
|
2726
|
-
if (practiceCardIds.length === 0) return [];
|
|
2821
|
+
if (practiceCardIds.length === 0) return { cards: [], debts };
|
|
2727
2822
|
logger.info(
|
|
2728
2823
|
`[Prescribed] Group '${group.id}' practice: ${practiceTags.length} unlocked under-practiced skill(s), emitting ${practiceCardIds.length} drill card(s)`
|
|
2729
2824
|
);
|
|
@@ -2753,7 +2848,7 @@ var init_prescribed = __esm({
|
|
|
2753
2848
|
]
|
|
2754
2849
|
});
|
|
2755
2850
|
}
|
|
2756
|
-
return cards;
|
|
2851
|
+
return { cards, debts };
|
|
2757
2852
|
}
|
|
2758
2853
|
/**
|
|
2759
2854
|
* True for a skill that was *gated and is now reached*: it has at least one
|
|
@@ -5430,6 +5525,7 @@ var init_3 = __esm({
|
|
|
5430
5525
|
"./PipelineAssembler.ts": () => Promise.resolve().then(() => (init_PipelineAssembler(), PipelineAssembler_exports)),
|
|
5431
5526
|
"./PipelineDebugger.ts": () => Promise.resolve().then(() => (init_PipelineDebugger(), PipelineDebugger_exports)),
|
|
5432
5527
|
"./SrsDebugger.ts": () => Promise.resolve().then(() => (init_SrsDebugger(), SrsDebugger_exports)),
|
|
5528
|
+
"./StrategyPressureDebugger.ts": () => Promise.resolve().then(() => (init_StrategyPressureDebugger(), StrategyPressureDebugger_exports)),
|
|
5433
5529
|
"./defaults.ts": () => Promise.resolve().then(() => (init_defaults(), defaults_exports)),
|
|
5434
5530
|
"./diversityRerank.ts": () => Promise.resolve().then(() => (init_diversityRerank(), diversityRerank_exports)),
|
|
5435
5531
|
"./filters/WeightedFilter.ts": () => Promise.resolve().then(() => (init_WeightedFilter(), WeightedFilter_exports)),
|
|
@@ -5462,7 +5558,9 @@ __export(navigators_exports, {
|
|
|
5462
5558
|
NavigatorRole: () => NavigatorRole,
|
|
5463
5559
|
NavigatorRoles: () => NavigatorRoles,
|
|
5464
5560
|
Navigators: () => Navigators,
|
|
5561
|
+
captureStrategyPressure: () => captureStrategyPressure,
|
|
5465
5562
|
clearSrsBacklogDebug: () => clearSrsBacklogDebug,
|
|
5563
|
+
clearStrategyPressureDebug: () => clearStrategyPressureDebug,
|
|
5466
5564
|
diversityRerank: () => diversityRerank,
|
|
5467
5565
|
getActivePipeline: () => getActivePipeline,
|
|
5468
5566
|
getCardOrigin: () => getCardOrigin,
|
|
@@ -5470,6 +5568,7 @@ __export(navigators_exports, {
|
|
|
5470
5568
|
getRegisteredNavigatorNames: () => getRegisteredNavigatorNames,
|
|
5471
5569
|
getRegisteredNavigatorRole: () => getRegisteredNavigatorRole,
|
|
5472
5570
|
getSrsBacklogDebug: () => getSrsBacklogDebug,
|
|
5571
|
+
getStrategyPressureDebug: () => getStrategyPressureDebug,
|
|
5473
5572
|
hasRegisteredNavigator: () => hasRegisteredNavigator,
|
|
5474
5573
|
initializeNavigatorRegistry: () => initializeNavigatorRegistry,
|
|
5475
5574
|
isFilter: () => isFilter,
|
|
@@ -5552,6 +5651,7 @@ var init_navigators = __esm({
|
|
|
5552
5651
|
init_diversityRerank();
|
|
5553
5652
|
init_PipelineDebugger();
|
|
5554
5653
|
init_SrsDebugger();
|
|
5654
|
+
init_StrategyPressureDebugger();
|
|
5555
5655
|
init_logger();
|
|
5556
5656
|
init_();
|
|
5557
5657
|
init_2();
|
|
@@ -7257,6 +7357,8 @@ var init_CouchDBSyncStrategy = __esm({
|
|
|
7257
7357
|
CouchDBSyncStrategy = class {
|
|
7258
7358
|
syncHandle;
|
|
7259
7359
|
// Handle to cancel sync if needed
|
|
7360
|
+
/** In-flight one-shot hydration pull, so a timed-out pull can be abandoned. */
|
|
7361
|
+
hydrationHandle;
|
|
7260
7362
|
setupRemoteDB(username) {
|
|
7261
7363
|
if (username === GuestUsername || username.startsWith(GuestUsername)) {
|
|
7262
7364
|
return getLocalUserDB(username);
|
|
@@ -7271,8 +7373,26 @@ var init_CouchDBSyncStrategy = __esm({
|
|
|
7271
7373
|
return this.getUserDB(username);
|
|
7272
7374
|
}
|
|
7273
7375
|
}
|
|
7376
|
+
/**
|
|
7377
|
+
* One-shot remote → local pull. Resolves once the local mirror is caught up.
|
|
7378
|
+
*
|
|
7379
|
+
* Pull only: pushing here would upload whatever the local DB happens to hold
|
|
7380
|
+
* before we know what the remote already has, which is the conflict-leaf
|
|
7381
|
+
* problem hydration exists to avoid. Local changes go up when startSync()
|
|
7382
|
+
* takes over.
|
|
7383
|
+
*/
|
|
7384
|
+
async hydrate(localDB, remoteDB) {
|
|
7385
|
+
const replication = pouchdb_setup_default.replicate(remoteDB, localDB, {});
|
|
7386
|
+
this.hydrationHandle = replication;
|
|
7387
|
+
try {
|
|
7388
|
+
const info = await replication;
|
|
7389
|
+
return { docsWritten: info.docs_written };
|
|
7390
|
+
} finally {
|
|
7391
|
+
this.hydrationHandle = void 0;
|
|
7392
|
+
}
|
|
7393
|
+
}
|
|
7274
7394
|
startSync(localDB, remoteDB) {
|
|
7275
|
-
if (localDB !== remoteDB) {
|
|
7395
|
+
if (localDB.name !== remoteDB.name) {
|
|
7276
7396
|
this.syncHandle = pouchdb_setup_default.sync(localDB, remoteDB, {
|
|
7277
7397
|
live: true,
|
|
7278
7398
|
retry: true
|
|
@@ -7280,8 +7400,20 @@ var init_CouchDBSyncStrategy = __esm({
|
|
|
7280
7400
|
}
|
|
7281
7401
|
}
|
|
7282
7402
|
stopSync() {
|
|
7403
|
+
if (this.hydrationHandle) {
|
|
7404
|
+
try {
|
|
7405
|
+
this.hydrationHandle.cancel();
|
|
7406
|
+
} catch (e) {
|
|
7407
|
+
logger.warn(`Failed to cancel hydration pull (continuing anyway): ${e}`);
|
|
7408
|
+
}
|
|
7409
|
+
this.hydrationHandle = void 0;
|
|
7410
|
+
}
|
|
7283
7411
|
if (this.syncHandle) {
|
|
7284
|
-
|
|
7412
|
+
try {
|
|
7413
|
+
this.syncHandle.cancel();
|
|
7414
|
+
} catch (e) {
|
|
7415
|
+
logger.warn(`Failed to cancel user sync (continuing anyway): ${e}`);
|
|
7416
|
+
}
|
|
7285
7417
|
this.syncHandle = void 0;
|
|
7286
7418
|
}
|
|
7287
7419
|
}
|
|
@@ -7550,6 +7682,19 @@ var init_couch = __esm({
|
|
|
7550
7682
|
});
|
|
7551
7683
|
|
|
7552
7684
|
// src/impl/common/BaseUserDB.ts
|
|
7685
|
+
function withTimeout(p, ms, label) {
|
|
7686
|
+
let timer;
|
|
7687
|
+
return Promise.race([
|
|
7688
|
+
p,
|
|
7689
|
+
new Promise((_resolve, reject) => {
|
|
7690
|
+
timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
|
|
7691
|
+
})
|
|
7692
|
+
]).finally(() => {
|
|
7693
|
+
if (timer !== void 0) {
|
|
7694
|
+
clearTimeout(timer);
|
|
7695
|
+
}
|
|
7696
|
+
});
|
|
7697
|
+
}
|
|
7553
7698
|
function accomodateGuest() {
|
|
7554
7699
|
logger.log("[funnel] accomodateGuest() called");
|
|
7555
7700
|
if (typeof localStorage === "undefined") {
|
|
@@ -7723,7 +7868,7 @@ async function dropUserFromClassroom(user, classID) {
|
|
|
7723
7868
|
async function getUserClassrooms(user) {
|
|
7724
7869
|
return getOrCreateClassroomRegistrationsDoc(user);
|
|
7725
7870
|
}
|
|
7726
|
-
var import_common12, import_moment6, log4, BaseUser, userCoursesDoc, userClassroomsDoc;
|
|
7871
|
+
var import_common12, import_moment6, log4, HYDRATION_TIMEOUT_MS, BaseUser, userCoursesDoc, userClassroomsDoc;
|
|
7727
7872
|
var init_BaseUserDB = __esm({
|
|
7728
7873
|
"src/impl/common/BaseUserDB.ts"() {
|
|
7729
7874
|
"use strict";
|
|
@@ -7740,6 +7885,7 @@ var init_BaseUserDB = __esm({
|
|
|
7740
7885
|
log4 = (s) => {
|
|
7741
7886
|
logger.info(s);
|
|
7742
7887
|
};
|
|
7888
|
+
HYDRATION_TIMEOUT_MS = 15e3;
|
|
7743
7889
|
BaseUser = class _BaseUser {
|
|
7744
7890
|
static _instance;
|
|
7745
7891
|
static _initialized = false;
|
|
@@ -7768,6 +7914,29 @@ var init_BaseUserDB = __esm({
|
|
|
7768
7914
|
writeDB;
|
|
7769
7915
|
// Database to use for write operations (local-first approach)
|
|
7770
7916
|
updateQueue;
|
|
7917
|
+
_hydration = { state: "not-required" };
|
|
7918
|
+
/**
|
|
7919
|
+
* How far the local mirror can be trusted. See {@link UserHydrationStatus}.
|
|
7920
|
+
*
|
|
7921
|
+
* Consumers that would otherwise read a 404 as "new user" — onboarding
|
|
7922
|
+
* gates, first-run experiences, progress dashboards — should check for
|
|
7923
|
+
* `failed` and present a retry affordance rather than an empty state.
|
|
7924
|
+
*/
|
|
7925
|
+
hydrationStatus() {
|
|
7926
|
+
return { ...this._hydration };
|
|
7927
|
+
}
|
|
7928
|
+
/**
|
|
7929
|
+
* Whether a missing document may be treated as genuinely absent, allowing
|
|
7930
|
+
* callers to create it with defaults.
|
|
7931
|
+
*
|
|
7932
|
+
* False only when hydration failed or is still running: a 404 then may just
|
|
7933
|
+
* mean "not pulled down yet", and materializing a default would both lie to
|
|
7934
|
+
* the user and, once live sync starts, push a rev-1 document that loses to
|
|
7935
|
+
* the real one — silently discarding it.
|
|
7936
|
+
*/
|
|
7937
|
+
canMaterializeDefaults() {
|
|
7938
|
+
return this._hydration.state !== "failed" && this._hydration.state !== "hydrating";
|
|
7939
|
+
}
|
|
7771
7940
|
async createAccount(username, password) {
|
|
7772
7941
|
if (!this.syncStrategy.canCreateAccount()) {
|
|
7773
7942
|
throw new Error("Account creation not supported by current sync strategy");
|
|
@@ -7885,6 +8054,11 @@ Currently logged-in as ${this._username}.`
|
|
|
7885
8054
|
} catch (e) {
|
|
7886
8055
|
const err = e;
|
|
7887
8056
|
if (err.status === 404) {
|
|
8057
|
+
if (!this.canMaterializeDefaults()) {
|
|
8058
|
+
throw new Error(
|
|
8059
|
+
`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.`
|
|
8060
|
+
);
|
|
8061
|
+
}
|
|
7888
8062
|
await this.localDB.put({
|
|
7889
8063
|
_id: _BaseUser.DOC_IDS.COURSE_REGISTRATIONS,
|
|
7890
8064
|
courses: [],
|
|
@@ -8127,6 +8301,11 @@ Currently logged-in as ${this._username}.`
|
|
|
8127
8301
|
} catch (e) {
|
|
8128
8302
|
const err = e;
|
|
8129
8303
|
if (err.name && err.name === "not_found") {
|
|
8304
|
+
if (!this.canMaterializeDefaults()) {
|
|
8305
|
+
throw new Error(
|
|
8306
|
+
`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.`
|
|
8307
|
+
);
|
|
8308
|
+
}
|
|
8130
8309
|
await this.localDB.put(defaultConfig);
|
|
8131
8310
|
return this.getConfig();
|
|
8132
8311
|
} else {
|
|
@@ -8200,7 +8379,9 @@ Currently logged-in as ${this._username}.`
|
|
|
8200
8379
|
_BaseUser._initialized = true;
|
|
8201
8380
|
return;
|
|
8202
8381
|
}
|
|
8382
|
+
this.syncStrategy.stopSync?.();
|
|
8203
8383
|
this.setDBandQ();
|
|
8384
|
+
await this.hydrateLocalMirror();
|
|
8204
8385
|
this.syncStrategy.startSync(this.localDB, this.remoteDB);
|
|
8205
8386
|
this.applyDesignDocs().catch((error) => {
|
|
8206
8387
|
log4(`Error in applyDesignDocs background task: ${error}`);
|
|
@@ -8216,6 +8397,85 @@ Currently logged-in as ${this._username}.`
|
|
|
8216
8397
|
});
|
|
8217
8398
|
_BaseUser._initialized = true;
|
|
8218
8399
|
}
|
|
8400
|
+
/**
|
|
8401
|
+
* Pull this account's documents into the local mirror, if that hasn't
|
|
8402
|
+
* happened on this device before.
|
|
8403
|
+
*
|
|
8404
|
+
* Runs between setDBandQ() and startSync() — after the handles exist, before
|
|
8405
|
+
* anything can observe an empty local DB or replicate one upward.
|
|
8406
|
+
*
|
|
8407
|
+
* Never throws: a failure is recorded as `failed` state and reported through
|
|
8408
|
+
* hydrationStatus(), because throwing here would abort init() and leave
|
|
8409
|
+
* BaseUser._initialized false forever (BaseUser.instance() polls it with no
|
|
8410
|
+
* ceiling). Callers decide what a failure means for them.
|
|
8411
|
+
*/
|
|
8412
|
+
async hydrateLocalMirror() {
|
|
8413
|
+
if (this.localDB.name === this.remoteDB.name || !this.syncStrategy.hydrate) {
|
|
8414
|
+
this._hydration = { state: "not-required" };
|
|
8415
|
+
return;
|
|
8416
|
+
}
|
|
8417
|
+
if (await this.hasHydrationMarker()) {
|
|
8418
|
+
this._hydration = { state: "stale" };
|
|
8419
|
+
return;
|
|
8420
|
+
}
|
|
8421
|
+
this._hydration = { state: "hydrating" };
|
|
8422
|
+
const start = Date.now();
|
|
8423
|
+
try {
|
|
8424
|
+
const { docsWritten } = await withTimeout(
|
|
8425
|
+
this.syncStrategy.hydrate(this.localDB, this.remoteDB),
|
|
8426
|
+
HYDRATION_TIMEOUT_MS,
|
|
8427
|
+
`Hydration of local mirror for ${this._username}`
|
|
8428
|
+
);
|
|
8429
|
+
await this.writeHydrationMarker();
|
|
8430
|
+
this._hydration = {
|
|
8431
|
+
state: "hydrated",
|
|
8432
|
+
docsWritten,
|
|
8433
|
+
durationMs: Date.now() - start
|
|
8434
|
+
};
|
|
8435
|
+
log4(
|
|
8436
|
+
`Hydrated local mirror for ${this._username}: ${docsWritten} docs in ${this._hydration.durationMs}ms`
|
|
8437
|
+
);
|
|
8438
|
+
} catch (e) {
|
|
8439
|
+
this.syncStrategy.stopSync?.();
|
|
8440
|
+
this._hydration = {
|
|
8441
|
+
state: "failed",
|
|
8442
|
+
durationMs: Date.now() - start,
|
|
8443
|
+
error: e instanceof Error ? e.message : String(e)
|
|
8444
|
+
};
|
|
8445
|
+
logger.error(`Failed to hydrate local mirror for ${this._username}:`, e);
|
|
8446
|
+
}
|
|
8447
|
+
}
|
|
8448
|
+
/**
|
|
8449
|
+
* Has a full pull completed on this device for the CURRENT account?
|
|
8450
|
+
*
|
|
8451
|
+
* The marker is a `_local/` document, which never replicates — so its
|
|
8452
|
+
* presence describes this device's mirror and cannot arrive from elsewhere.
|
|
8453
|
+
*/
|
|
8454
|
+
async hasHydrationMarker() {
|
|
8455
|
+
try {
|
|
8456
|
+
const marker = await this.localDB.get(HYDRATION_MARKER_ID);
|
|
8457
|
+
return marker.username === this._username;
|
|
8458
|
+
} catch {
|
|
8459
|
+
return false;
|
|
8460
|
+
}
|
|
8461
|
+
}
|
|
8462
|
+
async writeHydrationMarker() {
|
|
8463
|
+
try {
|
|
8464
|
+
let existingRev;
|
|
8465
|
+
try {
|
|
8466
|
+
existingRev = (await this.localDB.get(HYDRATION_MARKER_ID))._rev;
|
|
8467
|
+
} catch {
|
|
8468
|
+
}
|
|
8469
|
+
await this.localDB.put({
|
|
8470
|
+
_id: HYDRATION_MARKER_ID,
|
|
8471
|
+
_rev: existingRev,
|
|
8472
|
+
username: this._username,
|
|
8473
|
+
hydratedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
8474
|
+
});
|
|
8475
|
+
} catch (e) {
|
|
8476
|
+
logger.warn(`Could not write hydration marker for ${this._username}: ${e}`);
|
|
8477
|
+
}
|
|
8478
|
+
}
|
|
8219
8479
|
static designDocs = [
|
|
8220
8480
|
{
|
|
8221
8481
|
_id: "_design/reviewCards",
|
|
@@ -8552,6 +8812,11 @@ Currently logged-in as ${this._username}.`
|
|
|
8552
8812
|
} catch (e) {
|
|
8553
8813
|
const err = e;
|
|
8554
8814
|
if (err.status === 404) {
|
|
8815
|
+
if (!this.canMaterializeDefaults()) {
|
|
8816
|
+
throw new Error(
|
|
8817
|
+
`Cannot read strategy state '${strategyKey}' for ${this._username}: the local mirror is unavailable (hydration state '${this._hydration.state}').`
|
|
8818
|
+
);
|
|
8819
|
+
}
|
|
8555
8820
|
return null;
|
|
8556
8821
|
}
|
|
8557
8822
|
throw e;
|
|
@@ -10072,6 +10337,15 @@ var init_userOutcome = __esm({
|
|
|
10072
10337
|
}
|
|
10073
10338
|
});
|
|
10074
10339
|
|
|
10340
|
+
// src/core/types/hydration.ts
|
|
10341
|
+
var HYDRATION_MARKER_ID;
|
|
10342
|
+
var init_hydration = __esm({
|
|
10343
|
+
"src/core/types/hydration.ts"() {
|
|
10344
|
+
"use strict";
|
|
10345
|
+
HYDRATION_MARKER_ID = "_local/hydration";
|
|
10346
|
+
}
|
|
10347
|
+
});
|
|
10348
|
+
|
|
10075
10349
|
// src/core/bulkImport/cardProcessor.ts
|
|
10076
10350
|
async function importParsedCards(parsedCards, courseDB, config) {
|
|
10077
10351
|
const results = [];
|
|
@@ -10552,6 +10826,7 @@ var init_core = __esm({
|
|
|
10552
10826
|
init_user();
|
|
10553
10827
|
init_strategyState();
|
|
10554
10828
|
init_userOutcome();
|
|
10829
|
+
init_hydration();
|
|
10555
10830
|
init_Loggable();
|
|
10556
10831
|
init_util();
|
|
10557
10832
|
init_navigators();
|
|
@@ -10574,6 +10849,7 @@ __export(index_exports, {
|
|
|
10574
10849
|
ENV: () => ENV,
|
|
10575
10850
|
FileSystemError: () => FileSystemError,
|
|
10576
10851
|
GuestUsername: () => GuestUsername,
|
|
10852
|
+
HYDRATION_MARKER_ID: () => HYDRATION_MARKER_ID,
|
|
10577
10853
|
Loggable: () => Loggable,
|
|
10578
10854
|
NOT_SET: () => NOT_SET,
|
|
10579
10855
|
NavigatorRole: () => NavigatorRole,
|
|
@@ -10589,8 +10865,10 @@ __export(index_exports, {
|
|
|
10589
10865
|
areQuestionRecords: () => areQuestionRecords,
|
|
10590
10866
|
buildStrategyStateId: () => buildStrategyStateId,
|
|
10591
10867
|
captureMixerRun: () => captureMixerRun,
|
|
10868
|
+
captureStrategyPressure: () => captureStrategyPressure,
|
|
10592
10869
|
clearSrsBacklogDebug: () => clearSrsBacklogDebug,
|
|
10593
10870
|
clearStaleSessionDebugState: () => clearStaleSessionDebugState,
|
|
10871
|
+
clearStrategyPressureDebug: () => clearStrategyPressureDebug,
|
|
10594
10872
|
computeDeviation: () => computeDeviation,
|
|
10595
10873
|
computeEffectiveWeight: () => computeEffectiveWeight,
|
|
10596
10874
|
computeOutcomeSignal: () => computeOutcomeSignal,
|
|
@@ -10613,6 +10891,7 @@ __export(index_exports, {
|
|
|
10613
10891
|
getRegisteredNavigatorNames: () => getRegisteredNavigatorNames,
|
|
10614
10892
|
getRegisteredNavigatorRole: () => getRegisteredNavigatorRole,
|
|
10615
10893
|
getSrsBacklogDebug: () => getSrsBacklogDebug,
|
|
10894
|
+
getStrategyPressureDebug: () => getStrategyPressureDebug,
|
|
10616
10895
|
getStudySource: () => getStudySource,
|
|
10617
10896
|
hasRegisteredNavigator: () => hasRegisteredNavigator,
|
|
10618
10897
|
importParsedCards: () => importParsedCards,
|
|
@@ -13585,6 +13864,7 @@ mountMixerDebugger();
|
|
|
13585
13864
|
init_logger();
|
|
13586
13865
|
init_PipelineDebugger();
|
|
13587
13866
|
init_SrsDebugger();
|
|
13867
|
+
init_StrategyPressureDebugger();
|
|
13588
13868
|
|
|
13589
13869
|
// src/study/SessionOverlay.ts
|
|
13590
13870
|
init_logger();
|
|
@@ -13675,7 +13955,7 @@ function render() {
|
|
|
13675
13955
|
attachHandlers();
|
|
13676
13956
|
return;
|
|
13677
13957
|
}
|
|
13678
|
-
overlayEl.innerHTML = headerHtml() + replanHtml(s) + metaHtml(s) + hintsHtml(s.sessionHints) + backlogHtml(s.reviewBacklog) + queueHtml("supplyQ", "supplyQ", s.supplyQ) + queueHtml("failedQ", "failedQ", s.failedQ) + drawnHtml("drawn", s.drawnCards);
|
|
13958
|
+
overlayEl.innerHTML = headerHtml() + replanHtml(s) + metaHtml(s) + hintsHtml(s.sessionHints) + backlogHtml(s.reviewBacklog) + strategyPressureHtml(s.strategyPressure) + queueHtml("supplyQ", "supplyQ", s.supplyQ) + queueHtml("failedQ", "failedQ", s.failedQ) + drawnHtml("drawn", s.drawnCards);
|
|
13679
13959
|
attachHandlers();
|
|
13680
13960
|
}
|
|
13681
13961
|
function attachHandlers() {
|
|
@@ -13779,6 +14059,46 @@ function backlogHtml(backlog) {
|
|
|
13779
14059
|
}).join("");
|
|
13780
14060
|
return `<div style="margin-bottom:6px"><div style="color:#93c5fd">review backpressure</div>${rows}</div>`;
|
|
13781
14061
|
}
|
|
14062
|
+
function pressureColor(multiplier, max) {
|
|
14063
|
+
if (multiplier <= 1.001) return "#86efac";
|
|
14064
|
+
if (max !== void 0 && max > 1) {
|
|
14065
|
+
return (multiplier - 1) / (max - 1) >= 0.75 ? "#fca5a5" : "#fcd34d";
|
|
14066
|
+
}
|
|
14067
|
+
return multiplier >= 3 ? "#fca5a5" : "#fcd34d";
|
|
14068
|
+
}
|
|
14069
|
+
function toggleKey(raw) {
|
|
14070
|
+
return raw.replace(/[^\w:.|-]/g, "_");
|
|
14071
|
+
}
|
|
14072
|
+
function gaugeHtml(sectionKey, g) {
|
|
14073
|
+
const cap = g.max !== void 0 ? `<span style="opacity:.5">/${g.max}</span>` : "";
|
|
14074
|
+
const detail = g.detail ? ` <span style="opacity:.6">${esc(g.detail)}</span>` : "";
|
|
14075
|
+
let html = `<div style="margin-left:6px">${esc(g.label)} <span style="color:${pressureColor(g.multiplier, g.max)}">\xD7${g.multiplier.toFixed(2)}</span>${cap}${detail}`;
|
|
14076
|
+
const items = g.items ?? [];
|
|
14077
|
+
if (items.length > 0) {
|
|
14078
|
+
const key = toggleKey(`sp|${sectionKey}|${g.id}`);
|
|
14079
|
+
const isOpen = !!expanded[key];
|
|
14080
|
+
const shown = isOpen ? items : items.slice(0, INLINE_THRESHOLD);
|
|
14081
|
+
html += `<div style="margin-left:6px">` + shown.map(
|
|
14082
|
+
(it) => `<div style="white-space:nowrap;opacity:.75">${esc(it.label)}${it.value ? ` <span style="opacity:.6">${esc(it.value)}</span>` : ""}</div>`
|
|
14083
|
+
).join("");
|
|
14084
|
+
if (items.length > INLINE_THRESHOLD) {
|
|
14085
|
+
const footer = isOpen ? "\u25BE show less" : `\u2026 +${items.length - shown.length} more`;
|
|
14086
|
+
html += `<div data-q="${key}" style="cursor:pointer;opacity:.6">${footer}</div>`;
|
|
14087
|
+
}
|
|
14088
|
+
html += `</div>`;
|
|
14089
|
+
}
|
|
14090
|
+
return html + `</div>`;
|
|
14091
|
+
}
|
|
14092
|
+
function strategyPressureHtml(list) {
|
|
14093
|
+
if (!list.length) return "";
|
|
14094
|
+
const sections = list.map((s) => {
|
|
14095
|
+
const sectionKey = `${s.source}:${s.courseId.slice(0, 8)}`;
|
|
14096
|
+
const top = s.topScore !== null && s.topScore !== void 0 ? s.topScore.toFixed(2) : "\u2014";
|
|
14097
|
+
const hints = s.hintsLabel ? `<div style="margin-left:6px;opacity:.6">hints: ${esc(s.hintsLabel)}</div>` : "";
|
|
14098
|
+
return `<div style="margin-left:6px"><span style="opacity:.7">${esc(s.source)} \xB7 ${esc(s.courseId.slice(0, 8))}</span> <span style="opacity:.6">top score ${top}</span>` + s.gauges.map((g) => gaugeHtml(sectionKey, g)).join("") + hints + `</div>`;
|
|
14099
|
+
}).join("");
|
|
14100
|
+
return `<div style="margin-bottom:6px"><div style="color:#fdba74">strategy backpressure</div>${sections}</div>`;
|
|
14101
|
+
}
|
|
13782
14102
|
function fmtScore(score) {
|
|
13783
14103
|
if (score === void 0) return "";
|
|
13784
14104
|
if (!Number.isFinite(score)) return "REQ";
|
|
@@ -13879,6 +14199,24 @@ function snapshotToText(s) {
|
|
|
13879
14199
|
);
|
|
13880
14200
|
}
|
|
13881
14201
|
}
|
|
14202
|
+
if (s.strategyPressure.length) {
|
|
14203
|
+
lines.push("");
|
|
14204
|
+
lines.push("strategy backpressure:");
|
|
14205
|
+
for (const sp of s.strategyPressure) {
|
|
14206
|
+
const top = sp.topScore !== null && sp.topScore !== void 0 ? sp.topScore.toFixed(2) : "\u2014";
|
|
14207
|
+
lines.push(` ${sp.source} (${sp.courseId.slice(0, 8)}): top score ${top}`);
|
|
14208
|
+
for (const g of sp.gauges) {
|
|
14209
|
+
const cap = g.max !== void 0 ? `/${g.max}` : "";
|
|
14210
|
+
lines.push(
|
|
14211
|
+
` ${g.label} \xD7${g.multiplier.toFixed(2)}${cap}${g.detail ? ` \u2014 ${g.detail}` : ""}`
|
|
14212
|
+
);
|
|
14213
|
+
for (const it of g.items ?? []) {
|
|
14214
|
+
lines.push(` ${it.label}${it.value ? ` (${it.value})` : ""}`);
|
|
14215
|
+
}
|
|
14216
|
+
}
|
|
14217
|
+
if (sp.hintsLabel) lines.push(` hints: ${sp.hintsLabel}`);
|
|
14218
|
+
}
|
|
14219
|
+
}
|
|
13882
14220
|
const queueText = (label, q) => {
|
|
13883
14221
|
lines.push("");
|
|
13884
14222
|
lines.push(`${label}: ${q.length} (drawn ${q.dequeueCount})`);
|
|
@@ -13916,6 +14254,7 @@ var MAX_HISTORY = 5;
|
|
|
13916
14254
|
function clearStaleSessionDebugState() {
|
|
13917
14255
|
clearRunHistory();
|
|
13918
14256
|
clearSrsBacklogDebug();
|
|
14257
|
+
clearStrategyPressureDebug();
|
|
13919
14258
|
}
|
|
13920
14259
|
function startSessionTracking(supplyQLength, failedQLength) {
|
|
13921
14260
|
const sessionId = `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -14604,6 +14943,7 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14604
14943
|
supplyQ: describe(this.supplyQ),
|
|
14605
14944
|
failedQ: describe(this.failedQ),
|
|
14606
14945
|
reviewBacklog: getSrsBacklogDebug(),
|
|
14946
|
+
strategyPressure: getStrategyPressureDebug(),
|
|
14607
14947
|
drawnCards
|
|
14608
14948
|
};
|
|
14609
14949
|
}
|
|
@@ -15290,6 +15630,7 @@ init_userDBHelpers();
|
|
|
15290
15630
|
ENV,
|
|
15291
15631
|
FileSystemError,
|
|
15292
15632
|
GuestUsername,
|
|
15633
|
+
HYDRATION_MARKER_ID,
|
|
15293
15634
|
Loggable,
|
|
15294
15635
|
NOT_SET,
|
|
15295
15636
|
NavigatorRole,
|
|
@@ -15305,8 +15646,10 @@ init_userDBHelpers();
|
|
|
15305
15646
|
areQuestionRecords,
|
|
15306
15647
|
buildStrategyStateId,
|
|
15307
15648
|
captureMixerRun,
|
|
15649
|
+
captureStrategyPressure,
|
|
15308
15650
|
clearSrsBacklogDebug,
|
|
15309
15651
|
clearStaleSessionDebugState,
|
|
15652
|
+
clearStrategyPressureDebug,
|
|
15310
15653
|
computeDeviation,
|
|
15311
15654
|
computeEffectiveWeight,
|
|
15312
15655
|
computeOutcomeSignal,
|
|
@@ -15329,6 +15672,7 @@ init_userDBHelpers();
|
|
|
15329
15672
|
getRegisteredNavigatorNames,
|
|
15330
15673
|
getRegisteredNavigatorRole,
|
|
15331
15674
|
getSrsBacklogDebug,
|
|
15675
|
+
getStrategyPressureDebug,
|
|
15332
15676
|
getStudySource,
|
|
15333
15677
|
hasRegisteredNavigator,
|
|
15334
15678
|
importParsedCards,
|