@vue-skuilder/db 0.1.11 → 0.1.12
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/core/index.d.mts +7 -6
- package/dist/core/index.d.ts +7 -6
- package/dist/core/index.js +146 -37
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +146 -37
- package/dist/core/index.mjs.map +1 -1
- package/dist/{dataLayerProvider-VieuAAkV.d.mts → dataLayerProvider-BiP3kWix.d.mts} +1 -1
- package/dist/{dataLayerProvider-juuqUHOP.d.ts → dataLayerProvider-DSdeyRT3.d.ts} +1 -1
- package/dist/impl/couch/index.d.mts +3 -3
- package/dist/impl/couch/index.d.ts +3 -3
- package/dist/impl/couch/index.js +146 -37
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +146 -37
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.d.mts +14 -6
- package/dist/impl/static/index.d.ts +14 -6
- package/dist/impl/static/index.js +147 -39
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +147 -39
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/{index-DZyxHCcf.d.mts → index-Bmll7Xse.d.mts} +1 -1
- package/dist/{index-CWY6yhkV.d.ts → index-CD8BZz2k.d.ts} +1 -1
- package/dist/index.d.mts +119 -24
- package/dist/index.d.ts +119 -24
- package/dist/index.js +785 -261
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +789 -265
- package/dist/index.mjs.map +1 -1
- package/dist/{types-DtoI27Xh.d.ts → types-CewsN87z.d.ts} +1 -1
- package/dist/{types-Che4wTwA.d.mts → types-Dbp5DaRR.d.mts} +1 -1
- package/dist/{types-legacy-B8ahaCbj.d.mts → types-legacy-6ettoclI.d.mts} +13 -2
- package/dist/{types-legacy-B8ahaCbj.d.ts → types-legacy-6ettoclI.d.ts} +13 -2
- package/dist/{userDB-DJ8HMw83.d.mts → userDB-C4yyAnpp.d.mts} +3 -3
- package/dist/{userDB-B7zTQ123.d.ts → userDB-CD6s6ZCp.d.ts} +3 -3
- package/dist/util/packer/index.d.mts +3 -3
- package/dist/util/packer/index.d.ts +3 -3
- package/package.json +3 -3
- package/src/core/navigators/hardcodedOrder.ts +64 -0
- package/src/core/navigators/index.ts +1 -0
- package/src/core/types/contentNavigationStrategy.ts +2 -1
- package/src/core/types/types-legacy.ts +2 -2
- package/src/impl/common/BaseUserDB.ts +15 -11
- package/src/impl/couch/courseDB.ts +74 -27
- package/src/impl/couch/updateQueue.ts +8 -3
- package/src/impl/static/StaticDataLayerProvider.ts +57 -17
- package/src/impl/static/courseDB.ts +17 -12
- package/src/impl/static/coursesDB.ts +10 -6
- package/src/study/ItemQueue.ts +58 -0
- package/src/study/SessionController.ts +132 -178
- package/src/study/services/CardHydrationService.ts +153 -0
- package/src/study/services/EloService.ts +85 -0
- package/src/study/services/ResponseProcessor.ts +224 -0
- package/src/study/services/SrsService.ts +44 -0
package/dist/index.mjs
CHANGED
|
@@ -445,7 +445,9 @@ var init_updateQueue = __esm({
|
|
|
445
445
|
async applyUpdates(id) {
|
|
446
446
|
logger.debug(`Applying updates on doc: ${id}`);
|
|
447
447
|
if (this.inprogressUpdates[id]) {
|
|
448
|
-
|
|
448
|
+
while (this.inprogressUpdates[id]) {
|
|
449
|
+
await new Promise((resolve) => setTimeout(resolve, Math.random() * 50));
|
|
450
|
+
}
|
|
449
451
|
return this.applyUpdates(id);
|
|
450
452
|
} else {
|
|
451
453
|
if (this.pendingUpdates[id] && this.pendingUpdates[id].length > 0) {
|
|
@@ -481,6 +483,9 @@ var init_updateQueue = __esm({
|
|
|
481
483
|
if (e.name === "conflict" && i < MAX_RETRIES - 1) {
|
|
482
484
|
logger.warn(`Conflict on update for doc ${id}, retry #${i + 1}`);
|
|
483
485
|
await new Promise((res) => setTimeout(res, 50 * Math.random()));
|
|
486
|
+
} else if (e.name === "not_found" && i === 0) {
|
|
487
|
+
logger.warn(`Update failed for ${id} - does not exist. Throwing to caller.`);
|
|
488
|
+
throw e;
|
|
484
489
|
} else {
|
|
485
490
|
delete this.inprogressUpdates[id];
|
|
486
491
|
if (this.pendingUpdates[id]) {
|
|
@@ -959,12 +964,74 @@ var init_elo = __esm({
|
|
|
959
964
|
}
|
|
960
965
|
});
|
|
961
966
|
|
|
967
|
+
// src/core/navigators/hardcodedOrder.ts
|
|
968
|
+
var hardcodedOrder_exports = {};
|
|
969
|
+
__export(hardcodedOrder_exports, {
|
|
970
|
+
default: () => HardcodedOrderNavigator
|
|
971
|
+
});
|
|
972
|
+
var HardcodedOrderNavigator;
|
|
973
|
+
var init_hardcodedOrder = __esm({
|
|
974
|
+
"src/core/navigators/hardcodedOrder.ts"() {
|
|
975
|
+
"use strict";
|
|
976
|
+
init_navigators();
|
|
977
|
+
init_logger();
|
|
978
|
+
HardcodedOrderNavigator = class extends ContentNavigator {
|
|
979
|
+
orderedCardIds = [];
|
|
980
|
+
user;
|
|
981
|
+
course;
|
|
982
|
+
constructor(user, course, strategyData) {
|
|
983
|
+
super();
|
|
984
|
+
this.user = user;
|
|
985
|
+
this.course = course;
|
|
986
|
+
if (strategyData.serializedData) {
|
|
987
|
+
try {
|
|
988
|
+
this.orderedCardIds = JSON.parse(strategyData.serializedData);
|
|
989
|
+
} catch (e) {
|
|
990
|
+
logger.error("Failed to parse serializedData for HardcodedOrderNavigator", e);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
async getPendingReviews() {
|
|
995
|
+
const reviews = await this.user.getPendingReviews(this.course.getCourseID());
|
|
996
|
+
return reviews.map((r) => {
|
|
997
|
+
return {
|
|
998
|
+
...r,
|
|
999
|
+
contentSourceType: "course",
|
|
1000
|
+
contentSourceID: this.course.getCourseID(),
|
|
1001
|
+
cardID: r.cardId,
|
|
1002
|
+
courseID: r.courseId,
|
|
1003
|
+
reviewID: r._id,
|
|
1004
|
+
status: "review"
|
|
1005
|
+
};
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
async getNewCards(limit = 99) {
|
|
1009
|
+
const activeCardIds = (await this.user.getActiveCards()).map((c) => c.cardID);
|
|
1010
|
+
const newCardIds = this.orderedCardIds.filter(
|
|
1011
|
+
(cardId) => !activeCardIds.includes(cardId)
|
|
1012
|
+
);
|
|
1013
|
+
const cardsToReturn = newCardIds.slice(0, limit);
|
|
1014
|
+
return cardsToReturn.map((cardId) => {
|
|
1015
|
+
return {
|
|
1016
|
+
cardID: cardId,
|
|
1017
|
+
courseID: this.course.getCourseID(),
|
|
1018
|
+
contentSourceType: "course",
|
|
1019
|
+
contentSourceID: this.course.getCourseID(),
|
|
1020
|
+
status: "new"
|
|
1021
|
+
};
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
});
|
|
1027
|
+
|
|
962
1028
|
// import("./**/*") in src/core/navigators/index.ts
|
|
963
1029
|
var globImport;
|
|
964
1030
|
var init_ = __esm({
|
|
965
1031
|
'import("./**/*") in src/core/navigators/index.ts'() {
|
|
966
1032
|
globImport = __glob({
|
|
967
1033
|
"./elo.ts": () => Promise.resolve().then(() => (init_elo(), elo_exports)),
|
|
1034
|
+
"./hardcodedOrder.ts": () => Promise.resolve().then(() => (init_hardcodedOrder(), hardcodedOrder_exports)),
|
|
968
1035
|
"./index.ts": () => Promise.resolve().then(() => (init_navigators(), navigators_exports))
|
|
969
1036
|
});
|
|
970
1037
|
}
|
|
@@ -984,6 +1051,7 @@ var init_navigators = __esm({
|
|
|
984
1051
|
init_();
|
|
985
1052
|
Navigators = /* @__PURE__ */ ((Navigators2) => {
|
|
986
1053
|
Navigators2["ELO"] = "elo";
|
|
1054
|
+
Navigators2["HARDCODED"] = "hardcodedOrder";
|
|
987
1055
|
return Navigators2;
|
|
988
1056
|
})(Navigators || {});
|
|
989
1057
|
ContentNavigator = class {
|
|
@@ -1258,6 +1326,23 @@ var init_courseDB = __esm({
|
|
|
1258
1326
|
if (!doc.docType || !(doc.docType === "CARD" /* CARD */)) {
|
|
1259
1327
|
throw new Error(`failed to remove ${id} from course ${this.id}. id does not point to a card`);
|
|
1260
1328
|
}
|
|
1329
|
+
try {
|
|
1330
|
+
const appliedTags = await this.getAppliedTags(id);
|
|
1331
|
+
const results = await Promise.allSettled(
|
|
1332
|
+
appliedTags.rows.map(async (tagRow) => {
|
|
1333
|
+
const tagId = tagRow.id;
|
|
1334
|
+
await this.removeTagFromCard(id, tagId);
|
|
1335
|
+
})
|
|
1336
|
+
);
|
|
1337
|
+
results.forEach((result, index) => {
|
|
1338
|
+
if (result.status === "rejected") {
|
|
1339
|
+
const tagId = appliedTags.rows[index].id;
|
|
1340
|
+
logger.error(`Failed to remove card ${id} from tag ${tagId}: ${result.reason}`);
|
|
1341
|
+
}
|
|
1342
|
+
});
|
|
1343
|
+
} catch (error) {
|
|
1344
|
+
logger.error(`Error removing card ${id} from tags: ${error}`);
|
|
1345
|
+
}
|
|
1261
1346
|
return this.db.remove(doc);
|
|
1262
1347
|
}
|
|
1263
1348
|
async getCardDisplayableDataIDs(id) {
|
|
@@ -1441,23 +1526,9 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
1441
1526
|
////////////////////////////////////
|
|
1442
1527
|
getNavigationStrategy(id) {
|
|
1443
1528
|
logger.debug(`[courseDB] Getting navigation strategy: ${id}`);
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
name: "ELO",
|
|
1448
|
-
description: "ELO-based navigation strategy for ordering content by difficulty",
|
|
1449
|
-
implementingClass: "elo" /* ELO */,
|
|
1450
|
-
course: this.id,
|
|
1451
|
-
serializedData: ""
|
|
1452
|
-
// serde is a noop for ELO navigator.
|
|
1453
|
-
};
|
|
1454
|
-
return Promise.resolve(strategy);
|
|
1455
|
-
}
|
|
1456
|
-
getAllNavigationStrategies() {
|
|
1457
|
-
logger.debug("[courseDB] Returning hard-coded navigation strategies");
|
|
1458
|
-
const strategies = [
|
|
1459
|
-
{
|
|
1460
|
-
id: "ELO",
|
|
1529
|
+
if (id == "") {
|
|
1530
|
+
const strategy = {
|
|
1531
|
+
_id: "NAVIGATION_STRATEGY-ELO",
|
|
1461
1532
|
docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
|
|
1462
1533
|
name: "ELO",
|
|
1463
1534
|
description: "ELO-based navigation strategy for ordering content by difficulty",
|
|
@@ -1465,14 +1536,25 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
1465
1536
|
course: this.id,
|
|
1466
1537
|
serializedData: ""
|
|
1467
1538
|
// serde is a noop for ELO navigator.
|
|
1468
|
-
}
|
|
1469
|
-
|
|
1470
|
-
|
|
1539
|
+
};
|
|
1540
|
+
return Promise.resolve(strategy);
|
|
1541
|
+
} else {
|
|
1542
|
+
return this.db.get(id);
|
|
1543
|
+
}
|
|
1471
1544
|
}
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1545
|
+
async getAllNavigationStrategies() {
|
|
1546
|
+
const prefix = DocTypePrefixes["NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */];
|
|
1547
|
+
const result = await this.db.allDocs({
|
|
1548
|
+
startkey: prefix,
|
|
1549
|
+
endkey: `${prefix}\uFFF0`,
|
|
1550
|
+
include_docs: true
|
|
1551
|
+
});
|
|
1552
|
+
return result.rows.map((row) => row.doc);
|
|
1553
|
+
}
|
|
1554
|
+
async addNavigationStrategy(data) {
|
|
1555
|
+
logger.debug(`[courseDB] Adding navigation strategy: ${data._id}`);
|
|
1556
|
+
return this.db.put(data).then(() => {
|
|
1557
|
+
});
|
|
1476
1558
|
}
|
|
1477
1559
|
updateNavigationStrategy(id, data) {
|
|
1478
1560
|
logger.debug(`[courseDB] Updating navigation strategy: ${id}`);
|
|
@@ -1480,9 +1562,32 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
1480
1562
|
return Promise.resolve();
|
|
1481
1563
|
}
|
|
1482
1564
|
async surfaceNavigationStrategy() {
|
|
1565
|
+
try {
|
|
1566
|
+
const config = await this.getCourseConfig();
|
|
1567
|
+
if (config.defaultNavigationStrategyId) {
|
|
1568
|
+
try {
|
|
1569
|
+
const strategy = await this.getNavigationStrategy(config.defaultNavigationStrategyId);
|
|
1570
|
+
if (strategy) {
|
|
1571
|
+
logger.debug(`Surfacing strategy ${strategy.name} from course config`);
|
|
1572
|
+
return strategy;
|
|
1573
|
+
}
|
|
1574
|
+
} catch (e) {
|
|
1575
|
+
logger.warn(
|
|
1576
|
+
// @ts-expect-error tmp: defaultNavigationStrategyId property does not yet exist
|
|
1577
|
+
`Failed to load strategy '${config.defaultNavigationStrategyId}' specified in course config. Falling back to ELO.`,
|
|
1578
|
+
e
|
|
1579
|
+
);
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
} catch (e) {
|
|
1583
|
+
logger.warn(
|
|
1584
|
+
"Could not retrieve course config to determine navigation strategy. Falling back to ELO.",
|
|
1585
|
+
e
|
|
1586
|
+
);
|
|
1587
|
+
}
|
|
1483
1588
|
logger.warn(`Returning hard-coded default ELO navigator`);
|
|
1484
1589
|
const ret = {
|
|
1485
|
-
|
|
1590
|
+
_id: "NAVIGATION_STRATEGY-ELO",
|
|
1486
1591
|
docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
|
|
1487
1592
|
name: "ELO",
|
|
1488
1593
|
description: "ELO-based navigation strategy",
|
|
@@ -2945,17 +3050,21 @@ Currently logged-in as ${this._username}.`
|
|
|
2945
3050
|
} catch (e) {
|
|
2946
3051
|
const reason = e;
|
|
2947
3052
|
if (reason.status === 404) {
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
3053
|
+
try {
|
|
3054
|
+
const initCardHistory = {
|
|
3055
|
+
_id: cardHistoryID,
|
|
3056
|
+
cardID: record.cardID,
|
|
3057
|
+
courseID: record.courseID,
|
|
3058
|
+
records: [record],
|
|
3059
|
+
lapses: 0,
|
|
3060
|
+
streak: 0,
|
|
3061
|
+
bestInterval: 0
|
|
3062
|
+
};
|
|
3063
|
+
const putResult = await this.writeDB.put(initCardHistory);
|
|
3064
|
+
return { ...initCardHistory, _rev: putResult.rev };
|
|
3065
|
+
} catch (creationError) {
|
|
3066
|
+
throw new Error(`Failed to create CardHistory for ${cardHistoryID}. Reason: ${creationError}`);
|
|
3067
|
+
}
|
|
2959
3068
|
} else {
|
|
2960
3069
|
throw new Error(`putCardRecord failed because of:
|
|
2961
3070
|
name:${reason.name}
|
|
@@ -3735,16 +3844,20 @@ var init_courseDB2 = __esm({
|
|
|
3735
3844
|
async updateCardElo(cardId, _elo) {
|
|
3736
3845
|
return { ok: true, id: cardId, rev: "1-static" };
|
|
3737
3846
|
}
|
|
3738
|
-
async getNewCards(limit) {
|
|
3739
|
-
const
|
|
3740
|
-
return
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3847
|
+
async getNewCards(limit = 99) {
|
|
3848
|
+
const activeCards = await this.userDB.getActiveCards();
|
|
3849
|
+
return (await this.getCardsCenteredAtELO({ limit, elo: "user" }, (c) => {
|
|
3850
|
+
if (activeCards.some((ac) => c.cardID === ac.cardID)) {
|
|
3851
|
+
return false;
|
|
3852
|
+
} else {
|
|
3853
|
+
return true;
|
|
3854
|
+
}
|
|
3855
|
+
})).map((c) => {
|
|
3856
|
+
return {
|
|
3857
|
+
...c,
|
|
3858
|
+
status: "new"
|
|
3859
|
+
};
|
|
3860
|
+
});
|
|
3748
3861
|
}
|
|
3749
3862
|
async getCardsCenteredAtELO(options, filter) {
|
|
3750
3863
|
let targetElo = typeof options.elo === "number" ? options.elo : 1e3;
|
|
@@ -3925,7 +4038,7 @@ var init_courseDB2 = __esm({
|
|
|
3925
4038
|
// Navigation Strategy Manager implementation
|
|
3926
4039
|
async getNavigationStrategy(_id) {
|
|
3927
4040
|
return {
|
|
3928
|
-
|
|
4041
|
+
_id: "NAVIGATION_STRATEGY-ELO",
|
|
3929
4042
|
docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
|
|
3930
4043
|
name: "ELO",
|
|
3931
4044
|
description: "ELO-based navigation strategy",
|
|
@@ -3990,11 +4103,17 @@ var init_coursesDB = __esm({
|
|
|
3990
4103
|
this.manifests = manifests;
|
|
3991
4104
|
}
|
|
3992
4105
|
async getCourseConfig(courseId) {
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
4106
|
+
const manifest = this.manifests[courseId];
|
|
4107
|
+
if (!manifest) {
|
|
4108
|
+
logger.warn(`Course manifest for ${courseId} not found`);
|
|
4109
|
+
throw new Error(`Course ${courseId} not found`);
|
|
4110
|
+
}
|
|
4111
|
+
if (manifest.courseConfig) {
|
|
4112
|
+
return manifest.courseConfig;
|
|
4113
|
+
} else {
|
|
4114
|
+
logger.warn(`Course config not found in manifest for course ${courseId}`);
|
|
4115
|
+
throw new Error(`Course config not found for course ${courseId}`);
|
|
3996
4116
|
}
|
|
3997
|
-
return {};
|
|
3998
4117
|
}
|
|
3999
4118
|
async getCourseList() {
|
|
4000
4119
|
return Object.keys(this.manifests).map(
|
|
@@ -4086,23 +4205,49 @@ var init_StaticDataLayerProvider = __esm({
|
|
|
4086
4205
|
config;
|
|
4087
4206
|
initialized = false;
|
|
4088
4207
|
courseUnpackers = /* @__PURE__ */ new Map();
|
|
4208
|
+
manifests = {};
|
|
4089
4209
|
constructor(config) {
|
|
4090
4210
|
this.config = {
|
|
4091
|
-
staticContentPath: config.staticContentPath || "/static-courses",
|
|
4092
4211
|
localStoragePrefix: config.localStoragePrefix || "skuilder-static",
|
|
4093
|
-
|
|
4212
|
+
rootManifest: config.rootManifest || { dependencies: {} },
|
|
4213
|
+
rootManifestUrl: config.rootManifestUrl || "/"
|
|
4094
4214
|
};
|
|
4095
4215
|
}
|
|
4216
|
+
async resolveCourseDependencies() {
|
|
4217
|
+
logger.info("[StaticDataLayerProvider] Starting course dependency resolution...");
|
|
4218
|
+
const rootManifest = this.config.rootManifest;
|
|
4219
|
+
for (const [courseName, courseUrl] of Object.entries(rootManifest.dependencies || {})) {
|
|
4220
|
+
try {
|
|
4221
|
+
logger.debug(`[StaticDataLayerProvider] Resolving dependency: ${courseName} from ${courseUrl}`);
|
|
4222
|
+
const courseManifestUrl = new URL(courseUrl, this.config.rootManifestUrl).href;
|
|
4223
|
+
const courseJsonResponse = await fetch(courseManifestUrl);
|
|
4224
|
+
if (!courseJsonResponse.ok) {
|
|
4225
|
+
throw new Error(`Failed to fetch course manifest for ${courseName}`);
|
|
4226
|
+
}
|
|
4227
|
+
const courseJson = await courseJsonResponse.json();
|
|
4228
|
+
if (courseJson.content && courseJson.content.manifest) {
|
|
4229
|
+
const baseUrl = new URL(".", courseManifestUrl).href;
|
|
4230
|
+
const finalManifestUrl = new URL(courseJson.content.manifest, courseManifestUrl).href;
|
|
4231
|
+
const finalManifestResponse = await fetch(finalManifestUrl);
|
|
4232
|
+
if (!finalManifestResponse.ok) {
|
|
4233
|
+
throw new Error(`Failed to fetch final content manifest for ${courseName} at ${finalManifestUrl}`);
|
|
4234
|
+
}
|
|
4235
|
+
const finalManifest = await finalManifestResponse.json();
|
|
4236
|
+
this.manifests[courseName] = finalManifest;
|
|
4237
|
+
const unpacker = new StaticDataUnpacker(finalManifest, baseUrl);
|
|
4238
|
+
this.courseUnpackers.set(courseName, unpacker);
|
|
4239
|
+
logger.info(`[StaticDataLayerProvider] Successfully resolved and prepared course: ${courseName}`);
|
|
4240
|
+
}
|
|
4241
|
+
} catch (e) {
|
|
4242
|
+
logger.error(`[StaticDataLayerProvider] Failed to resolve dependency ${courseName}:`, e);
|
|
4243
|
+
}
|
|
4244
|
+
}
|
|
4245
|
+
logger.info("[StaticDataLayerProvider] Course dependency resolution complete.");
|
|
4246
|
+
}
|
|
4096
4247
|
async initialize() {
|
|
4097
4248
|
if (this.initialized) return;
|
|
4098
4249
|
logger.info("Initializing static data layer provider");
|
|
4099
|
-
|
|
4100
|
-
const unpacker = new StaticDataUnpacker(
|
|
4101
|
-
manifest,
|
|
4102
|
-
`${this.config.staticContentPath}/${courseId}`
|
|
4103
|
-
);
|
|
4104
|
-
this.courseUnpackers.set(courseId, unpacker);
|
|
4105
|
-
}
|
|
4250
|
+
await this.resolveCourseDependencies();
|
|
4106
4251
|
this.initialized = true;
|
|
4107
4252
|
}
|
|
4108
4253
|
async teardown() {
|
|
@@ -4116,13 +4261,13 @@ var init_StaticDataLayerProvider = __esm({
|
|
|
4116
4261
|
getCourseDB(courseId) {
|
|
4117
4262
|
const unpacker = this.courseUnpackers.get(courseId);
|
|
4118
4263
|
if (!unpacker) {
|
|
4119
|
-
throw new Error(`Course ${courseId} not found in static data
|
|
4264
|
+
throw new Error(`Course ${courseId} not found or failed to initialize in static data layer.`);
|
|
4120
4265
|
}
|
|
4121
|
-
const manifest = this.
|
|
4266
|
+
const manifest = this.manifests[courseId];
|
|
4122
4267
|
return new StaticCourseDB(courseId, unpacker, this.getUserDB(), manifest);
|
|
4123
4268
|
}
|
|
4124
4269
|
getCoursesDB() {
|
|
4125
|
-
return new StaticCoursesDB(this.
|
|
4270
|
+
return new StaticCoursesDB(this.manifests);
|
|
4126
4271
|
}
|
|
4127
4272
|
async getClassroomDB(_classId, _type) {
|
|
4128
4273
|
throw new Error("Classrooms not supported in static mode");
|
|
@@ -4428,6 +4573,501 @@ var init_core = __esm({
|
|
|
4428
4573
|
init_core();
|
|
4429
4574
|
init_courseLookupDB();
|
|
4430
4575
|
|
|
4576
|
+
// src/study/services/SrsService.ts
|
|
4577
|
+
init_couch();
|
|
4578
|
+
import moment7 from "moment";
|
|
4579
|
+
|
|
4580
|
+
// src/study/SpacedRepetition.ts
|
|
4581
|
+
init_util();
|
|
4582
|
+
init_logger();
|
|
4583
|
+
import moment6 from "moment";
|
|
4584
|
+
var duration = moment6.duration;
|
|
4585
|
+
function newInterval(user, cardHistory) {
|
|
4586
|
+
if (areQuestionRecords(cardHistory)) {
|
|
4587
|
+
return newQuestionInterval(user, cardHistory);
|
|
4588
|
+
} else {
|
|
4589
|
+
return 1e5;
|
|
4590
|
+
}
|
|
4591
|
+
}
|
|
4592
|
+
function newQuestionInterval(user, cardHistory) {
|
|
4593
|
+
const records = cardHistory.records;
|
|
4594
|
+
const currentAttempt = records[records.length - 1];
|
|
4595
|
+
const lastInterval = lastSuccessfulInterval(records);
|
|
4596
|
+
if (lastInterval > cardHistory.bestInterval) {
|
|
4597
|
+
cardHistory.bestInterval = lastInterval;
|
|
4598
|
+
void user.update(cardHistory._id, {
|
|
4599
|
+
bestInterval: lastInterval
|
|
4600
|
+
});
|
|
4601
|
+
}
|
|
4602
|
+
if (currentAttempt.isCorrect) {
|
|
4603
|
+
const skill = Math.min(1, Math.max(0, currentAttempt.performance));
|
|
4604
|
+
logger.debug(`Demontrated skill: ${skill}`);
|
|
4605
|
+
const interval = lastInterval * (0.75 + skill);
|
|
4606
|
+
cardHistory.lapses = getLapses(cardHistory.records);
|
|
4607
|
+
cardHistory.streak = getStreak(cardHistory.records);
|
|
4608
|
+
if (cardHistory.lapses && cardHistory.streak && cardHistory.bestInterval && (cardHistory.lapses >= 0 || cardHistory.streak >= 0)) {
|
|
4609
|
+
const ret = (cardHistory.lapses * interval + cardHistory.streak * cardHistory.bestInterval) / (cardHistory.lapses + cardHistory.streak);
|
|
4610
|
+
logger.debug(`Weighted average interval calculation:
|
|
4611
|
+
(${cardHistory.lapses} * ${interval} + ${cardHistory.streak} * ${cardHistory.bestInterval}) / (${cardHistory.lapses} + ${cardHistory.streak}) = ${ret}`);
|
|
4612
|
+
return ret;
|
|
4613
|
+
} else {
|
|
4614
|
+
return interval;
|
|
4615
|
+
}
|
|
4616
|
+
} else {
|
|
4617
|
+
return 0;
|
|
4618
|
+
}
|
|
4619
|
+
}
|
|
4620
|
+
function lastSuccessfulInterval(cardHistory) {
|
|
4621
|
+
for (let i = cardHistory.length - 1; i >= 1; i--) {
|
|
4622
|
+
if (cardHistory[i].priorAttemps === 0 && cardHistory[i].isCorrect) {
|
|
4623
|
+
const lastInterval = secondsBetween(cardHistory[i - 1].timeStamp, cardHistory[i].timeStamp);
|
|
4624
|
+
const ret = Math.max(lastInterval, 20 * 60 * 60);
|
|
4625
|
+
logger.debug(`Last interval w/ this card was: ${lastInterval}s, returning ${ret}s`);
|
|
4626
|
+
return ret;
|
|
4627
|
+
}
|
|
4628
|
+
}
|
|
4629
|
+
return getInitialInterval(cardHistory);
|
|
4630
|
+
}
|
|
4631
|
+
function getStreak(records) {
|
|
4632
|
+
let streak = 0;
|
|
4633
|
+
let index = records.length - 1;
|
|
4634
|
+
while (index >= 0 && records[index].isCorrect) {
|
|
4635
|
+
index--;
|
|
4636
|
+
streak++;
|
|
4637
|
+
}
|
|
4638
|
+
return streak;
|
|
4639
|
+
}
|
|
4640
|
+
function getLapses(records) {
|
|
4641
|
+
return records.filter((r) => r.isCorrect === false).length;
|
|
4642
|
+
}
|
|
4643
|
+
function getInitialInterval(cardHistory) {
|
|
4644
|
+
logger.warn(`history of length: ${cardHistory.length} ignored!`);
|
|
4645
|
+
return 60 * 60 * 24 * 3;
|
|
4646
|
+
}
|
|
4647
|
+
function secondsBetween(start, end) {
|
|
4648
|
+
start = moment6(start);
|
|
4649
|
+
end = moment6(end);
|
|
4650
|
+
const ret = duration(end.diff(start)).asSeconds();
|
|
4651
|
+
return ret;
|
|
4652
|
+
}
|
|
4653
|
+
|
|
4654
|
+
// src/study/services/SrsService.ts
|
|
4655
|
+
init_logger();
|
|
4656
|
+
var SrsService = class {
|
|
4657
|
+
user;
|
|
4658
|
+
constructor(user) {
|
|
4659
|
+
this.user = user;
|
|
4660
|
+
}
|
|
4661
|
+
/**
|
|
4662
|
+
* Calculates the next review time for a card based on its history and
|
|
4663
|
+
* schedules it in the user's database.
|
|
4664
|
+
* @param history The full history of the card.
|
|
4665
|
+
* @param item The study session item, used to determine if a previous review needs to be cleared.
|
|
4666
|
+
*/
|
|
4667
|
+
async scheduleReview(history, item) {
|
|
4668
|
+
const nextInterval = newInterval(this.user, history);
|
|
4669
|
+
const nextReviewTime = moment7.utc().add(nextInterval, "seconds");
|
|
4670
|
+
if (isReview(item)) {
|
|
4671
|
+
logger.info(`[SrsService] Removing previously scheduled review for: ${item.cardID}`);
|
|
4672
|
+
void this.user.removeScheduledCardReview(item.reviewID);
|
|
4673
|
+
}
|
|
4674
|
+
void this.user.scheduleCardReview({
|
|
4675
|
+
user: this.user.getUsername(),
|
|
4676
|
+
course_id: history.courseID,
|
|
4677
|
+
card_id: history.cardID,
|
|
4678
|
+
time: nextReviewTime,
|
|
4679
|
+
scheduledFor: item.contentSourceType,
|
|
4680
|
+
schedulingAgentId: item.contentSourceID
|
|
4681
|
+
});
|
|
4682
|
+
}
|
|
4683
|
+
};
|
|
4684
|
+
|
|
4685
|
+
// src/study/services/EloService.ts
|
|
4686
|
+
init_logger();
|
|
4687
|
+
import { adjustCourseScores, toCourseElo as toCourseElo3 } from "@vue-skuilder/common";
|
|
4688
|
+
var EloService = class {
|
|
4689
|
+
dataLayer;
|
|
4690
|
+
user;
|
|
4691
|
+
constructor(dataLayer, user) {
|
|
4692
|
+
this.dataLayer = dataLayer;
|
|
4693
|
+
this.user = user;
|
|
4694
|
+
}
|
|
4695
|
+
/**
|
|
4696
|
+
* Updates both user and card ELO ratings based on user performance.
|
|
4697
|
+
* @param userScore Score between 0-1 representing user performance
|
|
4698
|
+
* @param course_id Course identifier
|
|
4699
|
+
* @param card_id Card identifier
|
|
4700
|
+
* @param userCourseRegDoc User's course registration document (will be mutated)
|
|
4701
|
+
* @param currentCard Current card session record
|
|
4702
|
+
* @param k Optional K-factor for ELO calculation
|
|
4703
|
+
*/
|
|
4704
|
+
async updateUserAndCardElo(userScore, course_id, card_id, userCourseRegDoc, currentCard, k) {
|
|
4705
|
+
if (k) {
|
|
4706
|
+
logger.warn(`k value interpretation not currently implemented`);
|
|
4707
|
+
}
|
|
4708
|
+
const courseDB = this.dataLayer.getCourseDB(currentCard.card.course_id);
|
|
4709
|
+
const userElo = toCourseElo3(userCourseRegDoc.courses.find((c) => c.courseID === course_id).elo);
|
|
4710
|
+
const cardElo = (await courseDB.getCardEloData([currentCard.card.card_id]))[0];
|
|
4711
|
+
if (cardElo && userElo) {
|
|
4712
|
+
const eloUpdate = adjustCourseScores(userElo, cardElo, userScore);
|
|
4713
|
+
userCourseRegDoc.courses.find((c) => c.courseID === course_id).elo = eloUpdate.userElo;
|
|
4714
|
+
const results = await Promise.allSettled([
|
|
4715
|
+
this.user.updateUserElo(course_id, eloUpdate.userElo),
|
|
4716
|
+
courseDB.updateCardElo(card_id, eloUpdate.cardElo)
|
|
4717
|
+
]);
|
|
4718
|
+
const userEloStatus = results[0].status === "fulfilled";
|
|
4719
|
+
const cardEloStatus = results[1].status === "fulfilled";
|
|
4720
|
+
if (userEloStatus && cardEloStatus) {
|
|
4721
|
+
const user = results[0].value;
|
|
4722
|
+
const card = results[1].value;
|
|
4723
|
+
if (user.ok && card && card.ok) {
|
|
4724
|
+
logger.info(
|
|
4725
|
+
`[EloService] Updated ELOS:
|
|
4726
|
+
User: ${JSON.stringify(eloUpdate.userElo)})
|
|
4727
|
+
Card: ${JSON.stringify(eloUpdate.cardElo)})
|
|
4728
|
+
`
|
|
4729
|
+
);
|
|
4730
|
+
}
|
|
4731
|
+
} else {
|
|
4732
|
+
logger.warn(
|
|
4733
|
+
`[EloService] Partial ELO update:
|
|
4734
|
+
User ELO update: ${userEloStatus ? "SUCCESS" : "FAILED"}
|
|
4735
|
+
Card ELO update: ${cardEloStatus ? "SUCCESS" : "FAILED"}`
|
|
4736
|
+
);
|
|
4737
|
+
if (!userEloStatus && results[0].status === "rejected") {
|
|
4738
|
+
logger.error("[EloService] User ELO update error:", results[0].reason);
|
|
4739
|
+
}
|
|
4740
|
+
if (!cardEloStatus && results[1].status === "rejected") {
|
|
4741
|
+
logger.error("[EloService] Card ELO update error:", results[1].reason);
|
|
4742
|
+
}
|
|
4743
|
+
}
|
|
4744
|
+
}
|
|
4745
|
+
}
|
|
4746
|
+
};
|
|
4747
|
+
|
|
4748
|
+
// src/study/services/ResponseProcessor.ts
|
|
4749
|
+
init_core();
|
|
4750
|
+
init_logger();
|
|
4751
|
+
var ResponseProcessor = class {
|
|
4752
|
+
srsService;
|
|
4753
|
+
eloService;
|
|
4754
|
+
constructor(srsService, eloService) {
|
|
4755
|
+
this.srsService = srsService;
|
|
4756
|
+
this.eloService = eloService;
|
|
4757
|
+
}
|
|
4758
|
+
/**
|
|
4759
|
+
* Processes a user's response to a card, handling SRS scheduling and ELO updates.
|
|
4760
|
+
* @param cardRecord User's response record
|
|
4761
|
+
* @param cardHistory Promise resolving to the card's history
|
|
4762
|
+
* @param studySessionItem Current study session item
|
|
4763
|
+
* @param courseRegistrationDoc User's course registration (for ELO updates)
|
|
4764
|
+
* @param currentCard Current study session record
|
|
4765
|
+
* @param courseId Course identifier
|
|
4766
|
+
* @param cardId Card identifier
|
|
4767
|
+
* @param maxAttemptsPerView Maximum attempts allowed per view
|
|
4768
|
+
* @param maxSessionViews Maximum session views for this card
|
|
4769
|
+
* @param sessionViews Current number of session views
|
|
4770
|
+
* @returns ResponseResult with navigation and UI instructions
|
|
4771
|
+
*/
|
|
4772
|
+
async processResponse(cardRecord, cardHistory, studySessionItem, courseRegistrationDoc, currentCard, courseId, cardId, maxAttemptsPerView, maxSessionViews, sessionViews) {
|
|
4773
|
+
if (!isQuestionRecord(cardRecord)) {
|
|
4774
|
+
return {
|
|
4775
|
+
nextCardAction: "dismiss-success",
|
|
4776
|
+
shouldLoadNextCard: true,
|
|
4777
|
+
isCorrect: true,
|
|
4778
|
+
// non-question records are considered "correct"
|
|
4779
|
+
shouldClearFeedbackShadow: true
|
|
4780
|
+
};
|
|
4781
|
+
}
|
|
4782
|
+
const history = await cardHistory;
|
|
4783
|
+
if (cardRecord.isCorrect) {
|
|
4784
|
+
return this.processCorrectResponse(
|
|
4785
|
+
cardRecord,
|
|
4786
|
+
history,
|
|
4787
|
+
studySessionItem,
|
|
4788
|
+
courseRegistrationDoc,
|
|
4789
|
+
currentCard,
|
|
4790
|
+
courseId,
|
|
4791
|
+
cardId
|
|
4792
|
+
);
|
|
4793
|
+
} else {
|
|
4794
|
+
return this.processIncorrectResponse(
|
|
4795
|
+
cardRecord,
|
|
4796
|
+
history,
|
|
4797
|
+
courseRegistrationDoc,
|
|
4798
|
+
currentCard,
|
|
4799
|
+
courseId,
|
|
4800
|
+
cardId,
|
|
4801
|
+
maxAttemptsPerView,
|
|
4802
|
+
maxSessionViews,
|
|
4803
|
+
sessionViews
|
|
4804
|
+
);
|
|
4805
|
+
}
|
|
4806
|
+
}
|
|
4807
|
+
/**
|
|
4808
|
+
* Handles processing for correct responses: SRS scheduling and ELO updates.
|
|
4809
|
+
*/
|
|
4810
|
+
processCorrectResponse(cardRecord, history, studySessionItem, courseRegistrationDoc, currentCard, courseId, cardId) {
|
|
4811
|
+
if (cardRecord.priorAttemps === 0) {
|
|
4812
|
+
void this.srsService.scheduleReview(history, studySessionItem);
|
|
4813
|
+
if (history.records.length === 1) {
|
|
4814
|
+
const userScore = 0.5 + cardRecord.performance / 2;
|
|
4815
|
+
void this.eloService.updateUserAndCardElo(
|
|
4816
|
+
userScore,
|
|
4817
|
+
courseId,
|
|
4818
|
+
cardId,
|
|
4819
|
+
courseRegistrationDoc,
|
|
4820
|
+
currentCard
|
|
4821
|
+
);
|
|
4822
|
+
} else {
|
|
4823
|
+
const k = Math.ceil(32 / history.records.length);
|
|
4824
|
+
const userScore = 0.5 + cardRecord.performance / 2;
|
|
4825
|
+
void this.eloService.updateUserAndCardElo(
|
|
4826
|
+
userScore,
|
|
4827
|
+
courseId,
|
|
4828
|
+
cardId,
|
|
4829
|
+
courseRegistrationDoc,
|
|
4830
|
+
currentCard,
|
|
4831
|
+
k
|
|
4832
|
+
);
|
|
4833
|
+
}
|
|
4834
|
+
logger.info(
|
|
4835
|
+
"[ResponseProcessor] Processed correct response with SRS scheduling and ELO update"
|
|
4836
|
+
);
|
|
4837
|
+
return {
|
|
4838
|
+
nextCardAction: "dismiss-success",
|
|
4839
|
+
shouldLoadNextCard: true,
|
|
4840
|
+
isCorrect: true,
|
|
4841
|
+
performanceScore: cardRecord.performance,
|
|
4842
|
+
shouldClearFeedbackShadow: true
|
|
4843
|
+
};
|
|
4844
|
+
} else {
|
|
4845
|
+
logger.info(
|
|
4846
|
+
"[ResponseProcessor] Processed correct response (retry attempt - no scheduling/ELO)"
|
|
4847
|
+
);
|
|
4848
|
+
return {
|
|
4849
|
+
nextCardAction: "marked-failed",
|
|
4850
|
+
shouldLoadNextCard: true,
|
|
4851
|
+
isCorrect: true,
|
|
4852
|
+
performanceScore: cardRecord.performance,
|
|
4853
|
+
shouldClearFeedbackShadow: true
|
|
4854
|
+
};
|
|
4855
|
+
}
|
|
4856
|
+
}
|
|
4857
|
+
/**
|
|
4858
|
+
* Handles processing for incorrect responses: ELO updates only.
|
|
4859
|
+
*/
|
|
4860
|
+
processIncorrectResponse(cardRecord, history, courseRegistrationDoc, currentCard, courseId, cardId, maxAttemptsPerView, maxSessionViews, sessionViews) {
|
|
4861
|
+
if (history.records.length !== 1 && cardRecord.priorAttemps === 0) {
|
|
4862
|
+
void this.eloService.updateUserAndCardElo(
|
|
4863
|
+
0,
|
|
4864
|
+
// Failed response = 0 score
|
|
4865
|
+
courseId,
|
|
4866
|
+
cardId,
|
|
4867
|
+
courseRegistrationDoc,
|
|
4868
|
+
currentCard
|
|
4869
|
+
);
|
|
4870
|
+
logger.info("[ResponseProcessor] Processed incorrect response with ELO update");
|
|
4871
|
+
} else {
|
|
4872
|
+
logger.info("[ResponseProcessor] Processed incorrect response (no ELO update needed)");
|
|
4873
|
+
}
|
|
4874
|
+
if (currentCard.records.length >= maxAttemptsPerView) {
|
|
4875
|
+
if (sessionViews >= maxSessionViews) {
|
|
4876
|
+
void this.eloService.updateUserAndCardElo(
|
|
4877
|
+
0,
|
|
4878
|
+
courseId,
|
|
4879
|
+
cardId,
|
|
4880
|
+
courseRegistrationDoc,
|
|
4881
|
+
currentCard
|
|
4882
|
+
);
|
|
4883
|
+
return {
|
|
4884
|
+
nextCardAction: "dismiss-failed",
|
|
4885
|
+
shouldLoadNextCard: true,
|
|
4886
|
+
isCorrect: false,
|
|
4887
|
+
shouldClearFeedbackShadow: true
|
|
4888
|
+
};
|
|
4889
|
+
} else {
|
|
4890
|
+
return {
|
|
4891
|
+
nextCardAction: "marked-failed",
|
|
4892
|
+
shouldLoadNextCard: true,
|
|
4893
|
+
isCorrect: false,
|
|
4894
|
+
shouldClearFeedbackShadow: true
|
|
4895
|
+
};
|
|
4896
|
+
}
|
|
4897
|
+
} else {
|
|
4898
|
+
return {
|
|
4899
|
+
nextCardAction: "none",
|
|
4900
|
+
shouldLoadNextCard: false,
|
|
4901
|
+
isCorrect: false,
|
|
4902
|
+
shouldClearFeedbackShadow: true
|
|
4903
|
+
};
|
|
4904
|
+
}
|
|
4905
|
+
}
|
|
4906
|
+
};
|
|
4907
|
+
|
|
4908
|
+
// src/study/services/CardHydrationService.ts
|
|
4909
|
+
init_logger();
|
|
4910
|
+
import {
|
|
4911
|
+
displayableDataToViewData,
|
|
4912
|
+
isCourseElo,
|
|
4913
|
+
toCourseElo as toCourseElo4
|
|
4914
|
+
} from "@vue-skuilder/common";
|
|
4915
|
+
|
|
4916
|
+
// src/study/ItemQueue.ts
|
|
4917
|
+
var ItemQueue = class {
|
|
4918
|
+
q = [];
|
|
4919
|
+
seenCardIds = [];
|
|
4920
|
+
_dequeueCount = 0;
|
|
4921
|
+
get dequeueCount() {
|
|
4922
|
+
return this._dequeueCount;
|
|
4923
|
+
}
|
|
4924
|
+
add(item, cardId) {
|
|
4925
|
+
if (this.seenCardIds.find((d) => d === cardId)) {
|
|
4926
|
+
return;
|
|
4927
|
+
}
|
|
4928
|
+
this.seenCardIds.push(cardId);
|
|
4929
|
+
this.q.push(item);
|
|
4930
|
+
}
|
|
4931
|
+
addAll(items, cardIdExtractor) {
|
|
4932
|
+
items.forEach((i) => this.add(i, cardIdExtractor(i)));
|
|
4933
|
+
}
|
|
4934
|
+
get length() {
|
|
4935
|
+
return this.q.length;
|
|
4936
|
+
}
|
|
4937
|
+
peek(index) {
|
|
4938
|
+
return this.q[index];
|
|
4939
|
+
}
|
|
4940
|
+
dequeue(cardIdExtractor) {
|
|
4941
|
+
if (this.q.length !== 0) {
|
|
4942
|
+
this._dequeueCount++;
|
|
4943
|
+
const item = this.q.splice(0, 1)[0];
|
|
4944
|
+
if (cardIdExtractor) {
|
|
4945
|
+
const cardId = cardIdExtractor(item);
|
|
4946
|
+
const index = this.seenCardIds.indexOf(cardId);
|
|
4947
|
+
if (index > -1) {
|
|
4948
|
+
this.seenCardIds.splice(index, 1);
|
|
4949
|
+
}
|
|
4950
|
+
}
|
|
4951
|
+
return item;
|
|
4952
|
+
} else {
|
|
4953
|
+
return null;
|
|
4954
|
+
}
|
|
4955
|
+
}
|
|
4956
|
+
get toString() {
|
|
4957
|
+
return `${typeof this.q[0]}:
|
|
4958
|
+
` + this.q.map((i) => ` ${i.courseID}+${i.cardID}: ${i.status}`).join("\n");
|
|
4959
|
+
}
|
|
4960
|
+
};
|
|
4961
|
+
|
|
4962
|
+
// src/study/services/CardHydrationService.ts
|
|
4963
|
+
var CardHydrationService = class {
|
|
4964
|
+
constructor(getViewComponent, getCourseDB3, selectNextItemToHydrate, removeItemFromQueue, hasAvailableCards) {
|
|
4965
|
+
this.getViewComponent = getViewComponent;
|
|
4966
|
+
this.getCourseDB = getCourseDB3;
|
|
4967
|
+
this.selectNextItemToHydrate = selectNextItemToHydrate;
|
|
4968
|
+
this.removeItemFromQueue = removeItemFromQueue;
|
|
4969
|
+
this.hasAvailableCards = hasAvailableCards;
|
|
4970
|
+
}
|
|
4971
|
+
hydratedQ = new ItemQueue();
|
|
4972
|
+
failedCardCache = /* @__PURE__ */ new Map();
|
|
4973
|
+
hydrationInProgress = false;
|
|
4974
|
+
BUFFER_SIZE = 5;
|
|
4975
|
+
/**
|
|
4976
|
+
* Get the next hydrated card from the queue.
|
|
4977
|
+
* @returns Hydrated card or null if none available
|
|
4978
|
+
*/
|
|
4979
|
+
dequeueHydratedCard() {
|
|
4980
|
+
return this.hydratedQ.dequeue((item) => item.item.cardID);
|
|
4981
|
+
}
|
|
4982
|
+
/**
|
|
4983
|
+
* Check if hydration should be triggered and start background hydration if needed.
|
|
4984
|
+
*/
|
|
4985
|
+
async ensureHydratedCards() {
|
|
4986
|
+
if (this.hydratedQ.length < 3) {
|
|
4987
|
+
void this.fillHydratedQueue();
|
|
4988
|
+
}
|
|
4989
|
+
}
|
|
4990
|
+
/**
|
|
4991
|
+
* Wait for a hydrated card to become available.
|
|
4992
|
+
* @returns Promise that resolves to a hydrated card or null
|
|
4993
|
+
*/
|
|
4994
|
+
async waitForHydratedCard() {
|
|
4995
|
+
if (this.hydratedQ.length === 0 && this.hasAvailableCards()) {
|
|
4996
|
+
void this.fillHydratedQueue();
|
|
4997
|
+
}
|
|
4998
|
+
while (this.hydratedQ.length === 0 && this.hasAvailableCards()) {
|
|
4999
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
5000
|
+
}
|
|
5001
|
+
return this.dequeueHydratedCard();
|
|
5002
|
+
}
|
|
5003
|
+
/**
|
|
5004
|
+
* Get current hydrated queue length.
|
|
5005
|
+
*/
|
|
5006
|
+
get hydratedCount() {
|
|
5007
|
+
return this.hydratedQ.length;
|
|
5008
|
+
}
|
|
5009
|
+
/**
|
|
5010
|
+
* Fill the hydrated queue up to BUFFER_SIZE with pre-fetched cards.
|
|
5011
|
+
*/
|
|
5012
|
+
async fillHydratedQueue() {
|
|
5013
|
+
if (this.hydrationInProgress) {
|
|
5014
|
+
return;
|
|
5015
|
+
}
|
|
5016
|
+
this.hydrationInProgress = true;
|
|
5017
|
+
try {
|
|
5018
|
+
while (this.hydratedQ.length < this.BUFFER_SIZE) {
|
|
5019
|
+
const nextItem = this.selectNextItemToHydrate();
|
|
5020
|
+
if (!nextItem) {
|
|
5021
|
+
return;
|
|
5022
|
+
}
|
|
5023
|
+
try {
|
|
5024
|
+
if (this.failedCardCache.has(nextItem.cardID)) {
|
|
5025
|
+
const cachedCard = this.failedCardCache.get(nextItem.cardID);
|
|
5026
|
+
this.hydratedQ.add(cachedCard, cachedCard.item.cardID);
|
|
5027
|
+
this.failedCardCache.delete(nextItem.cardID);
|
|
5028
|
+
} else {
|
|
5029
|
+
const courseDB = this.getCourseDB(nextItem.courseID);
|
|
5030
|
+
const cardData = await courseDB.getCourseDoc(nextItem.cardID);
|
|
5031
|
+
if (!isCourseElo(cardData.elo)) {
|
|
5032
|
+
cardData.elo = toCourseElo4(cardData.elo);
|
|
5033
|
+
}
|
|
5034
|
+
const view = this.getViewComponent(cardData.id_view);
|
|
5035
|
+
const dataDocs = await Promise.all(
|
|
5036
|
+
cardData.id_displayable_data.map(
|
|
5037
|
+
(id) => courseDB.getCourseDoc(id, {
|
|
5038
|
+
attachments: true,
|
|
5039
|
+
binary: true
|
|
5040
|
+
})
|
|
5041
|
+
)
|
|
5042
|
+
);
|
|
5043
|
+
const data = dataDocs.map(displayableDataToViewData).reverse();
|
|
5044
|
+
this.hydratedQ.add(
|
|
5045
|
+
{
|
|
5046
|
+
item: nextItem,
|
|
5047
|
+
view,
|
|
5048
|
+
data
|
|
5049
|
+
},
|
|
5050
|
+
nextItem.cardID
|
|
5051
|
+
);
|
|
5052
|
+
}
|
|
5053
|
+
} catch (e) {
|
|
5054
|
+
logger.error(`Error hydrating card ${nextItem.cardID}:`, e);
|
|
5055
|
+
} finally {
|
|
5056
|
+
this.removeItemFromQueue(nextItem);
|
|
5057
|
+
}
|
|
5058
|
+
}
|
|
5059
|
+
} finally {
|
|
5060
|
+
this.hydrationInProgress = false;
|
|
5061
|
+
}
|
|
5062
|
+
}
|
|
5063
|
+
/**
|
|
5064
|
+
* Cache a failed card for quick re-access.
|
|
5065
|
+
*/
|
|
5066
|
+
cacheFailedCard(card) {
|
|
5067
|
+
this.failedCardCache.set(card.item.cardID, card);
|
|
5068
|
+
}
|
|
5069
|
+
};
|
|
5070
|
+
|
|
4431
5071
|
// src/study/SessionController.ts
|
|
4432
5072
|
init_couch();
|
|
4433
5073
|
|
|
@@ -5855,65 +6495,27 @@ init_dataDirectory();
|
|
|
5855
6495
|
init_tuiLogger();
|
|
5856
6496
|
|
|
5857
6497
|
// src/study/SessionController.ts
|
|
5858
|
-
import {
|
|
5859
|
-
displayableDataToViewData,
|
|
5860
|
-
isCourseElo,
|
|
5861
|
-
toCourseElo as toCourseElo3
|
|
5862
|
-
} from "@vue-skuilder/common";
|
|
5863
6498
|
function randomInt(min, max) {
|
|
5864
6499
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
5865
6500
|
}
|
|
5866
|
-
var ItemQueue = class {
|
|
5867
|
-
q = [];
|
|
5868
|
-
seenCardIds = [];
|
|
5869
|
-
_dequeueCount = 0;
|
|
5870
|
-
get dequeueCount() {
|
|
5871
|
-
return this._dequeueCount;
|
|
5872
|
-
}
|
|
5873
|
-
add(item, cardId) {
|
|
5874
|
-
if (this.seenCardIds.find((d) => d === cardId)) {
|
|
5875
|
-
return;
|
|
5876
|
-
}
|
|
5877
|
-
this.seenCardIds.push(cardId);
|
|
5878
|
-
this.q.push(item);
|
|
5879
|
-
}
|
|
5880
|
-
addAll(items, cardIdExtractor) {
|
|
5881
|
-
items.forEach((i) => this.add(i, cardIdExtractor(i)));
|
|
5882
|
-
}
|
|
5883
|
-
get length() {
|
|
5884
|
-
return this.q.length;
|
|
5885
|
-
}
|
|
5886
|
-
peek(index) {
|
|
5887
|
-
return this.q[index];
|
|
5888
|
-
}
|
|
5889
|
-
dequeue() {
|
|
5890
|
-
if (this.q.length !== 0) {
|
|
5891
|
-
this._dequeueCount++;
|
|
5892
|
-
return this.q.splice(0, 1)[0];
|
|
5893
|
-
} else {
|
|
5894
|
-
return null;
|
|
5895
|
-
}
|
|
5896
|
-
}
|
|
5897
|
-
get toString() {
|
|
5898
|
-
return `${typeof this.q[0]}:
|
|
5899
|
-
` + this.q.map((i) => ` ${i.courseID}+${i.cardID}: ${i.status}`).join("\n");
|
|
5900
|
-
}
|
|
5901
|
-
};
|
|
5902
6501
|
var SessionController = class extends Loggable {
|
|
5903
6502
|
_className = "SessionController";
|
|
6503
|
+
services;
|
|
6504
|
+
srsService;
|
|
6505
|
+
eloService;
|
|
6506
|
+
hydrationService;
|
|
5904
6507
|
sources;
|
|
5905
|
-
dataLayer
|
|
5906
|
-
getViewComponent;
|
|
6508
|
+
// dataLayer and getViewComponent now injected into CardHydrationService
|
|
5907
6509
|
_sessionRecord = [];
|
|
5908
6510
|
set sessionRecord(r) {
|
|
5909
6511
|
this._sessionRecord = r;
|
|
5910
6512
|
}
|
|
6513
|
+
// Session card stores
|
|
6514
|
+
_currentCard = null;
|
|
5911
6515
|
reviewQ = new ItemQueue();
|
|
5912
6516
|
newQ = new ItemQueue();
|
|
5913
6517
|
failedQ = new ItemQueue();
|
|
5914
|
-
|
|
5915
|
-
_currentCard = null;
|
|
5916
|
-
hydration_in_progress = false;
|
|
6518
|
+
// END Session card stores
|
|
5917
6519
|
startTime;
|
|
5918
6520
|
endTime;
|
|
5919
6521
|
_secondsRemaining;
|
|
@@ -5933,19 +6535,29 @@ var SessionController = class extends Loggable {
|
|
|
5933
6535
|
*/
|
|
5934
6536
|
constructor(sources, time, dataLayer, getViewComponent) {
|
|
5935
6537
|
super();
|
|
6538
|
+
this.srsService = new SrsService(dataLayer.getUserDB());
|
|
6539
|
+
this.eloService = new EloService(dataLayer, dataLayer.getUserDB());
|
|
6540
|
+
this.hydrationService = new CardHydrationService(
|
|
6541
|
+
getViewComponent,
|
|
6542
|
+
(courseId) => dataLayer.getCourseDB(courseId),
|
|
6543
|
+
() => this._selectNextItemToHydrate(),
|
|
6544
|
+
(item) => this.removeItemFromQueue(item),
|
|
6545
|
+
() => this.hasAvailableCards()
|
|
6546
|
+
);
|
|
6547
|
+
this.services = {
|
|
6548
|
+
response: new ResponseProcessor(this.srsService, this.eloService)
|
|
6549
|
+
};
|
|
5936
6550
|
this.sources = sources;
|
|
5937
6551
|
this.startTime = /* @__PURE__ */ new Date();
|
|
5938
6552
|
this._secondsRemaining = time;
|
|
5939
6553
|
this.endTime = new Date(this.startTime.valueOf() + 1e3 * this._secondsRemaining);
|
|
5940
|
-
this.dataLayer = dataLayer;
|
|
5941
|
-
this.getViewComponent = getViewComponent;
|
|
5942
6554
|
this.log(`Session constructed:
|
|
5943
6555
|
startTime: ${this.startTime}
|
|
5944
6556
|
endTime: ${this.endTime}`);
|
|
5945
6557
|
}
|
|
5946
6558
|
tick() {
|
|
5947
6559
|
this._secondsRemaining = Math.floor((this.endTime.valueOf() - Date.now()) / 1e3);
|
|
5948
|
-
if (this._secondsRemaining
|
|
6560
|
+
if (this._secondsRemaining <= 0) {
|
|
5949
6561
|
clearInterval(this._intervalHandle);
|
|
5950
6562
|
}
|
|
5951
6563
|
}
|
|
@@ -5989,7 +6601,7 @@ var SessionController = class extends Loggable {
|
|
|
5989
6601
|
} catch (e) {
|
|
5990
6602
|
this.error("Error preparing study session:", e);
|
|
5991
6603
|
}
|
|
5992
|
-
await this.
|
|
6604
|
+
await this.hydrationService.ensureHydratedCards();
|
|
5993
6605
|
this._intervalHandle = setInterval(() => {
|
|
5994
6606
|
this.tick();
|
|
5995
6607
|
}, 1e3);
|
|
@@ -6050,7 +6662,7 @@ var SessionController = class extends Loggable {
|
|
|
6050
6662
|
}
|
|
6051
6663
|
}
|
|
6052
6664
|
}
|
|
6053
|
-
_selectNextItemToHydrate(
|
|
6665
|
+
_selectNextItemToHydrate() {
|
|
6054
6666
|
const choice = Math.random();
|
|
6055
6667
|
let newBound = 0.1;
|
|
6056
6668
|
let reviewBound = 0.75;
|
|
@@ -6083,9 +6695,6 @@ var SessionController = class extends Loggable {
|
|
|
6083
6695
|
newBound = 0.01;
|
|
6084
6696
|
reviewBound = 0.1;
|
|
6085
6697
|
}
|
|
6086
|
-
if (this.failedQ.length === 1 && action === "marked-failed") {
|
|
6087
|
-
reviewBound = 1;
|
|
6088
|
-
}
|
|
6089
6698
|
if (this.failedQ.length === 0) {
|
|
6090
6699
|
reviewBound = 1;
|
|
6091
6700
|
}
|
|
@@ -6105,36 +6714,73 @@ var SessionController = class extends Loggable {
|
|
|
6105
6714
|
}
|
|
6106
6715
|
async nextCard(action = "dismiss-success") {
|
|
6107
6716
|
this.dismissCurrentCard(action);
|
|
6108
|
-
|
|
6717
|
+
if (this._secondsRemaining <= 0 && this.failedQ.length === 0) {
|
|
6718
|
+
this._currentCard = null;
|
|
6719
|
+
return null;
|
|
6720
|
+
}
|
|
6721
|
+
let card = this.hydrationService.dequeueHydratedCard();
|
|
6109
6722
|
if (!card && this.hasAvailableCards()) {
|
|
6110
|
-
|
|
6111
|
-
card = await this.nextHydratedCard();
|
|
6723
|
+
card = await this.hydrationService.waitForHydratedCard();
|
|
6112
6724
|
}
|
|
6113
|
-
|
|
6114
|
-
|
|
6725
|
+
await this.hydrationService.ensureHydratedCards();
|
|
6726
|
+
if (card) {
|
|
6727
|
+
this._currentCard = card;
|
|
6728
|
+
} else {
|
|
6729
|
+
this._currentCard = null;
|
|
6115
6730
|
}
|
|
6116
6731
|
return card;
|
|
6117
6732
|
}
|
|
6733
|
+
/**
|
|
6734
|
+
* Public API for processing user responses to cards.
|
|
6735
|
+
* @param cardRecord User's response record
|
|
6736
|
+
* @param cardHistory Promise resolving to the card's history
|
|
6737
|
+
* @param courseRegistrationDoc User's course registration document
|
|
6738
|
+
* @param currentCard Current study session record
|
|
6739
|
+
* @param courseId Course identifier
|
|
6740
|
+
* @param cardId Card identifier
|
|
6741
|
+
* @param maxAttemptsPerView Maximum attempts allowed per view
|
|
6742
|
+
* @param maxSessionViews Maximum session views for this card
|
|
6743
|
+
* @param sessionViews Current number of session views
|
|
6744
|
+
* @returns ResponseResult with navigation and UI instructions
|
|
6745
|
+
*/
|
|
6746
|
+
async submitResponse(cardRecord, cardHistory, courseRegistrationDoc, currentCard, courseId, cardId, maxAttemptsPerView, maxSessionViews, sessionViews) {
|
|
6747
|
+
const studySessionItem = {
|
|
6748
|
+
...currentCard.item
|
|
6749
|
+
};
|
|
6750
|
+
return await this.services.response.processResponse(
|
|
6751
|
+
cardRecord,
|
|
6752
|
+
cardHistory,
|
|
6753
|
+
studySessionItem,
|
|
6754
|
+
courseRegistrationDoc,
|
|
6755
|
+
currentCard,
|
|
6756
|
+
courseId,
|
|
6757
|
+
cardId,
|
|
6758
|
+
maxAttemptsPerView,
|
|
6759
|
+
maxSessionViews,
|
|
6760
|
+
sessionViews
|
|
6761
|
+
);
|
|
6762
|
+
}
|
|
6118
6763
|
dismissCurrentCard(action = "dismiss-success") {
|
|
6119
6764
|
if (this._currentCard) {
|
|
6120
6765
|
if (action === "dismiss-success") {
|
|
6121
6766
|
} else if (action === "marked-failed") {
|
|
6767
|
+
this.hydrationService.cacheFailedCard(this._currentCard);
|
|
6122
6768
|
let failedItem;
|
|
6123
|
-
if (isReview(this._currentCard)) {
|
|
6769
|
+
if (isReview(this._currentCard.item)) {
|
|
6124
6770
|
failedItem = {
|
|
6125
|
-
cardID: this._currentCard.cardID,
|
|
6126
|
-
courseID: this._currentCard.courseID,
|
|
6127
|
-
contentSourceID: this._currentCard.contentSourceID,
|
|
6128
|
-
contentSourceType: this._currentCard.contentSourceType,
|
|
6771
|
+
cardID: this._currentCard.item.cardID,
|
|
6772
|
+
courseID: this._currentCard.item.courseID,
|
|
6773
|
+
contentSourceID: this._currentCard.item.contentSourceID,
|
|
6774
|
+
contentSourceType: this._currentCard.item.contentSourceType,
|
|
6129
6775
|
status: "failed-review",
|
|
6130
|
-
reviewID: this._currentCard.reviewID
|
|
6776
|
+
reviewID: this._currentCard.item.reviewID
|
|
6131
6777
|
};
|
|
6132
6778
|
} else {
|
|
6133
6779
|
failedItem = {
|
|
6134
|
-
cardID: this._currentCard.cardID,
|
|
6135
|
-
courseID: this._currentCard.courseID,
|
|
6136
|
-
contentSourceID: this._currentCard.contentSourceID,
|
|
6137
|
-
contentSourceType: this._currentCard.contentSourceType,
|
|
6780
|
+
cardID: this._currentCard.item.cardID,
|
|
6781
|
+
courseID: this._currentCard.item.courseID,
|
|
6782
|
+
contentSourceID: this._currentCard.item.contentSourceID,
|
|
6783
|
+
contentSourceType: this._currentCard.item.contentSourceType,
|
|
6138
6784
|
status: "failed-new"
|
|
6139
6785
|
};
|
|
6140
6786
|
}
|
|
@@ -6147,141 +6793,19 @@ var SessionController = class extends Loggable {
|
|
|
6147
6793
|
hasAvailableCards() {
|
|
6148
6794
|
return this.reviewQ.length > 0 || this.newQ.length > 0 || this.failedQ.length > 0;
|
|
6149
6795
|
}
|
|
6150
|
-
|
|
6151
|
-
|
|
6152
|
-
|
|
6153
|
-
|
|
6154
|
-
|
|
6155
|
-
|
|
6156
|
-
|
|
6157
|
-
|
|
6158
|
-
return;
|
|
6159
|
-
}
|
|
6160
|
-
const BUFFER_SIZE = 5;
|
|
6161
|
-
this.hydration_in_progress = true;
|
|
6162
|
-
while (this.hydratedQ.length < BUFFER_SIZE) {
|
|
6163
|
-
const nextItem = this._selectNextItemToHydrate();
|
|
6164
|
-
if (!nextItem) {
|
|
6165
|
-
return;
|
|
6166
|
-
}
|
|
6167
|
-
try {
|
|
6168
|
-
const cardData = await this.dataLayer.getCourseDB(nextItem.courseID).getCourseDoc(nextItem.cardID);
|
|
6169
|
-
if (!isCourseElo(cardData.elo)) {
|
|
6170
|
-
cardData.elo = toCourseElo3(cardData.elo);
|
|
6171
|
-
}
|
|
6172
|
-
const view = this.getViewComponent(cardData.id_view);
|
|
6173
|
-
const dataDocs = await Promise.all(
|
|
6174
|
-
cardData.id_displayable_data.map(
|
|
6175
|
-
(id) => this.dataLayer.getCourseDB(nextItem.courseID).getCourseDoc(id, {
|
|
6176
|
-
attachments: true,
|
|
6177
|
-
binary: true
|
|
6178
|
-
})
|
|
6179
|
-
)
|
|
6180
|
-
);
|
|
6181
|
-
const data = dataDocs.map(displayableDataToViewData).reverse();
|
|
6182
|
-
this.hydratedQ.add(
|
|
6183
|
-
{
|
|
6184
|
-
item: nextItem,
|
|
6185
|
-
view,
|
|
6186
|
-
data
|
|
6187
|
-
},
|
|
6188
|
-
nextItem.cardID
|
|
6189
|
-
);
|
|
6190
|
-
if (this.reviewQ.peek(0) === nextItem) {
|
|
6191
|
-
this.reviewQ.dequeue();
|
|
6192
|
-
} else if (this.newQ.peek(0) === nextItem) {
|
|
6193
|
-
this.newQ.dequeue();
|
|
6194
|
-
} else {
|
|
6195
|
-
this.failedQ.dequeue();
|
|
6196
|
-
}
|
|
6197
|
-
} catch (e) {
|
|
6198
|
-
this.error(`Error hydrating card ${nextItem.cardID}:`, e);
|
|
6199
|
-
if (this.reviewQ.peek(0) === nextItem) {
|
|
6200
|
-
this.reviewQ.dequeue();
|
|
6201
|
-
} else if (this.newQ.peek(0) === nextItem) {
|
|
6202
|
-
this.newQ.dequeue();
|
|
6203
|
-
} else {
|
|
6204
|
-
this.failedQ.dequeue();
|
|
6205
|
-
}
|
|
6206
|
-
}
|
|
6207
|
-
}
|
|
6208
|
-
this.hydration_in_progress = false;
|
|
6209
|
-
}
|
|
6210
|
-
};
|
|
6211
|
-
|
|
6212
|
-
// src/study/SpacedRepetition.ts
|
|
6213
|
-
init_util();
|
|
6214
|
-
init_logger();
|
|
6215
|
-
import moment6 from "moment";
|
|
6216
|
-
var duration = moment6.duration;
|
|
6217
|
-
function newInterval(user, cardHistory) {
|
|
6218
|
-
if (areQuestionRecords(cardHistory)) {
|
|
6219
|
-
return newQuestionInterval(user, cardHistory);
|
|
6220
|
-
} else {
|
|
6221
|
-
return 1e5;
|
|
6222
|
-
}
|
|
6223
|
-
}
|
|
6224
|
-
function newQuestionInterval(user, cardHistory) {
|
|
6225
|
-
const records = cardHistory.records;
|
|
6226
|
-
const currentAttempt = records[records.length - 1];
|
|
6227
|
-
const lastInterval = lastSuccessfulInterval(records);
|
|
6228
|
-
if (lastInterval > cardHistory.bestInterval) {
|
|
6229
|
-
cardHistory.bestInterval = lastInterval;
|
|
6230
|
-
void user.update(cardHistory._id, {
|
|
6231
|
-
bestInterval: lastInterval
|
|
6232
|
-
});
|
|
6233
|
-
}
|
|
6234
|
-
if (currentAttempt.isCorrect) {
|
|
6235
|
-
const skill = Math.min(1, Math.max(0, currentAttempt.performance));
|
|
6236
|
-
logger.debug(`Demontrated skill: ${skill}`);
|
|
6237
|
-
const interval = lastInterval * (0.75 + skill);
|
|
6238
|
-
cardHistory.lapses = getLapses(cardHistory.records);
|
|
6239
|
-
cardHistory.streak = getStreak(cardHistory.records);
|
|
6240
|
-
if (cardHistory.lapses && cardHistory.streak && cardHistory.bestInterval && (cardHistory.lapses >= 0 || cardHistory.streak >= 0)) {
|
|
6241
|
-
const ret = (cardHistory.lapses * interval + cardHistory.streak * cardHistory.bestInterval) / (cardHistory.lapses + cardHistory.streak);
|
|
6242
|
-
logger.debug(`Weighted average interval calculation:
|
|
6243
|
-
(${cardHistory.lapses} * ${interval} + ${cardHistory.streak} * ${cardHistory.bestInterval}) / (${cardHistory.lapses} + ${cardHistory.streak}) = ${ret}`);
|
|
6244
|
-
return ret;
|
|
6796
|
+
/**
|
|
6797
|
+
* Helper method for CardHydrationService to remove items from appropriate queue.
|
|
6798
|
+
*/
|
|
6799
|
+
removeItemFromQueue(item) {
|
|
6800
|
+
if (this.reviewQ.peek(0) === item) {
|
|
6801
|
+
this.reviewQ.dequeue((queueItem) => queueItem.cardID);
|
|
6802
|
+
} else if (this.newQ.peek(0) === item) {
|
|
6803
|
+
this.newQ.dequeue((queueItem) => queueItem.cardID);
|
|
6245
6804
|
} else {
|
|
6246
|
-
|
|
6805
|
+
this.failedQ.dequeue((queueItem) => queueItem.cardID);
|
|
6247
6806
|
}
|
|
6248
|
-
} else {
|
|
6249
|
-
return 0;
|
|
6250
6807
|
}
|
|
6251
|
-
}
|
|
6252
|
-
function lastSuccessfulInterval(cardHistory) {
|
|
6253
|
-
for (let i = cardHistory.length - 1; i >= 1; i--) {
|
|
6254
|
-
if (cardHistory[i].priorAttemps === 0 && cardHistory[i].isCorrect) {
|
|
6255
|
-
const lastInterval = secondsBetween(cardHistory[i - 1].timeStamp, cardHistory[i].timeStamp);
|
|
6256
|
-
const ret = Math.max(lastInterval, 20 * 60 * 60);
|
|
6257
|
-
logger.debug(`Last interval w/ this card was: ${lastInterval}s, returning ${ret}s`);
|
|
6258
|
-
return ret;
|
|
6259
|
-
}
|
|
6260
|
-
}
|
|
6261
|
-
return getInitialInterval(cardHistory);
|
|
6262
|
-
}
|
|
6263
|
-
function getStreak(records) {
|
|
6264
|
-
let streak = 0;
|
|
6265
|
-
let index = records.length - 1;
|
|
6266
|
-
while (index >= 0 && records[index].isCorrect) {
|
|
6267
|
-
index--;
|
|
6268
|
-
streak++;
|
|
6269
|
-
}
|
|
6270
|
-
return streak;
|
|
6271
|
-
}
|
|
6272
|
-
function getLapses(records) {
|
|
6273
|
-
return records.filter((r) => r.isCorrect === false).length;
|
|
6274
|
-
}
|
|
6275
|
-
function getInitialInterval(cardHistory) {
|
|
6276
|
-
logger.warn(`history of length: ${cardHistory.length} ignored!`);
|
|
6277
|
-
return 60 * 60 * 24 * 3;
|
|
6278
|
-
}
|
|
6279
|
-
function secondsBetween(start, end) {
|
|
6280
|
-
start = moment6(start);
|
|
6281
|
-
end = moment6(end);
|
|
6282
|
-
const ret = duration(end.diff(start)).asSeconds();
|
|
6283
|
-
return ret;
|
|
6284
|
-
}
|
|
6808
|
+
};
|
|
6285
6809
|
|
|
6286
6810
|
// src/index.ts
|
|
6287
6811
|
init_factory();
|