@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.mjs
CHANGED
|
@@ -1797,6 +1797,30 @@ var init_SrsDebugger = __esm({
|
|
|
1797
1797
|
}
|
|
1798
1798
|
});
|
|
1799
1799
|
|
|
1800
|
+
// src/core/navigators/StrategyPressureDebugger.ts
|
|
1801
|
+
var StrategyPressureDebugger_exports = {};
|
|
1802
|
+
__export(StrategyPressureDebugger_exports, {
|
|
1803
|
+
captureStrategyPressure: () => captureStrategyPressure,
|
|
1804
|
+
clearStrategyPressureDebug: () => clearStrategyPressureDebug,
|
|
1805
|
+
getStrategyPressureDebug: () => getStrategyPressureDebug
|
|
1806
|
+
});
|
|
1807
|
+
function captureStrategyPressure(snapshot) {
|
|
1808
|
+
snapshots2.set(`${snapshot.source}:${snapshot.courseId}`, snapshot);
|
|
1809
|
+
}
|
|
1810
|
+
function getStrategyPressureDebug() {
|
|
1811
|
+
return [...snapshots2.values()].sort((a, b) => b.timestamp - a.timestamp);
|
|
1812
|
+
}
|
|
1813
|
+
function clearStrategyPressureDebug() {
|
|
1814
|
+
snapshots2.clear();
|
|
1815
|
+
}
|
|
1816
|
+
var snapshots2;
|
|
1817
|
+
var init_StrategyPressureDebugger = __esm({
|
|
1818
|
+
"src/core/navigators/StrategyPressureDebugger.ts"() {
|
|
1819
|
+
"use strict";
|
|
1820
|
+
snapshots2 = /* @__PURE__ */ new Map();
|
|
1821
|
+
}
|
|
1822
|
+
});
|
|
1823
|
+
|
|
1800
1824
|
// src/core/navigators/generators/CompositeGenerator.ts
|
|
1801
1825
|
var CompositeGenerator_exports = {};
|
|
1802
1826
|
__export(CompositeGenerator_exports, {
|
|
@@ -2161,6 +2185,13 @@ function shuffleInPlace(arr) {
|
|
|
2161
2185
|
[arr[i], arr[j]] = [arr[j], arr[i]];
|
|
2162
2186
|
}
|
|
2163
2187
|
}
|
|
2188
|
+
function formatAge(ms) {
|
|
2189
|
+
const minutes = Math.max(0, Math.round(ms / 6e4));
|
|
2190
|
+
if (minutes < 60) return `${minutes}m`;
|
|
2191
|
+
const hours = Math.round(minutes / 60);
|
|
2192
|
+
if (hours < 24) return `${hours}h`;
|
|
2193
|
+
return `${Math.round(hours / 24)}d`;
|
|
2194
|
+
}
|
|
2164
2195
|
function pickTopByScore(cards, limit) {
|
|
2165
2196
|
return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
|
|
2166
2197
|
}
|
|
@@ -2169,6 +2200,7 @@ var init_prescribed = __esm({
|
|
|
2169
2200
|
"src/core/navigators/generators/prescribed.ts"() {
|
|
2170
2201
|
"use strict";
|
|
2171
2202
|
init_navigators();
|
|
2203
|
+
init_StrategyPressureDebugger();
|
|
2172
2204
|
init_logger();
|
|
2173
2205
|
DEFAULT_FRESHNESS_WINDOW = 3;
|
|
2174
2206
|
DEFAULT_MAX_DIRECT_PER_RUN = 3;
|
|
@@ -2245,6 +2277,7 @@ var init_prescribed = __esm({
|
|
|
2245
2277
|
const groupRuntimes = [];
|
|
2246
2278
|
const priorPracticeDebt = progress.practiceDebt ?? {};
|
|
2247
2279
|
const nextPracticeDebt = {};
|
|
2280
|
+
const practiceDebtsByGroup = /* @__PURE__ */ new Map();
|
|
2248
2281
|
for (const group of this.config.groups) {
|
|
2249
2282
|
const runtime = this.buildGroupRuntimeState({
|
|
2250
2283
|
group,
|
|
@@ -2293,7 +2326,7 @@ var init_prescribed = __esm({
|
|
|
2293
2326
|
courseId,
|
|
2294
2327
|
emittedIds
|
|
2295
2328
|
);
|
|
2296
|
-
const
|
|
2329
|
+
const practice = this.buildPracticeCards({
|
|
2297
2330
|
group,
|
|
2298
2331
|
courseId,
|
|
2299
2332
|
emittedIds,
|
|
@@ -2306,7 +2339,8 @@ var init_prescribed = __esm({
|
|
|
2306
2339
|
priorPracticeDebt,
|
|
2307
2340
|
nextPracticeDebt
|
|
2308
2341
|
});
|
|
2309
|
-
|
|
2342
|
+
practiceDebtsByGroup.set(group.id, practice.debts);
|
|
2343
|
+
emitted.push(...directCards, ...supportCards, ...discoveredSupportCards, ...practice.cards);
|
|
2310
2344
|
}
|
|
2311
2345
|
nextState.practiceDebt = nextPracticeDebt;
|
|
2312
2346
|
const hintSummary = this.buildSupportHintSummary(groupRuntimes);
|
|
@@ -2322,6 +2356,7 @@ var init_prescribed = __esm({
|
|
|
2322
2356
|
} else {
|
|
2323
2357
|
logger.info("[Prescribed] No hints to emit (no blocked targets or no support tags)");
|
|
2324
2358
|
}
|
|
2359
|
+
this.capturePressureSnapshot(courseId, groupRuntimes, practiceDebtsByGroup, emitted, hints);
|
|
2325
2360
|
if (emitted.length === 0) {
|
|
2326
2361
|
logger.info(
|
|
2327
2362
|
"[Prescribed] 0 cards emitted (all targets blocked, authored/discovered support candidates exhausted)" + (hints ? " \u2014 boost hints emitted but may not survive filters" : "")
|
|
@@ -2383,6 +2418,60 @@ var init_prescribed = __esm({
|
|
|
2383
2418
|
supportTags: [...supportTags].sort()
|
|
2384
2419
|
};
|
|
2385
2420
|
}
|
|
2421
|
+
/**
|
|
2422
|
+
* Translate this run's per-group runtimes and practice debts into gauges on
|
|
2423
|
+
* the generic strategy-pressure debug channel (rendered by the live session
|
|
2424
|
+
* overlay's "strategy backpressure" section).
|
|
2425
|
+
*/
|
|
2426
|
+
capturePressureSnapshot(courseId, groupRuntimes, practiceDebtsByGroup, emitted, hints) {
|
|
2427
|
+
const now = Date.now();
|
|
2428
|
+
const gauges = [];
|
|
2429
|
+
for (const runtime of groupRuntimes) {
|
|
2430
|
+
const group = runtime.group;
|
|
2431
|
+
const window2 = group.freshnessWindowSessions ?? DEFAULT_FRESHNESS_WINDOW;
|
|
2432
|
+
gauges.push({
|
|
2433
|
+
id: `group:${group.id}:target`,
|
|
2434
|
+
label: `${group.id} targets`,
|
|
2435
|
+
multiplier: runtime.pressureMultiplier,
|
|
2436
|
+
max: MAX_TARGET_MULTIPLIER,
|
|
2437
|
+
detail: `${runtime.pendingTargets.length} pending (${runtime.surfaceableTargets.length} surfaceable, ${runtime.blockedTargets.length} blocked) \xB7 sinceSurfaced ${runtime.sessionsSinceSurfaced}/${window2}`,
|
|
2438
|
+
items: runtime.blockedTargets.map((cardId) => ({ label: cardId, value: "blocked" }))
|
|
2439
|
+
});
|
|
2440
|
+
if (runtime.blockedTargets.length > 0) {
|
|
2441
|
+
const mode = runtime.supportCandidates.length > 0 ? "direct-support" : runtime.discoveredSupportCandidates.length > 0 ? "inserted-support" : "boost-only";
|
|
2442
|
+
gauges.push({
|
|
2443
|
+
id: `group:${group.id}:support`,
|
|
2444
|
+
label: `${group.id} support`,
|
|
2445
|
+
multiplier: runtime.supportMultiplier,
|
|
2446
|
+
max: MAX_SUPPORT_MULTIPLIER,
|
|
2447
|
+
detail: `mode=${mode} \xB7 ${runtime.supportTags.length} support tag(s)`,
|
|
2448
|
+
items: runtime.supportTags.map((tag) => ({ label: tag }))
|
|
2449
|
+
});
|
|
2450
|
+
}
|
|
2451
|
+
const debts = practiceDebtsByGroup.get(group.id) ?? [];
|
|
2452
|
+
if (debts.length > 0) {
|
|
2453
|
+
gauges.push({
|
|
2454
|
+
id: `group:${group.id}:practice-debt`,
|
|
2455
|
+
label: `${group.id} practice debt`,
|
|
2456
|
+
multiplier: Math.max(...debts.map((d) => d.multiplier)),
|
|
2457
|
+
max: MAX_PRACTICE_MULTIPLIER,
|
|
2458
|
+
detail: `${debts.length} under-practiced skill(s)`,
|
|
2459
|
+
items: debts.map((d) => ({
|
|
2460
|
+
label: d.tag,
|
|
2461
|
+
value: `\xD7${d.multiplier.toFixed(2)} \xB7 open ${formatAge(now - new Date(d.firstOwedAt).getTime())}`
|
|
2462
|
+
}))
|
|
2463
|
+
});
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
captureStrategyPressure({
|
|
2467
|
+
source: "prescribed",
|
|
2468
|
+
courseId,
|
|
2469
|
+
gauges,
|
|
2470
|
+
topScore: emitted.length > 0 ? Math.max(...emitted.map((c) => c.score)) : null,
|
|
2471
|
+
hintsLabel: hints?._label,
|
|
2472
|
+
timestamp: now
|
|
2473
|
+
});
|
|
2474
|
+
}
|
|
2386
2475
|
parseConfig(serializedData) {
|
|
2387
2476
|
try {
|
|
2388
2477
|
const parsed = JSON.parse(serializedData);
|
|
@@ -2539,6 +2628,7 @@ var init_prescribed = __esm({
|
|
|
2539
2628
|
supportTags: [...supportTags],
|
|
2540
2629
|
pressureMultiplier,
|
|
2541
2630
|
supportMultiplier,
|
|
2631
|
+
sessionsSinceSurfaced,
|
|
2542
2632
|
debugVersion: PRESCRIBED_DEBUG_VERSION
|
|
2543
2633
|
};
|
|
2544
2634
|
}
|
|
@@ -2671,13 +2761,13 @@ var init_prescribed = __esm({
|
|
|
2671
2761
|
nextPracticeDebt
|
|
2672
2762
|
} = args;
|
|
2673
2763
|
const patterns = group.practiceTagPatterns ?? [];
|
|
2674
|
-
if (patterns.length === 0) return [];
|
|
2764
|
+
if (patterns.length === 0) return { cards: [], debts: [] };
|
|
2675
2765
|
const practiceMinCount = group.practiceMinCount ?? DEFAULT_PRACTICE_MIN_COUNT;
|
|
2676
2766
|
const maxPractice = group.maxPracticeCardsPerRun ?? DEFAULT_MAX_PRACTICE_PER_RUN;
|
|
2677
2767
|
const practiceTags = [...cardsByTag.keys()].filter(
|
|
2678
2768
|
(tag) => patterns.some((p) => matchesTagPattern(tag, p)) && this.isUnlockedGatedSkill(tag, hierarchyConfigs, userTagElo, userGlobalElo) && (userTagElo[tag]?.count ?? 0) < practiceMinCount
|
|
2679
2769
|
);
|
|
2680
|
-
if (practiceTags.length === 0) return [];
|
|
2770
|
+
if (practiceTags.length === 0) return { cards: [], debts: [] };
|
|
2681
2771
|
const now = Date.now();
|
|
2682
2772
|
const DAY_MS = 24 * 60 * 60 * 1e3;
|
|
2683
2773
|
const tagMultiplier = /* @__PURE__ */ new Map();
|
|
@@ -2692,6 +2782,11 @@ var init_prescribed = __esm({
|
|
|
2692
2782
|
);
|
|
2693
2783
|
tagMultiplier.set(tag, mult);
|
|
2694
2784
|
}
|
|
2785
|
+
const debts = practiceTags.map((tag) => ({
|
|
2786
|
+
tag,
|
|
2787
|
+
multiplier: tagMultiplier.get(tag) ?? PRACTICE_BASE_MULT,
|
|
2788
|
+
firstOwedAt: nextPracticeDebt[tag]
|
|
2789
|
+
})).sort((a, b) => b.multiplier - a.multiplier || a.tag.localeCompare(b.tag));
|
|
2695
2790
|
const practiceCardIds = this.findDiscoveredSupportCards({
|
|
2696
2791
|
supportTags: practiceTags,
|
|
2697
2792
|
cardsByTag,
|
|
@@ -2700,7 +2795,7 @@ var init_prescribed = __esm({
|
|
|
2700
2795
|
excludedIds: emittedIds,
|
|
2701
2796
|
limit: maxPractice
|
|
2702
2797
|
});
|
|
2703
|
-
if (practiceCardIds.length === 0) return [];
|
|
2798
|
+
if (practiceCardIds.length === 0) return { cards: [], debts };
|
|
2704
2799
|
logger.info(
|
|
2705
2800
|
`[Prescribed] Group '${group.id}' practice: ${practiceTags.length} unlocked under-practiced skill(s), emitting ${practiceCardIds.length} drill card(s)`
|
|
2706
2801
|
);
|
|
@@ -2730,7 +2825,7 @@ var init_prescribed = __esm({
|
|
|
2730
2825
|
]
|
|
2731
2826
|
});
|
|
2732
2827
|
}
|
|
2733
|
-
return cards;
|
|
2828
|
+
return { cards, debts };
|
|
2734
2829
|
}
|
|
2735
2830
|
/**
|
|
2736
2831
|
* True for a skill that was *gated and is now reached*: it has at least one
|
|
@@ -5407,6 +5502,7 @@ var init_3 = __esm({
|
|
|
5407
5502
|
"./PipelineAssembler.ts": () => Promise.resolve().then(() => (init_PipelineAssembler(), PipelineAssembler_exports)),
|
|
5408
5503
|
"./PipelineDebugger.ts": () => Promise.resolve().then(() => (init_PipelineDebugger(), PipelineDebugger_exports)),
|
|
5409
5504
|
"./SrsDebugger.ts": () => Promise.resolve().then(() => (init_SrsDebugger(), SrsDebugger_exports)),
|
|
5505
|
+
"./StrategyPressureDebugger.ts": () => Promise.resolve().then(() => (init_StrategyPressureDebugger(), StrategyPressureDebugger_exports)),
|
|
5410
5506
|
"./defaults.ts": () => Promise.resolve().then(() => (init_defaults(), defaults_exports)),
|
|
5411
5507
|
"./diversityRerank.ts": () => Promise.resolve().then(() => (init_diversityRerank(), diversityRerank_exports)),
|
|
5412
5508
|
"./filters/WeightedFilter.ts": () => Promise.resolve().then(() => (init_WeightedFilter(), WeightedFilter_exports)),
|
|
@@ -5439,7 +5535,9 @@ __export(navigators_exports, {
|
|
|
5439
5535
|
NavigatorRole: () => NavigatorRole,
|
|
5440
5536
|
NavigatorRoles: () => NavigatorRoles,
|
|
5441
5537
|
Navigators: () => Navigators,
|
|
5538
|
+
captureStrategyPressure: () => captureStrategyPressure,
|
|
5442
5539
|
clearSrsBacklogDebug: () => clearSrsBacklogDebug,
|
|
5540
|
+
clearStrategyPressureDebug: () => clearStrategyPressureDebug,
|
|
5443
5541
|
diversityRerank: () => diversityRerank,
|
|
5444
5542
|
getActivePipeline: () => getActivePipeline,
|
|
5445
5543
|
getCardOrigin: () => getCardOrigin,
|
|
@@ -5447,6 +5545,7 @@ __export(navigators_exports, {
|
|
|
5447
5545
|
getRegisteredNavigatorNames: () => getRegisteredNavigatorNames,
|
|
5448
5546
|
getRegisteredNavigatorRole: () => getRegisteredNavigatorRole,
|
|
5449
5547
|
getSrsBacklogDebug: () => getSrsBacklogDebug,
|
|
5548
|
+
getStrategyPressureDebug: () => getStrategyPressureDebug,
|
|
5450
5549
|
hasRegisteredNavigator: () => hasRegisteredNavigator,
|
|
5451
5550
|
initializeNavigatorRegistry: () => initializeNavigatorRegistry,
|
|
5452
5551
|
isFilter: () => isFilter,
|
|
@@ -5529,6 +5628,7 @@ var init_navigators = __esm({
|
|
|
5529
5628
|
init_diversityRerank();
|
|
5530
5629
|
init_PipelineDebugger();
|
|
5531
5630
|
init_SrsDebugger();
|
|
5631
|
+
init_StrategyPressureDebugger();
|
|
5532
5632
|
init_logger();
|
|
5533
5633
|
init_();
|
|
5534
5634
|
init_2();
|
|
@@ -7238,6 +7338,8 @@ var init_CouchDBSyncStrategy = __esm({
|
|
|
7238
7338
|
CouchDBSyncStrategy = class {
|
|
7239
7339
|
syncHandle;
|
|
7240
7340
|
// Handle to cancel sync if needed
|
|
7341
|
+
/** In-flight one-shot hydration pull, so a timed-out pull can be abandoned. */
|
|
7342
|
+
hydrationHandle;
|
|
7241
7343
|
setupRemoteDB(username) {
|
|
7242
7344
|
if (username === GuestUsername || username.startsWith(GuestUsername)) {
|
|
7243
7345
|
return getLocalUserDB(username);
|
|
@@ -7252,8 +7354,26 @@ var init_CouchDBSyncStrategy = __esm({
|
|
|
7252
7354
|
return this.getUserDB(username);
|
|
7253
7355
|
}
|
|
7254
7356
|
}
|
|
7357
|
+
/**
|
|
7358
|
+
* One-shot remote → local pull. Resolves once the local mirror is caught up.
|
|
7359
|
+
*
|
|
7360
|
+
* Pull only: pushing here would upload whatever the local DB happens to hold
|
|
7361
|
+
* before we know what the remote already has, which is the conflict-leaf
|
|
7362
|
+
* problem hydration exists to avoid. Local changes go up when startSync()
|
|
7363
|
+
* takes over.
|
|
7364
|
+
*/
|
|
7365
|
+
async hydrate(localDB, remoteDB) {
|
|
7366
|
+
const replication = pouchdb_setup_default.replicate(remoteDB, localDB, {});
|
|
7367
|
+
this.hydrationHandle = replication;
|
|
7368
|
+
try {
|
|
7369
|
+
const info = await replication;
|
|
7370
|
+
return { docsWritten: info.docs_written };
|
|
7371
|
+
} finally {
|
|
7372
|
+
this.hydrationHandle = void 0;
|
|
7373
|
+
}
|
|
7374
|
+
}
|
|
7255
7375
|
startSync(localDB, remoteDB) {
|
|
7256
|
-
if (localDB !== remoteDB) {
|
|
7376
|
+
if (localDB.name !== remoteDB.name) {
|
|
7257
7377
|
this.syncHandle = pouchdb_setup_default.sync(localDB, remoteDB, {
|
|
7258
7378
|
live: true,
|
|
7259
7379
|
retry: true
|
|
@@ -7261,8 +7381,20 @@ var init_CouchDBSyncStrategy = __esm({
|
|
|
7261
7381
|
}
|
|
7262
7382
|
}
|
|
7263
7383
|
stopSync() {
|
|
7384
|
+
if (this.hydrationHandle) {
|
|
7385
|
+
try {
|
|
7386
|
+
this.hydrationHandle.cancel();
|
|
7387
|
+
} catch (e) {
|
|
7388
|
+
logger.warn(`Failed to cancel hydration pull (continuing anyway): ${e}`);
|
|
7389
|
+
}
|
|
7390
|
+
this.hydrationHandle = void 0;
|
|
7391
|
+
}
|
|
7264
7392
|
if (this.syncHandle) {
|
|
7265
|
-
|
|
7393
|
+
try {
|
|
7394
|
+
this.syncHandle.cancel();
|
|
7395
|
+
} catch (e) {
|
|
7396
|
+
logger.warn(`Failed to cancel user sync (continuing anyway): ${e}`);
|
|
7397
|
+
}
|
|
7266
7398
|
this.syncHandle = void 0;
|
|
7267
7399
|
}
|
|
7268
7400
|
}
|
|
@@ -7533,6 +7665,19 @@ var init_couch = __esm({
|
|
|
7533
7665
|
// src/impl/common/BaseUserDB.ts
|
|
7534
7666
|
import { Status as Status3 } from "@vue-skuilder/common";
|
|
7535
7667
|
import moment6 from "moment";
|
|
7668
|
+
function withTimeout(p, ms, label) {
|
|
7669
|
+
let timer;
|
|
7670
|
+
return Promise.race([
|
|
7671
|
+
p,
|
|
7672
|
+
new Promise((_resolve, reject) => {
|
|
7673
|
+
timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
|
|
7674
|
+
})
|
|
7675
|
+
]).finally(() => {
|
|
7676
|
+
if (timer !== void 0) {
|
|
7677
|
+
clearTimeout(timer);
|
|
7678
|
+
}
|
|
7679
|
+
});
|
|
7680
|
+
}
|
|
7536
7681
|
function accomodateGuest() {
|
|
7537
7682
|
logger.log("[funnel] accomodateGuest() called");
|
|
7538
7683
|
if (typeof localStorage === "undefined") {
|
|
@@ -7706,7 +7851,7 @@ async function dropUserFromClassroom(user, classID) {
|
|
|
7706
7851
|
async function getUserClassrooms(user) {
|
|
7707
7852
|
return getOrCreateClassroomRegistrationsDoc(user);
|
|
7708
7853
|
}
|
|
7709
|
-
var log4, BaseUser, userCoursesDoc, userClassroomsDoc;
|
|
7854
|
+
var log4, HYDRATION_TIMEOUT_MS, BaseUser, userCoursesDoc, userClassroomsDoc;
|
|
7710
7855
|
var init_BaseUserDB = __esm({
|
|
7711
7856
|
"src/impl/common/BaseUserDB.ts"() {
|
|
7712
7857
|
"use strict";
|
|
@@ -7721,6 +7866,7 @@ var init_BaseUserDB = __esm({
|
|
|
7721
7866
|
log4 = (s) => {
|
|
7722
7867
|
logger.info(s);
|
|
7723
7868
|
};
|
|
7869
|
+
HYDRATION_TIMEOUT_MS = 15e3;
|
|
7724
7870
|
BaseUser = class _BaseUser {
|
|
7725
7871
|
static _instance;
|
|
7726
7872
|
static _initialized = false;
|
|
@@ -7749,6 +7895,29 @@ var init_BaseUserDB = __esm({
|
|
|
7749
7895
|
writeDB;
|
|
7750
7896
|
// Database to use for write operations (local-first approach)
|
|
7751
7897
|
updateQueue;
|
|
7898
|
+
_hydration = { state: "not-required" };
|
|
7899
|
+
/**
|
|
7900
|
+
* How far the local mirror can be trusted. See {@link UserHydrationStatus}.
|
|
7901
|
+
*
|
|
7902
|
+
* Consumers that would otherwise read a 404 as "new user" — onboarding
|
|
7903
|
+
* gates, first-run experiences, progress dashboards — should check for
|
|
7904
|
+
* `failed` and present a retry affordance rather than an empty state.
|
|
7905
|
+
*/
|
|
7906
|
+
hydrationStatus() {
|
|
7907
|
+
return { ...this._hydration };
|
|
7908
|
+
}
|
|
7909
|
+
/**
|
|
7910
|
+
* Whether a missing document may be treated as genuinely absent, allowing
|
|
7911
|
+
* callers to create it with defaults.
|
|
7912
|
+
*
|
|
7913
|
+
* False only when hydration failed or is still running: a 404 then may just
|
|
7914
|
+
* mean "not pulled down yet", and materializing a default would both lie to
|
|
7915
|
+
* the user and, once live sync starts, push a rev-1 document that loses to
|
|
7916
|
+
* the real one — silently discarding it.
|
|
7917
|
+
*/
|
|
7918
|
+
canMaterializeDefaults() {
|
|
7919
|
+
return this._hydration.state !== "failed" && this._hydration.state !== "hydrating";
|
|
7920
|
+
}
|
|
7752
7921
|
async createAccount(username, password) {
|
|
7753
7922
|
if (!this.syncStrategy.canCreateAccount()) {
|
|
7754
7923
|
throw new Error("Account creation not supported by current sync strategy");
|
|
@@ -7866,6 +8035,11 @@ Currently logged-in as ${this._username}.`
|
|
|
7866
8035
|
} catch (e) {
|
|
7867
8036
|
const err = e;
|
|
7868
8037
|
if (err.status === 404) {
|
|
8038
|
+
if (!this.canMaterializeDefaults()) {
|
|
8039
|
+
throw new Error(
|
|
8040
|
+
`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.`
|
|
8041
|
+
);
|
|
8042
|
+
}
|
|
7869
8043
|
await this.localDB.put({
|
|
7870
8044
|
_id: _BaseUser.DOC_IDS.COURSE_REGISTRATIONS,
|
|
7871
8045
|
courses: [],
|
|
@@ -8108,6 +8282,11 @@ Currently logged-in as ${this._username}.`
|
|
|
8108
8282
|
} catch (e) {
|
|
8109
8283
|
const err = e;
|
|
8110
8284
|
if (err.name && err.name === "not_found") {
|
|
8285
|
+
if (!this.canMaterializeDefaults()) {
|
|
8286
|
+
throw new Error(
|
|
8287
|
+
`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.`
|
|
8288
|
+
);
|
|
8289
|
+
}
|
|
8111
8290
|
await this.localDB.put(defaultConfig);
|
|
8112
8291
|
return this.getConfig();
|
|
8113
8292
|
} else {
|
|
@@ -8181,7 +8360,9 @@ Currently logged-in as ${this._username}.`
|
|
|
8181
8360
|
_BaseUser._initialized = true;
|
|
8182
8361
|
return;
|
|
8183
8362
|
}
|
|
8363
|
+
this.syncStrategy.stopSync?.();
|
|
8184
8364
|
this.setDBandQ();
|
|
8365
|
+
await this.hydrateLocalMirror();
|
|
8185
8366
|
this.syncStrategy.startSync(this.localDB, this.remoteDB);
|
|
8186
8367
|
this.applyDesignDocs().catch((error) => {
|
|
8187
8368
|
log4(`Error in applyDesignDocs background task: ${error}`);
|
|
@@ -8197,6 +8378,85 @@ Currently logged-in as ${this._username}.`
|
|
|
8197
8378
|
});
|
|
8198
8379
|
_BaseUser._initialized = true;
|
|
8199
8380
|
}
|
|
8381
|
+
/**
|
|
8382
|
+
* Pull this account's documents into the local mirror, if that hasn't
|
|
8383
|
+
* happened on this device before.
|
|
8384
|
+
*
|
|
8385
|
+
* Runs between setDBandQ() and startSync() — after the handles exist, before
|
|
8386
|
+
* anything can observe an empty local DB or replicate one upward.
|
|
8387
|
+
*
|
|
8388
|
+
* Never throws: a failure is recorded as `failed` state and reported through
|
|
8389
|
+
* hydrationStatus(), because throwing here would abort init() and leave
|
|
8390
|
+
* BaseUser._initialized false forever (BaseUser.instance() polls it with no
|
|
8391
|
+
* ceiling). Callers decide what a failure means for them.
|
|
8392
|
+
*/
|
|
8393
|
+
async hydrateLocalMirror() {
|
|
8394
|
+
if (this.localDB.name === this.remoteDB.name || !this.syncStrategy.hydrate) {
|
|
8395
|
+
this._hydration = { state: "not-required" };
|
|
8396
|
+
return;
|
|
8397
|
+
}
|
|
8398
|
+
if (await this.hasHydrationMarker()) {
|
|
8399
|
+
this._hydration = { state: "stale" };
|
|
8400
|
+
return;
|
|
8401
|
+
}
|
|
8402
|
+
this._hydration = { state: "hydrating" };
|
|
8403
|
+
const start = Date.now();
|
|
8404
|
+
try {
|
|
8405
|
+
const { docsWritten } = await withTimeout(
|
|
8406
|
+
this.syncStrategy.hydrate(this.localDB, this.remoteDB),
|
|
8407
|
+
HYDRATION_TIMEOUT_MS,
|
|
8408
|
+
`Hydration of local mirror for ${this._username}`
|
|
8409
|
+
);
|
|
8410
|
+
await this.writeHydrationMarker();
|
|
8411
|
+
this._hydration = {
|
|
8412
|
+
state: "hydrated",
|
|
8413
|
+
docsWritten,
|
|
8414
|
+
durationMs: Date.now() - start
|
|
8415
|
+
};
|
|
8416
|
+
log4(
|
|
8417
|
+
`Hydrated local mirror for ${this._username}: ${docsWritten} docs in ${this._hydration.durationMs}ms`
|
|
8418
|
+
);
|
|
8419
|
+
} catch (e) {
|
|
8420
|
+
this.syncStrategy.stopSync?.();
|
|
8421
|
+
this._hydration = {
|
|
8422
|
+
state: "failed",
|
|
8423
|
+
durationMs: Date.now() - start,
|
|
8424
|
+
error: e instanceof Error ? e.message : String(e)
|
|
8425
|
+
};
|
|
8426
|
+
logger.error(`Failed to hydrate local mirror for ${this._username}:`, e);
|
|
8427
|
+
}
|
|
8428
|
+
}
|
|
8429
|
+
/**
|
|
8430
|
+
* Has a full pull completed on this device for the CURRENT account?
|
|
8431
|
+
*
|
|
8432
|
+
* The marker is a `_local/` document, which never replicates — so its
|
|
8433
|
+
* presence describes this device's mirror and cannot arrive from elsewhere.
|
|
8434
|
+
*/
|
|
8435
|
+
async hasHydrationMarker() {
|
|
8436
|
+
try {
|
|
8437
|
+
const marker = await this.localDB.get(HYDRATION_MARKER_ID);
|
|
8438
|
+
return marker.username === this._username;
|
|
8439
|
+
} catch {
|
|
8440
|
+
return false;
|
|
8441
|
+
}
|
|
8442
|
+
}
|
|
8443
|
+
async writeHydrationMarker() {
|
|
8444
|
+
try {
|
|
8445
|
+
let existingRev;
|
|
8446
|
+
try {
|
|
8447
|
+
existingRev = (await this.localDB.get(HYDRATION_MARKER_ID))._rev;
|
|
8448
|
+
} catch {
|
|
8449
|
+
}
|
|
8450
|
+
await this.localDB.put({
|
|
8451
|
+
_id: HYDRATION_MARKER_ID,
|
|
8452
|
+
_rev: existingRev,
|
|
8453
|
+
username: this._username,
|
|
8454
|
+
hydratedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
8455
|
+
});
|
|
8456
|
+
} catch (e) {
|
|
8457
|
+
logger.warn(`Could not write hydration marker for ${this._username}: ${e}`);
|
|
8458
|
+
}
|
|
8459
|
+
}
|
|
8200
8460
|
static designDocs = [
|
|
8201
8461
|
{
|
|
8202
8462
|
_id: "_design/reviewCards",
|
|
@@ -8533,6 +8793,11 @@ Currently logged-in as ${this._username}.`
|
|
|
8533
8793
|
} catch (e) {
|
|
8534
8794
|
const err = e;
|
|
8535
8795
|
if (err.status === 404) {
|
|
8796
|
+
if (!this.canMaterializeDefaults()) {
|
|
8797
|
+
throw new Error(
|
|
8798
|
+
`Cannot read strategy state '${strategyKey}' for ${this._username}: the local mirror is unavailable (hydration state '${this._hydration.state}').`
|
|
8799
|
+
);
|
|
8800
|
+
}
|
|
8536
8801
|
return null;
|
|
8537
8802
|
}
|
|
8538
8803
|
throw e;
|
|
@@ -10052,6 +10317,15 @@ var init_userOutcome = __esm({
|
|
|
10052
10317
|
}
|
|
10053
10318
|
});
|
|
10054
10319
|
|
|
10320
|
+
// src/core/types/hydration.ts
|
|
10321
|
+
var HYDRATION_MARKER_ID;
|
|
10322
|
+
var init_hydration = __esm({
|
|
10323
|
+
"src/core/types/hydration.ts"() {
|
|
10324
|
+
"use strict";
|
|
10325
|
+
HYDRATION_MARKER_ID = "_local/hydration";
|
|
10326
|
+
}
|
|
10327
|
+
});
|
|
10328
|
+
|
|
10055
10329
|
// src/core/bulkImport/cardProcessor.ts
|
|
10056
10330
|
import { Status as Status5 } from "@vue-skuilder/common";
|
|
10057
10331
|
async function importParsedCards(parsedCards, courseDB, config) {
|
|
@@ -10531,6 +10805,7 @@ var init_core = __esm({
|
|
|
10531
10805
|
init_user();
|
|
10532
10806
|
init_strategyState();
|
|
10533
10807
|
init_userOutcome();
|
|
10808
|
+
init_hydration();
|
|
10534
10809
|
init_Loggable();
|
|
10535
10810
|
init_util();
|
|
10536
10811
|
init_navigators();
|
|
@@ -13471,6 +13746,7 @@ mountMixerDebugger();
|
|
|
13471
13746
|
init_logger();
|
|
13472
13747
|
init_PipelineDebugger();
|
|
13473
13748
|
init_SrsDebugger();
|
|
13749
|
+
init_StrategyPressureDebugger();
|
|
13474
13750
|
|
|
13475
13751
|
// src/study/SessionOverlay.ts
|
|
13476
13752
|
init_logger();
|
|
@@ -13561,7 +13837,7 @@ function render() {
|
|
|
13561
13837
|
attachHandlers();
|
|
13562
13838
|
return;
|
|
13563
13839
|
}
|
|
13564
|
-
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);
|
|
13840
|
+
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);
|
|
13565
13841
|
attachHandlers();
|
|
13566
13842
|
}
|
|
13567
13843
|
function attachHandlers() {
|
|
@@ -13665,6 +13941,46 @@ function backlogHtml(backlog) {
|
|
|
13665
13941
|
}).join("");
|
|
13666
13942
|
return `<div style="margin-bottom:6px"><div style="color:#93c5fd">review backpressure</div>${rows}</div>`;
|
|
13667
13943
|
}
|
|
13944
|
+
function pressureColor(multiplier, max) {
|
|
13945
|
+
if (multiplier <= 1.001) return "#86efac";
|
|
13946
|
+
if (max !== void 0 && max > 1) {
|
|
13947
|
+
return (multiplier - 1) / (max - 1) >= 0.75 ? "#fca5a5" : "#fcd34d";
|
|
13948
|
+
}
|
|
13949
|
+
return multiplier >= 3 ? "#fca5a5" : "#fcd34d";
|
|
13950
|
+
}
|
|
13951
|
+
function toggleKey(raw) {
|
|
13952
|
+
return raw.replace(/[^\w:.|-]/g, "_");
|
|
13953
|
+
}
|
|
13954
|
+
function gaugeHtml(sectionKey, g) {
|
|
13955
|
+
const cap = g.max !== void 0 ? `<span style="opacity:.5">/${g.max}</span>` : "";
|
|
13956
|
+
const detail = g.detail ? ` <span style="opacity:.6">${esc(g.detail)}</span>` : "";
|
|
13957
|
+
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}`;
|
|
13958
|
+
const items = g.items ?? [];
|
|
13959
|
+
if (items.length > 0) {
|
|
13960
|
+
const key = toggleKey(`sp|${sectionKey}|${g.id}`);
|
|
13961
|
+
const isOpen = !!expanded[key];
|
|
13962
|
+
const shown = isOpen ? items : items.slice(0, INLINE_THRESHOLD);
|
|
13963
|
+
html += `<div style="margin-left:6px">` + shown.map(
|
|
13964
|
+
(it) => `<div style="white-space:nowrap;opacity:.75">${esc(it.label)}${it.value ? ` <span style="opacity:.6">${esc(it.value)}</span>` : ""}</div>`
|
|
13965
|
+
).join("");
|
|
13966
|
+
if (items.length > INLINE_THRESHOLD) {
|
|
13967
|
+
const footer = isOpen ? "\u25BE show less" : `\u2026 +${items.length - shown.length} more`;
|
|
13968
|
+
html += `<div data-q="${key}" style="cursor:pointer;opacity:.6">${footer}</div>`;
|
|
13969
|
+
}
|
|
13970
|
+
html += `</div>`;
|
|
13971
|
+
}
|
|
13972
|
+
return html + `</div>`;
|
|
13973
|
+
}
|
|
13974
|
+
function strategyPressureHtml(list) {
|
|
13975
|
+
if (!list.length) return "";
|
|
13976
|
+
const sections = list.map((s) => {
|
|
13977
|
+
const sectionKey = `${s.source}:${s.courseId.slice(0, 8)}`;
|
|
13978
|
+
const top = s.topScore !== null && s.topScore !== void 0 ? s.topScore.toFixed(2) : "\u2014";
|
|
13979
|
+
const hints = s.hintsLabel ? `<div style="margin-left:6px;opacity:.6">hints: ${esc(s.hintsLabel)}</div>` : "";
|
|
13980
|
+
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>`;
|
|
13981
|
+
}).join("");
|
|
13982
|
+
return `<div style="margin-bottom:6px"><div style="color:#fdba74">strategy backpressure</div>${sections}</div>`;
|
|
13983
|
+
}
|
|
13668
13984
|
function fmtScore(score) {
|
|
13669
13985
|
if (score === void 0) return "";
|
|
13670
13986
|
if (!Number.isFinite(score)) return "REQ";
|
|
@@ -13765,6 +14081,24 @@ function snapshotToText(s) {
|
|
|
13765
14081
|
);
|
|
13766
14082
|
}
|
|
13767
14083
|
}
|
|
14084
|
+
if (s.strategyPressure.length) {
|
|
14085
|
+
lines.push("");
|
|
14086
|
+
lines.push("strategy backpressure:");
|
|
14087
|
+
for (const sp of s.strategyPressure) {
|
|
14088
|
+
const top = sp.topScore !== null && sp.topScore !== void 0 ? sp.topScore.toFixed(2) : "\u2014";
|
|
14089
|
+
lines.push(` ${sp.source} (${sp.courseId.slice(0, 8)}): top score ${top}`);
|
|
14090
|
+
for (const g of sp.gauges) {
|
|
14091
|
+
const cap = g.max !== void 0 ? `/${g.max}` : "";
|
|
14092
|
+
lines.push(
|
|
14093
|
+
` ${g.label} \xD7${g.multiplier.toFixed(2)}${cap}${g.detail ? ` \u2014 ${g.detail}` : ""}`
|
|
14094
|
+
);
|
|
14095
|
+
for (const it of g.items ?? []) {
|
|
14096
|
+
lines.push(` ${it.label}${it.value ? ` (${it.value})` : ""}`);
|
|
14097
|
+
}
|
|
14098
|
+
}
|
|
14099
|
+
if (sp.hintsLabel) lines.push(` hints: ${sp.hintsLabel}`);
|
|
14100
|
+
}
|
|
14101
|
+
}
|
|
13768
14102
|
const queueText = (label, q) => {
|
|
13769
14103
|
lines.push("");
|
|
13770
14104
|
lines.push(`${label}: ${q.length} (drawn ${q.dequeueCount})`);
|
|
@@ -13802,6 +14136,7 @@ var MAX_HISTORY = 5;
|
|
|
13802
14136
|
function clearStaleSessionDebugState() {
|
|
13803
14137
|
clearRunHistory();
|
|
13804
14138
|
clearSrsBacklogDebug();
|
|
14139
|
+
clearStrategyPressureDebug();
|
|
13805
14140
|
}
|
|
13806
14141
|
function startSessionTracking(supplyQLength, failedQLength) {
|
|
13807
14142
|
const sessionId = `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -14490,6 +14825,7 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14490
14825
|
supplyQ: describe(this.supplyQ),
|
|
14491
14826
|
failedQ: describe(this.failedQ),
|
|
14492
14827
|
reviewBacklog: getSrsBacklogDebug(),
|
|
14828
|
+
strategyPressure: getStrategyPressureDebug(),
|
|
14493
14829
|
drawnCards
|
|
14494
14830
|
};
|
|
14495
14831
|
}
|
|
@@ -15175,6 +15511,7 @@ export {
|
|
|
15175
15511
|
ENV,
|
|
15176
15512
|
FileSystemError,
|
|
15177
15513
|
GuestUsername,
|
|
15514
|
+
HYDRATION_MARKER_ID,
|
|
15178
15515
|
Loggable,
|
|
15179
15516
|
NOT_SET,
|
|
15180
15517
|
NavigatorRole,
|
|
@@ -15190,8 +15527,10 @@ export {
|
|
|
15190
15527
|
areQuestionRecords,
|
|
15191
15528
|
buildStrategyStateId,
|
|
15192
15529
|
captureMixerRun,
|
|
15530
|
+
captureStrategyPressure,
|
|
15193
15531
|
clearSrsBacklogDebug,
|
|
15194
15532
|
clearStaleSessionDebugState,
|
|
15533
|
+
clearStrategyPressureDebug,
|
|
15195
15534
|
computeDeviation,
|
|
15196
15535
|
computeEffectiveWeight,
|
|
15197
15536
|
computeOutcomeSignal,
|
|
@@ -15214,6 +15553,7 @@ export {
|
|
|
15214
15553
|
getRegisteredNavigatorNames,
|
|
15215
15554
|
getRegisteredNavigatorRole,
|
|
15216
15555
|
getSrsBacklogDebug,
|
|
15556
|
+
getStrategyPressureDebug,
|
|
15217
15557
|
getStudySource,
|
|
15218
15558
|
hasRegisteredNavigator,
|
|
15219
15559
|
importParsedCards,
|