@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.js
CHANGED
|
@@ -468,7 +468,9 @@ var init_updateQueue = __esm({
|
|
|
468
468
|
async applyUpdates(id) {
|
|
469
469
|
logger.debug(`Applying updates on doc: ${id}`);
|
|
470
470
|
if (this.inprogressUpdates[id]) {
|
|
471
|
-
|
|
471
|
+
while (this.inprogressUpdates[id]) {
|
|
472
|
+
await new Promise((resolve) => setTimeout(resolve, Math.random() * 50));
|
|
473
|
+
}
|
|
472
474
|
return this.applyUpdates(id);
|
|
473
475
|
} else {
|
|
474
476
|
if (this.pendingUpdates[id] && this.pendingUpdates[id].length > 0) {
|
|
@@ -504,6 +506,9 @@ var init_updateQueue = __esm({
|
|
|
504
506
|
if (e.name === "conflict" && i < MAX_RETRIES - 1) {
|
|
505
507
|
logger.warn(`Conflict on update for doc ${id}, retry #${i + 1}`);
|
|
506
508
|
await new Promise((res) => setTimeout(res, 50 * Math.random()));
|
|
509
|
+
} else if (e.name === "not_found" && i === 0) {
|
|
510
|
+
logger.warn(`Update failed for ${id} - does not exist. Throwing to caller.`);
|
|
511
|
+
throw e;
|
|
507
512
|
} else {
|
|
508
513
|
delete this.inprogressUpdates[id];
|
|
509
514
|
if (this.pendingUpdates[id]) {
|
|
@@ -982,12 +987,74 @@ var init_elo = __esm({
|
|
|
982
987
|
}
|
|
983
988
|
});
|
|
984
989
|
|
|
990
|
+
// src/core/navigators/hardcodedOrder.ts
|
|
991
|
+
var hardcodedOrder_exports = {};
|
|
992
|
+
__export(hardcodedOrder_exports, {
|
|
993
|
+
default: () => HardcodedOrderNavigator
|
|
994
|
+
});
|
|
995
|
+
var HardcodedOrderNavigator;
|
|
996
|
+
var init_hardcodedOrder = __esm({
|
|
997
|
+
"src/core/navigators/hardcodedOrder.ts"() {
|
|
998
|
+
"use strict";
|
|
999
|
+
init_navigators();
|
|
1000
|
+
init_logger();
|
|
1001
|
+
HardcodedOrderNavigator = class extends ContentNavigator {
|
|
1002
|
+
orderedCardIds = [];
|
|
1003
|
+
user;
|
|
1004
|
+
course;
|
|
1005
|
+
constructor(user, course, strategyData) {
|
|
1006
|
+
super();
|
|
1007
|
+
this.user = user;
|
|
1008
|
+
this.course = course;
|
|
1009
|
+
if (strategyData.serializedData) {
|
|
1010
|
+
try {
|
|
1011
|
+
this.orderedCardIds = JSON.parse(strategyData.serializedData);
|
|
1012
|
+
} catch (e) {
|
|
1013
|
+
logger.error("Failed to parse serializedData for HardcodedOrderNavigator", e);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
async getPendingReviews() {
|
|
1018
|
+
const reviews = await this.user.getPendingReviews(this.course.getCourseID());
|
|
1019
|
+
return reviews.map((r) => {
|
|
1020
|
+
return {
|
|
1021
|
+
...r,
|
|
1022
|
+
contentSourceType: "course",
|
|
1023
|
+
contentSourceID: this.course.getCourseID(),
|
|
1024
|
+
cardID: r.cardId,
|
|
1025
|
+
courseID: r.courseId,
|
|
1026
|
+
reviewID: r._id,
|
|
1027
|
+
status: "review"
|
|
1028
|
+
};
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
async getNewCards(limit = 99) {
|
|
1032
|
+
const activeCardIds = (await this.user.getActiveCards()).map((c) => c.cardID);
|
|
1033
|
+
const newCardIds = this.orderedCardIds.filter(
|
|
1034
|
+
(cardId) => !activeCardIds.includes(cardId)
|
|
1035
|
+
);
|
|
1036
|
+
const cardsToReturn = newCardIds.slice(0, limit);
|
|
1037
|
+
return cardsToReturn.map((cardId) => {
|
|
1038
|
+
return {
|
|
1039
|
+
cardID: cardId,
|
|
1040
|
+
courseID: this.course.getCourseID(),
|
|
1041
|
+
contentSourceType: "course",
|
|
1042
|
+
contentSourceID: this.course.getCourseID(),
|
|
1043
|
+
status: "new"
|
|
1044
|
+
};
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
});
|
|
1050
|
+
|
|
985
1051
|
// import("./**/*") in src/core/navigators/index.ts
|
|
986
1052
|
var globImport;
|
|
987
1053
|
var init_ = __esm({
|
|
988
1054
|
'import("./**/*") in src/core/navigators/index.ts'() {
|
|
989
1055
|
globImport = __glob({
|
|
990
1056
|
"./elo.ts": () => Promise.resolve().then(() => (init_elo(), elo_exports)),
|
|
1057
|
+
"./hardcodedOrder.ts": () => Promise.resolve().then(() => (init_hardcodedOrder(), hardcodedOrder_exports)),
|
|
991
1058
|
"./index.ts": () => Promise.resolve().then(() => (init_navigators(), navigators_exports))
|
|
992
1059
|
});
|
|
993
1060
|
}
|
|
@@ -1007,6 +1074,7 @@ var init_navigators = __esm({
|
|
|
1007
1074
|
init_();
|
|
1008
1075
|
Navigators = /* @__PURE__ */ ((Navigators2) => {
|
|
1009
1076
|
Navigators2["ELO"] = "elo";
|
|
1077
|
+
Navigators2["HARDCODED"] = "hardcodedOrder";
|
|
1010
1078
|
return Navigators2;
|
|
1011
1079
|
})(Navigators || {});
|
|
1012
1080
|
ContentNavigator = class {
|
|
@@ -1276,6 +1344,23 @@ var init_courseDB = __esm({
|
|
|
1276
1344
|
if (!doc.docType || !(doc.docType === "CARD" /* CARD */)) {
|
|
1277
1345
|
throw new Error(`failed to remove ${id} from course ${this.id}. id does not point to a card`);
|
|
1278
1346
|
}
|
|
1347
|
+
try {
|
|
1348
|
+
const appliedTags = await this.getAppliedTags(id);
|
|
1349
|
+
const results = await Promise.allSettled(
|
|
1350
|
+
appliedTags.rows.map(async (tagRow) => {
|
|
1351
|
+
const tagId = tagRow.id;
|
|
1352
|
+
await this.removeTagFromCard(id, tagId);
|
|
1353
|
+
})
|
|
1354
|
+
);
|
|
1355
|
+
results.forEach((result, index) => {
|
|
1356
|
+
if (result.status === "rejected") {
|
|
1357
|
+
const tagId = appliedTags.rows[index].id;
|
|
1358
|
+
logger.error(`Failed to remove card ${id} from tag ${tagId}: ${result.reason}`);
|
|
1359
|
+
}
|
|
1360
|
+
});
|
|
1361
|
+
} catch (error) {
|
|
1362
|
+
logger.error(`Error removing card ${id} from tags: ${error}`);
|
|
1363
|
+
}
|
|
1279
1364
|
return this.db.remove(doc);
|
|
1280
1365
|
}
|
|
1281
1366
|
async getCardDisplayableDataIDs(id) {
|
|
@@ -1459,23 +1544,9 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
1459
1544
|
////////////////////////////////////
|
|
1460
1545
|
getNavigationStrategy(id) {
|
|
1461
1546
|
logger.debug(`[courseDB] Getting navigation strategy: ${id}`);
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
name: "ELO",
|
|
1466
|
-
description: "ELO-based navigation strategy for ordering content by difficulty",
|
|
1467
|
-
implementingClass: "elo" /* ELO */,
|
|
1468
|
-
course: this.id,
|
|
1469
|
-
serializedData: ""
|
|
1470
|
-
// serde is a noop for ELO navigator.
|
|
1471
|
-
};
|
|
1472
|
-
return Promise.resolve(strategy);
|
|
1473
|
-
}
|
|
1474
|
-
getAllNavigationStrategies() {
|
|
1475
|
-
logger.debug("[courseDB] Returning hard-coded navigation strategies");
|
|
1476
|
-
const strategies = [
|
|
1477
|
-
{
|
|
1478
|
-
id: "ELO",
|
|
1547
|
+
if (id == "") {
|
|
1548
|
+
const strategy = {
|
|
1549
|
+
_id: "NAVIGATION_STRATEGY-ELO",
|
|
1479
1550
|
docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
|
|
1480
1551
|
name: "ELO",
|
|
1481
1552
|
description: "ELO-based navigation strategy for ordering content by difficulty",
|
|
@@ -1483,14 +1554,25 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
1483
1554
|
course: this.id,
|
|
1484
1555
|
serializedData: ""
|
|
1485
1556
|
// serde is a noop for ELO navigator.
|
|
1486
|
-
}
|
|
1487
|
-
|
|
1488
|
-
|
|
1557
|
+
};
|
|
1558
|
+
return Promise.resolve(strategy);
|
|
1559
|
+
} else {
|
|
1560
|
+
return this.db.get(id);
|
|
1561
|
+
}
|
|
1489
1562
|
}
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1563
|
+
async getAllNavigationStrategies() {
|
|
1564
|
+
const prefix = DocTypePrefixes["NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */];
|
|
1565
|
+
const result = await this.db.allDocs({
|
|
1566
|
+
startkey: prefix,
|
|
1567
|
+
endkey: `${prefix}\uFFF0`,
|
|
1568
|
+
include_docs: true
|
|
1569
|
+
});
|
|
1570
|
+
return result.rows.map((row) => row.doc);
|
|
1571
|
+
}
|
|
1572
|
+
async addNavigationStrategy(data) {
|
|
1573
|
+
logger.debug(`[courseDB] Adding navigation strategy: ${data._id}`);
|
|
1574
|
+
return this.db.put(data).then(() => {
|
|
1575
|
+
});
|
|
1494
1576
|
}
|
|
1495
1577
|
updateNavigationStrategy(id, data) {
|
|
1496
1578
|
logger.debug(`[courseDB] Updating navigation strategy: ${id}`);
|
|
@@ -1498,9 +1580,32 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
1498
1580
|
return Promise.resolve();
|
|
1499
1581
|
}
|
|
1500
1582
|
async surfaceNavigationStrategy() {
|
|
1583
|
+
try {
|
|
1584
|
+
const config = await this.getCourseConfig();
|
|
1585
|
+
if (config.defaultNavigationStrategyId) {
|
|
1586
|
+
try {
|
|
1587
|
+
const strategy = await this.getNavigationStrategy(config.defaultNavigationStrategyId);
|
|
1588
|
+
if (strategy) {
|
|
1589
|
+
logger.debug(`Surfacing strategy ${strategy.name} from course config`);
|
|
1590
|
+
return strategy;
|
|
1591
|
+
}
|
|
1592
|
+
} catch (e) {
|
|
1593
|
+
logger.warn(
|
|
1594
|
+
// @ts-expect-error tmp: defaultNavigationStrategyId property does not yet exist
|
|
1595
|
+
`Failed to load strategy '${config.defaultNavigationStrategyId}' specified in course config. Falling back to ELO.`,
|
|
1596
|
+
e
|
|
1597
|
+
);
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
} catch (e) {
|
|
1601
|
+
logger.warn(
|
|
1602
|
+
"Could not retrieve course config to determine navigation strategy. Falling back to ELO.",
|
|
1603
|
+
e
|
|
1604
|
+
);
|
|
1605
|
+
}
|
|
1501
1606
|
logger.warn(`Returning hard-coded default ELO navigator`);
|
|
1502
1607
|
const ret = {
|
|
1503
|
-
|
|
1608
|
+
_id: "NAVIGATION_STRATEGY-ELO",
|
|
1504
1609
|
docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
|
|
1505
1610
|
name: "ELO",
|
|
1506
1611
|
description: "ELO-based navigation strategy",
|
|
@@ -2964,17 +3069,21 @@ Currently logged-in as ${this._username}.`
|
|
|
2964
3069
|
} catch (e) {
|
|
2965
3070
|
const reason = e;
|
|
2966
3071
|
if (reason.status === 404) {
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
3072
|
+
try {
|
|
3073
|
+
const initCardHistory = {
|
|
3074
|
+
_id: cardHistoryID,
|
|
3075
|
+
cardID: record.cardID,
|
|
3076
|
+
courseID: record.courseID,
|
|
3077
|
+
records: [record],
|
|
3078
|
+
lapses: 0,
|
|
3079
|
+
streak: 0,
|
|
3080
|
+
bestInterval: 0
|
|
3081
|
+
};
|
|
3082
|
+
const putResult = await this.writeDB.put(initCardHistory);
|
|
3083
|
+
return { ...initCardHistory, _rev: putResult.rev };
|
|
3084
|
+
} catch (creationError) {
|
|
3085
|
+
throw new Error(`Failed to create CardHistory for ${cardHistoryID}. Reason: ${creationError}`);
|
|
3086
|
+
}
|
|
2978
3087
|
} else {
|
|
2979
3088
|
throw new Error(`putCardRecord failed because of:
|
|
2980
3089
|
name:${reason.name}
|
|
@@ -3754,16 +3863,20 @@ var init_courseDB2 = __esm({
|
|
|
3754
3863
|
async updateCardElo(cardId, _elo) {
|
|
3755
3864
|
return { ok: true, id: cardId, rev: "1-static" };
|
|
3756
3865
|
}
|
|
3757
|
-
async getNewCards(limit) {
|
|
3758
|
-
const
|
|
3759
|
-
return
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3866
|
+
async getNewCards(limit = 99) {
|
|
3867
|
+
const activeCards = await this.userDB.getActiveCards();
|
|
3868
|
+
return (await this.getCardsCenteredAtELO({ limit, elo: "user" }, (c) => {
|
|
3869
|
+
if (activeCards.some((ac) => c.cardID === ac.cardID)) {
|
|
3870
|
+
return false;
|
|
3871
|
+
} else {
|
|
3872
|
+
return true;
|
|
3873
|
+
}
|
|
3874
|
+
})).map((c) => {
|
|
3875
|
+
return {
|
|
3876
|
+
...c,
|
|
3877
|
+
status: "new"
|
|
3878
|
+
};
|
|
3879
|
+
});
|
|
3767
3880
|
}
|
|
3768
3881
|
async getCardsCenteredAtELO(options, filter) {
|
|
3769
3882
|
let targetElo = typeof options.elo === "number" ? options.elo : 1e3;
|
|
@@ -3944,7 +4057,7 @@ var init_courseDB2 = __esm({
|
|
|
3944
4057
|
// Navigation Strategy Manager implementation
|
|
3945
4058
|
async getNavigationStrategy(_id) {
|
|
3946
4059
|
return {
|
|
3947
|
-
|
|
4060
|
+
_id: "NAVIGATION_STRATEGY-ELO",
|
|
3948
4061
|
docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
|
|
3949
4062
|
name: "ELO",
|
|
3950
4063
|
description: "ELO-based navigation strategy",
|
|
@@ -4009,11 +4122,17 @@ var init_coursesDB = __esm({
|
|
|
4009
4122
|
this.manifests = manifests;
|
|
4010
4123
|
}
|
|
4011
4124
|
async getCourseConfig(courseId) {
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4125
|
+
const manifest = this.manifests[courseId];
|
|
4126
|
+
if (!manifest) {
|
|
4127
|
+
logger.warn(`Course manifest for ${courseId} not found`);
|
|
4128
|
+
throw new Error(`Course ${courseId} not found`);
|
|
4129
|
+
}
|
|
4130
|
+
if (manifest.courseConfig) {
|
|
4131
|
+
return manifest.courseConfig;
|
|
4132
|
+
} else {
|
|
4133
|
+
logger.warn(`Course config not found in manifest for course ${courseId}`);
|
|
4134
|
+
throw new Error(`Course config not found for course ${courseId}`);
|
|
4015
4135
|
}
|
|
4016
|
-
return {};
|
|
4017
4136
|
}
|
|
4018
4137
|
async getCourseList() {
|
|
4019
4138
|
return Object.keys(this.manifests).map(
|
|
@@ -4105,23 +4224,49 @@ var init_StaticDataLayerProvider = __esm({
|
|
|
4105
4224
|
config;
|
|
4106
4225
|
initialized = false;
|
|
4107
4226
|
courseUnpackers = /* @__PURE__ */ new Map();
|
|
4227
|
+
manifests = {};
|
|
4108
4228
|
constructor(config) {
|
|
4109
4229
|
this.config = {
|
|
4110
|
-
staticContentPath: config.staticContentPath || "/static-courses",
|
|
4111
4230
|
localStoragePrefix: config.localStoragePrefix || "skuilder-static",
|
|
4112
|
-
|
|
4231
|
+
rootManifest: config.rootManifest || { dependencies: {} },
|
|
4232
|
+
rootManifestUrl: config.rootManifestUrl || "/"
|
|
4113
4233
|
};
|
|
4114
4234
|
}
|
|
4235
|
+
async resolveCourseDependencies() {
|
|
4236
|
+
logger.info("[StaticDataLayerProvider] Starting course dependency resolution...");
|
|
4237
|
+
const rootManifest = this.config.rootManifest;
|
|
4238
|
+
for (const [courseName, courseUrl] of Object.entries(rootManifest.dependencies || {})) {
|
|
4239
|
+
try {
|
|
4240
|
+
logger.debug(`[StaticDataLayerProvider] Resolving dependency: ${courseName} from ${courseUrl}`);
|
|
4241
|
+
const courseManifestUrl = new URL(courseUrl, this.config.rootManifestUrl).href;
|
|
4242
|
+
const courseJsonResponse = await fetch(courseManifestUrl);
|
|
4243
|
+
if (!courseJsonResponse.ok) {
|
|
4244
|
+
throw new Error(`Failed to fetch course manifest for ${courseName}`);
|
|
4245
|
+
}
|
|
4246
|
+
const courseJson = await courseJsonResponse.json();
|
|
4247
|
+
if (courseJson.content && courseJson.content.manifest) {
|
|
4248
|
+
const baseUrl = new URL(".", courseManifestUrl).href;
|
|
4249
|
+
const finalManifestUrl = new URL(courseJson.content.manifest, courseManifestUrl).href;
|
|
4250
|
+
const finalManifestResponse = await fetch(finalManifestUrl);
|
|
4251
|
+
if (!finalManifestResponse.ok) {
|
|
4252
|
+
throw new Error(`Failed to fetch final content manifest for ${courseName} at ${finalManifestUrl}`);
|
|
4253
|
+
}
|
|
4254
|
+
const finalManifest = await finalManifestResponse.json();
|
|
4255
|
+
this.manifests[courseName] = finalManifest;
|
|
4256
|
+
const unpacker = new StaticDataUnpacker(finalManifest, baseUrl);
|
|
4257
|
+
this.courseUnpackers.set(courseName, unpacker);
|
|
4258
|
+
logger.info(`[StaticDataLayerProvider] Successfully resolved and prepared course: ${courseName}`);
|
|
4259
|
+
}
|
|
4260
|
+
} catch (e) {
|
|
4261
|
+
logger.error(`[StaticDataLayerProvider] Failed to resolve dependency ${courseName}:`, e);
|
|
4262
|
+
}
|
|
4263
|
+
}
|
|
4264
|
+
logger.info("[StaticDataLayerProvider] Course dependency resolution complete.");
|
|
4265
|
+
}
|
|
4115
4266
|
async initialize() {
|
|
4116
4267
|
if (this.initialized) return;
|
|
4117
4268
|
logger.info("Initializing static data layer provider");
|
|
4118
|
-
|
|
4119
|
-
const unpacker = new StaticDataUnpacker(
|
|
4120
|
-
manifest,
|
|
4121
|
-
`${this.config.staticContentPath}/${courseId}`
|
|
4122
|
-
);
|
|
4123
|
-
this.courseUnpackers.set(courseId, unpacker);
|
|
4124
|
-
}
|
|
4269
|
+
await this.resolveCourseDependencies();
|
|
4125
4270
|
this.initialized = true;
|
|
4126
4271
|
}
|
|
4127
4272
|
async teardown() {
|
|
@@ -4135,13 +4280,13 @@ var init_StaticDataLayerProvider = __esm({
|
|
|
4135
4280
|
getCourseDB(courseId) {
|
|
4136
4281
|
const unpacker = this.courseUnpackers.get(courseId);
|
|
4137
4282
|
if (!unpacker) {
|
|
4138
|
-
throw new Error(`Course ${courseId} not found in static data
|
|
4283
|
+
throw new Error(`Course ${courseId} not found or failed to initialize in static data layer.`);
|
|
4139
4284
|
}
|
|
4140
|
-
const manifest = this.
|
|
4285
|
+
const manifest = this.manifests[courseId];
|
|
4141
4286
|
return new StaticCourseDB(courseId, unpacker, this.getUserDB(), manifest);
|
|
4142
4287
|
}
|
|
4143
4288
|
getCoursesDB() {
|
|
4144
|
-
return new StaticCoursesDB(this.
|
|
4289
|
+
return new StaticCoursesDB(this.manifests);
|
|
4145
4290
|
}
|
|
4146
4291
|
async getClassroomDB(_classId, _type) {
|
|
4147
4292
|
throw new Error("Classrooms not supported in static mode");
|
|
@@ -4490,6 +4635,497 @@ module.exports = __toCommonJS(index_exports);
|
|
|
4490
4635
|
init_core();
|
|
4491
4636
|
init_courseLookupDB();
|
|
4492
4637
|
|
|
4638
|
+
// src/study/services/SrsService.ts
|
|
4639
|
+
var import_moment7 = __toESM(require("moment"));
|
|
4640
|
+
init_couch();
|
|
4641
|
+
|
|
4642
|
+
// src/study/SpacedRepetition.ts
|
|
4643
|
+
init_util();
|
|
4644
|
+
var import_moment6 = __toESM(require("moment"));
|
|
4645
|
+
init_logger();
|
|
4646
|
+
var duration = import_moment6.default.duration;
|
|
4647
|
+
function newInterval(user, cardHistory) {
|
|
4648
|
+
if (areQuestionRecords(cardHistory)) {
|
|
4649
|
+
return newQuestionInterval(user, cardHistory);
|
|
4650
|
+
} else {
|
|
4651
|
+
return 1e5;
|
|
4652
|
+
}
|
|
4653
|
+
}
|
|
4654
|
+
function newQuestionInterval(user, cardHistory) {
|
|
4655
|
+
const records = cardHistory.records;
|
|
4656
|
+
const currentAttempt = records[records.length - 1];
|
|
4657
|
+
const lastInterval = lastSuccessfulInterval(records);
|
|
4658
|
+
if (lastInterval > cardHistory.bestInterval) {
|
|
4659
|
+
cardHistory.bestInterval = lastInterval;
|
|
4660
|
+
void user.update(cardHistory._id, {
|
|
4661
|
+
bestInterval: lastInterval
|
|
4662
|
+
});
|
|
4663
|
+
}
|
|
4664
|
+
if (currentAttempt.isCorrect) {
|
|
4665
|
+
const skill = Math.min(1, Math.max(0, currentAttempt.performance));
|
|
4666
|
+
logger.debug(`Demontrated skill: ${skill}`);
|
|
4667
|
+
const interval = lastInterval * (0.75 + skill);
|
|
4668
|
+
cardHistory.lapses = getLapses(cardHistory.records);
|
|
4669
|
+
cardHistory.streak = getStreak(cardHistory.records);
|
|
4670
|
+
if (cardHistory.lapses && cardHistory.streak && cardHistory.bestInterval && (cardHistory.lapses >= 0 || cardHistory.streak >= 0)) {
|
|
4671
|
+
const ret = (cardHistory.lapses * interval + cardHistory.streak * cardHistory.bestInterval) / (cardHistory.lapses + cardHistory.streak);
|
|
4672
|
+
logger.debug(`Weighted average interval calculation:
|
|
4673
|
+
(${cardHistory.lapses} * ${interval} + ${cardHistory.streak} * ${cardHistory.bestInterval}) / (${cardHistory.lapses} + ${cardHistory.streak}) = ${ret}`);
|
|
4674
|
+
return ret;
|
|
4675
|
+
} else {
|
|
4676
|
+
return interval;
|
|
4677
|
+
}
|
|
4678
|
+
} else {
|
|
4679
|
+
return 0;
|
|
4680
|
+
}
|
|
4681
|
+
}
|
|
4682
|
+
function lastSuccessfulInterval(cardHistory) {
|
|
4683
|
+
for (let i = cardHistory.length - 1; i >= 1; i--) {
|
|
4684
|
+
if (cardHistory[i].priorAttemps === 0 && cardHistory[i].isCorrect) {
|
|
4685
|
+
const lastInterval = secondsBetween(cardHistory[i - 1].timeStamp, cardHistory[i].timeStamp);
|
|
4686
|
+
const ret = Math.max(lastInterval, 20 * 60 * 60);
|
|
4687
|
+
logger.debug(`Last interval w/ this card was: ${lastInterval}s, returning ${ret}s`);
|
|
4688
|
+
return ret;
|
|
4689
|
+
}
|
|
4690
|
+
}
|
|
4691
|
+
return getInitialInterval(cardHistory);
|
|
4692
|
+
}
|
|
4693
|
+
function getStreak(records) {
|
|
4694
|
+
let streak = 0;
|
|
4695
|
+
let index = records.length - 1;
|
|
4696
|
+
while (index >= 0 && records[index].isCorrect) {
|
|
4697
|
+
index--;
|
|
4698
|
+
streak++;
|
|
4699
|
+
}
|
|
4700
|
+
return streak;
|
|
4701
|
+
}
|
|
4702
|
+
function getLapses(records) {
|
|
4703
|
+
return records.filter((r) => r.isCorrect === false).length;
|
|
4704
|
+
}
|
|
4705
|
+
function getInitialInterval(cardHistory) {
|
|
4706
|
+
logger.warn(`history of length: ${cardHistory.length} ignored!`);
|
|
4707
|
+
return 60 * 60 * 24 * 3;
|
|
4708
|
+
}
|
|
4709
|
+
function secondsBetween(start, end) {
|
|
4710
|
+
start = (0, import_moment6.default)(start);
|
|
4711
|
+
end = (0, import_moment6.default)(end);
|
|
4712
|
+
const ret = duration(end.diff(start)).asSeconds();
|
|
4713
|
+
return ret;
|
|
4714
|
+
}
|
|
4715
|
+
|
|
4716
|
+
// src/study/services/SrsService.ts
|
|
4717
|
+
init_logger();
|
|
4718
|
+
var SrsService = class {
|
|
4719
|
+
user;
|
|
4720
|
+
constructor(user) {
|
|
4721
|
+
this.user = user;
|
|
4722
|
+
}
|
|
4723
|
+
/**
|
|
4724
|
+
* Calculates the next review time for a card based on its history and
|
|
4725
|
+
* schedules it in the user's database.
|
|
4726
|
+
* @param history The full history of the card.
|
|
4727
|
+
* @param item The study session item, used to determine if a previous review needs to be cleared.
|
|
4728
|
+
*/
|
|
4729
|
+
async scheduleReview(history, item) {
|
|
4730
|
+
const nextInterval = newInterval(this.user, history);
|
|
4731
|
+
const nextReviewTime = import_moment7.default.utc().add(nextInterval, "seconds");
|
|
4732
|
+
if (isReview(item)) {
|
|
4733
|
+
logger.info(`[SrsService] Removing previously scheduled review for: ${item.cardID}`);
|
|
4734
|
+
void this.user.removeScheduledCardReview(item.reviewID);
|
|
4735
|
+
}
|
|
4736
|
+
void this.user.scheduleCardReview({
|
|
4737
|
+
user: this.user.getUsername(),
|
|
4738
|
+
course_id: history.courseID,
|
|
4739
|
+
card_id: history.cardID,
|
|
4740
|
+
time: nextReviewTime,
|
|
4741
|
+
scheduledFor: item.contentSourceType,
|
|
4742
|
+
schedulingAgentId: item.contentSourceID
|
|
4743
|
+
});
|
|
4744
|
+
}
|
|
4745
|
+
};
|
|
4746
|
+
|
|
4747
|
+
// src/study/services/EloService.ts
|
|
4748
|
+
var import_common15 = require("@vue-skuilder/common");
|
|
4749
|
+
init_logger();
|
|
4750
|
+
var EloService = class {
|
|
4751
|
+
dataLayer;
|
|
4752
|
+
user;
|
|
4753
|
+
constructor(dataLayer, user) {
|
|
4754
|
+
this.dataLayer = dataLayer;
|
|
4755
|
+
this.user = user;
|
|
4756
|
+
}
|
|
4757
|
+
/**
|
|
4758
|
+
* Updates both user and card ELO ratings based on user performance.
|
|
4759
|
+
* @param userScore Score between 0-1 representing user performance
|
|
4760
|
+
* @param course_id Course identifier
|
|
4761
|
+
* @param card_id Card identifier
|
|
4762
|
+
* @param userCourseRegDoc User's course registration document (will be mutated)
|
|
4763
|
+
* @param currentCard Current card session record
|
|
4764
|
+
* @param k Optional K-factor for ELO calculation
|
|
4765
|
+
*/
|
|
4766
|
+
async updateUserAndCardElo(userScore, course_id, card_id, userCourseRegDoc, currentCard, k) {
|
|
4767
|
+
if (k) {
|
|
4768
|
+
logger.warn(`k value interpretation not currently implemented`);
|
|
4769
|
+
}
|
|
4770
|
+
const courseDB = this.dataLayer.getCourseDB(currentCard.card.course_id);
|
|
4771
|
+
const userElo = (0, import_common15.toCourseElo)(userCourseRegDoc.courses.find((c) => c.courseID === course_id).elo);
|
|
4772
|
+
const cardElo = (await courseDB.getCardEloData([currentCard.card.card_id]))[0];
|
|
4773
|
+
if (cardElo && userElo) {
|
|
4774
|
+
const eloUpdate = (0, import_common15.adjustCourseScores)(userElo, cardElo, userScore);
|
|
4775
|
+
userCourseRegDoc.courses.find((c) => c.courseID === course_id).elo = eloUpdate.userElo;
|
|
4776
|
+
const results = await Promise.allSettled([
|
|
4777
|
+
this.user.updateUserElo(course_id, eloUpdate.userElo),
|
|
4778
|
+
courseDB.updateCardElo(card_id, eloUpdate.cardElo)
|
|
4779
|
+
]);
|
|
4780
|
+
const userEloStatus = results[0].status === "fulfilled";
|
|
4781
|
+
const cardEloStatus = results[1].status === "fulfilled";
|
|
4782
|
+
if (userEloStatus && cardEloStatus) {
|
|
4783
|
+
const user = results[0].value;
|
|
4784
|
+
const card = results[1].value;
|
|
4785
|
+
if (user.ok && card && card.ok) {
|
|
4786
|
+
logger.info(
|
|
4787
|
+
`[EloService] Updated ELOS:
|
|
4788
|
+
User: ${JSON.stringify(eloUpdate.userElo)})
|
|
4789
|
+
Card: ${JSON.stringify(eloUpdate.cardElo)})
|
|
4790
|
+
`
|
|
4791
|
+
);
|
|
4792
|
+
}
|
|
4793
|
+
} else {
|
|
4794
|
+
logger.warn(
|
|
4795
|
+
`[EloService] Partial ELO update:
|
|
4796
|
+
User ELO update: ${userEloStatus ? "SUCCESS" : "FAILED"}
|
|
4797
|
+
Card ELO update: ${cardEloStatus ? "SUCCESS" : "FAILED"}`
|
|
4798
|
+
);
|
|
4799
|
+
if (!userEloStatus && results[0].status === "rejected") {
|
|
4800
|
+
logger.error("[EloService] User ELO update error:", results[0].reason);
|
|
4801
|
+
}
|
|
4802
|
+
if (!cardEloStatus && results[1].status === "rejected") {
|
|
4803
|
+
logger.error("[EloService] Card ELO update error:", results[1].reason);
|
|
4804
|
+
}
|
|
4805
|
+
}
|
|
4806
|
+
}
|
|
4807
|
+
}
|
|
4808
|
+
};
|
|
4809
|
+
|
|
4810
|
+
// src/study/services/ResponseProcessor.ts
|
|
4811
|
+
init_core();
|
|
4812
|
+
init_logger();
|
|
4813
|
+
var ResponseProcessor = class {
|
|
4814
|
+
srsService;
|
|
4815
|
+
eloService;
|
|
4816
|
+
constructor(srsService, eloService) {
|
|
4817
|
+
this.srsService = srsService;
|
|
4818
|
+
this.eloService = eloService;
|
|
4819
|
+
}
|
|
4820
|
+
/**
|
|
4821
|
+
* Processes a user's response to a card, handling SRS scheduling and ELO updates.
|
|
4822
|
+
* @param cardRecord User's response record
|
|
4823
|
+
* @param cardHistory Promise resolving to the card's history
|
|
4824
|
+
* @param studySessionItem Current study session item
|
|
4825
|
+
* @param courseRegistrationDoc User's course registration (for ELO updates)
|
|
4826
|
+
* @param currentCard Current study session record
|
|
4827
|
+
* @param courseId Course identifier
|
|
4828
|
+
* @param cardId Card identifier
|
|
4829
|
+
* @param maxAttemptsPerView Maximum attempts allowed per view
|
|
4830
|
+
* @param maxSessionViews Maximum session views for this card
|
|
4831
|
+
* @param sessionViews Current number of session views
|
|
4832
|
+
* @returns ResponseResult with navigation and UI instructions
|
|
4833
|
+
*/
|
|
4834
|
+
async processResponse(cardRecord, cardHistory, studySessionItem, courseRegistrationDoc, currentCard, courseId, cardId, maxAttemptsPerView, maxSessionViews, sessionViews) {
|
|
4835
|
+
if (!isQuestionRecord(cardRecord)) {
|
|
4836
|
+
return {
|
|
4837
|
+
nextCardAction: "dismiss-success",
|
|
4838
|
+
shouldLoadNextCard: true,
|
|
4839
|
+
isCorrect: true,
|
|
4840
|
+
// non-question records are considered "correct"
|
|
4841
|
+
shouldClearFeedbackShadow: true
|
|
4842
|
+
};
|
|
4843
|
+
}
|
|
4844
|
+
const history = await cardHistory;
|
|
4845
|
+
if (cardRecord.isCorrect) {
|
|
4846
|
+
return this.processCorrectResponse(
|
|
4847
|
+
cardRecord,
|
|
4848
|
+
history,
|
|
4849
|
+
studySessionItem,
|
|
4850
|
+
courseRegistrationDoc,
|
|
4851
|
+
currentCard,
|
|
4852
|
+
courseId,
|
|
4853
|
+
cardId
|
|
4854
|
+
);
|
|
4855
|
+
} else {
|
|
4856
|
+
return this.processIncorrectResponse(
|
|
4857
|
+
cardRecord,
|
|
4858
|
+
history,
|
|
4859
|
+
courseRegistrationDoc,
|
|
4860
|
+
currentCard,
|
|
4861
|
+
courseId,
|
|
4862
|
+
cardId,
|
|
4863
|
+
maxAttemptsPerView,
|
|
4864
|
+
maxSessionViews,
|
|
4865
|
+
sessionViews
|
|
4866
|
+
);
|
|
4867
|
+
}
|
|
4868
|
+
}
|
|
4869
|
+
/**
|
|
4870
|
+
* Handles processing for correct responses: SRS scheduling and ELO updates.
|
|
4871
|
+
*/
|
|
4872
|
+
processCorrectResponse(cardRecord, history, studySessionItem, courseRegistrationDoc, currentCard, courseId, cardId) {
|
|
4873
|
+
if (cardRecord.priorAttemps === 0) {
|
|
4874
|
+
void this.srsService.scheduleReview(history, studySessionItem);
|
|
4875
|
+
if (history.records.length === 1) {
|
|
4876
|
+
const userScore = 0.5 + cardRecord.performance / 2;
|
|
4877
|
+
void this.eloService.updateUserAndCardElo(
|
|
4878
|
+
userScore,
|
|
4879
|
+
courseId,
|
|
4880
|
+
cardId,
|
|
4881
|
+
courseRegistrationDoc,
|
|
4882
|
+
currentCard
|
|
4883
|
+
);
|
|
4884
|
+
} else {
|
|
4885
|
+
const k = Math.ceil(32 / history.records.length);
|
|
4886
|
+
const userScore = 0.5 + cardRecord.performance / 2;
|
|
4887
|
+
void this.eloService.updateUserAndCardElo(
|
|
4888
|
+
userScore,
|
|
4889
|
+
courseId,
|
|
4890
|
+
cardId,
|
|
4891
|
+
courseRegistrationDoc,
|
|
4892
|
+
currentCard,
|
|
4893
|
+
k
|
|
4894
|
+
);
|
|
4895
|
+
}
|
|
4896
|
+
logger.info(
|
|
4897
|
+
"[ResponseProcessor] Processed correct response with SRS scheduling and ELO update"
|
|
4898
|
+
);
|
|
4899
|
+
return {
|
|
4900
|
+
nextCardAction: "dismiss-success",
|
|
4901
|
+
shouldLoadNextCard: true,
|
|
4902
|
+
isCorrect: true,
|
|
4903
|
+
performanceScore: cardRecord.performance,
|
|
4904
|
+
shouldClearFeedbackShadow: true
|
|
4905
|
+
};
|
|
4906
|
+
} else {
|
|
4907
|
+
logger.info(
|
|
4908
|
+
"[ResponseProcessor] Processed correct response (retry attempt - no scheduling/ELO)"
|
|
4909
|
+
);
|
|
4910
|
+
return {
|
|
4911
|
+
nextCardAction: "marked-failed",
|
|
4912
|
+
shouldLoadNextCard: true,
|
|
4913
|
+
isCorrect: true,
|
|
4914
|
+
performanceScore: cardRecord.performance,
|
|
4915
|
+
shouldClearFeedbackShadow: true
|
|
4916
|
+
};
|
|
4917
|
+
}
|
|
4918
|
+
}
|
|
4919
|
+
/**
|
|
4920
|
+
* Handles processing for incorrect responses: ELO updates only.
|
|
4921
|
+
*/
|
|
4922
|
+
processIncorrectResponse(cardRecord, history, courseRegistrationDoc, currentCard, courseId, cardId, maxAttemptsPerView, maxSessionViews, sessionViews) {
|
|
4923
|
+
if (history.records.length !== 1 && cardRecord.priorAttemps === 0) {
|
|
4924
|
+
void this.eloService.updateUserAndCardElo(
|
|
4925
|
+
0,
|
|
4926
|
+
// Failed response = 0 score
|
|
4927
|
+
courseId,
|
|
4928
|
+
cardId,
|
|
4929
|
+
courseRegistrationDoc,
|
|
4930
|
+
currentCard
|
|
4931
|
+
);
|
|
4932
|
+
logger.info("[ResponseProcessor] Processed incorrect response with ELO update");
|
|
4933
|
+
} else {
|
|
4934
|
+
logger.info("[ResponseProcessor] Processed incorrect response (no ELO update needed)");
|
|
4935
|
+
}
|
|
4936
|
+
if (currentCard.records.length >= maxAttemptsPerView) {
|
|
4937
|
+
if (sessionViews >= maxSessionViews) {
|
|
4938
|
+
void this.eloService.updateUserAndCardElo(
|
|
4939
|
+
0,
|
|
4940
|
+
courseId,
|
|
4941
|
+
cardId,
|
|
4942
|
+
courseRegistrationDoc,
|
|
4943
|
+
currentCard
|
|
4944
|
+
);
|
|
4945
|
+
return {
|
|
4946
|
+
nextCardAction: "dismiss-failed",
|
|
4947
|
+
shouldLoadNextCard: true,
|
|
4948
|
+
isCorrect: false,
|
|
4949
|
+
shouldClearFeedbackShadow: true
|
|
4950
|
+
};
|
|
4951
|
+
} else {
|
|
4952
|
+
return {
|
|
4953
|
+
nextCardAction: "marked-failed",
|
|
4954
|
+
shouldLoadNextCard: true,
|
|
4955
|
+
isCorrect: false,
|
|
4956
|
+
shouldClearFeedbackShadow: true
|
|
4957
|
+
};
|
|
4958
|
+
}
|
|
4959
|
+
} else {
|
|
4960
|
+
return {
|
|
4961
|
+
nextCardAction: "none",
|
|
4962
|
+
shouldLoadNextCard: false,
|
|
4963
|
+
isCorrect: false,
|
|
4964
|
+
shouldClearFeedbackShadow: true
|
|
4965
|
+
};
|
|
4966
|
+
}
|
|
4967
|
+
}
|
|
4968
|
+
};
|
|
4969
|
+
|
|
4970
|
+
// src/study/services/CardHydrationService.ts
|
|
4971
|
+
var import_common16 = require("@vue-skuilder/common");
|
|
4972
|
+
init_logger();
|
|
4973
|
+
|
|
4974
|
+
// src/study/ItemQueue.ts
|
|
4975
|
+
var ItemQueue = class {
|
|
4976
|
+
q = [];
|
|
4977
|
+
seenCardIds = [];
|
|
4978
|
+
_dequeueCount = 0;
|
|
4979
|
+
get dequeueCount() {
|
|
4980
|
+
return this._dequeueCount;
|
|
4981
|
+
}
|
|
4982
|
+
add(item, cardId) {
|
|
4983
|
+
if (this.seenCardIds.find((d) => d === cardId)) {
|
|
4984
|
+
return;
|
|
4985
|
+
}
|
|
4986
|
+
this.seenCardIds.push(cardId);
|
|
4987
|
+
this.q.push(item);
|
|
4988
|
+
}
|
|
4989
|
+
addAll(items, cardIdExtractor) {
|
|
4990
|
+
items.forEach((i) => this.add(i, cardIdExtractor(i)));
|
|
4991
|
+
}
|
|
4992
|
+
get length() {
|
|
4993
|
+
return this.q.length;
|
|
4994
|
+
}
|
|
4995
|
+
peek(index) {
|
|
4996
|
+
return this.q[index];
|
|
4997
|
+
}
|
|
4998
|
+
dequeue(cardIdExtractor) {
|
|
4999
|
+
if (this.q.length !== 0) {
|
|
5000
|
+
this._dequeueCount++;
|
|
5001
|
+
const item = this.q.splice(0, 1)[0];
|
|
5002
|
+
if (cardIdExtractor) {
|
|
5003
|
+
const cardId = cardIdExtractor(item);
|
|
5004
|
+
const index = this.seenCardIds.indexOf(cardId);
|
|
5005
|
+
if (index > -1) {
|
|
5006
|
+
this.seenCardIds.splice(index, 1);
|
|
5007
|
+
}
|
|
5008
|
+
}
|
|
5009
|
+
return item;
|
|
5010
|
+
} else {
|
|
5011
|
+
return null;
|
|
5012
|
+
}
|
|
5013
|
+
}
|
|
5014
|
+
get toString() {
|
|
5015
|
+
return `${typeof this.q[0]}:
|
|
5016
|
+
` + this.q.map((i) => ` ${i.courseID}+${i.cardID}: ${i.status}`).join("\n");
|
|
5017
|
+
}
|
|
5018
|
+
};
|
|
5019
|
+
|
|
5020
|
+
// src/study/services/CardHydrationService.ts
|
|
5021
|
+
var CardHydrationService = class {
|
|
5022
|
+
constructor(getViewComponent, getCourseDB3, selectNextItemToHydrate, removeItemFromQueue, hasAvailableCards) {
|
|
5023
|
+
this.getViewComponent = getViewComponent;
|
|
5024
|
+
this.getCourseDB = getCourseDB3;
|
|
5025
|
+
this.selectNextItemToHydrate = selectNextItemToHydrate;
|
|
5026
|
+
this.removeItemFromQueue = removeItemFromQueue;
|
|
5027
|
+
this.hasAvailableCards = hasAvailableCards;
|
|
5028
|
+
}
|
|
5029
|
+
hydratedQ = new ItemQueue();
|
|
5030
|
+
failedCardCache = /* @__PURE__ */ new Map();
|
|
5031
|
+
hydrationInProgress = false;
|
|
5032
|
+
BUFFER_SIZE = 5;
|
|
5033
|
+
/**
|
|
5034
|
+
* Get the next hydrated card from the queue.
|
|
5035
|
+
* @returns Hydrated card or null if none available
|
|
5036
|
+
*/
|
|
5037
|
+
dequeueHydratedCard() {
|
|
5038
|
+
return this.hydratedQ.dequeue((item) => item.item.cardID);
|
|
5039
|
+
}
|
|
5040
|
+
/**
|
|
5041
|
+
* Check if hydration should be triggered and start background hydration if needed.
|
|
5042
|
+
*/
|
|
5043
|
+
async ensureHydratedCards() {
|
|
5044
|
+
if (this.hydratedQ.length < 3) {
|
|
5045
|
+
void this.fillHydratedQueue();
|
|
5046
|
+
}
|
|
5047
|
+
}
|
|
5048
|
+
/**
|
|
5049
|
+
* Wait for a hydrated card to become available.
|
|
5050
|
+
* @returns Promise that resolves to a hydrated card or null
|
|
5051
|
+
*/
|
|
5052
|
+
async waitForHydratedCard() {
|
|
5053
|
+
if (this.hydratedQ.length === 0 && this.hasAvailableCards()) {
|
|
5054
|
+
void this.fillHydratedQueue();
|
|
5055
|
+
}
|
|
5056
|
+
while (this.hydratedQ.length === 0 && this.hasAvailableCards()) {
|
|
5057
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
5058
|
+
}
|
|
5059
|
+
return this.dequeueHydratedCard();
|
|
5060
|
+
}
|
|
5061
|
+
/**
|
|
5062
|
+
* Get current hydrated queue length.
|
|
5063
|
+
*/
|
|
5064
|
+
get hydratedCount() {
|
|
5065
|
+
return this.hydratedQ.length;
|
|
5066
|
+
}
|
|
5067
|
+
/**
|
|
5068
|
+
* Fill the hydrated queue up to BUFFER_SIZE with pre-fetched cards.
|
|
5069
|
+
*/
|
|
5070
|
+
async fillHydratedQueue() {
|
|
5071
|
+
if (this.hydrationInProgress) {
|
|
5072
|
+
return;
|
|
5073
|
+
}
|
|
5074
|
+
this.hydrationInProgress = true;
|
|
5075
|
+
try {
|
|
5076
|
+
while (this.hydratedQ.length < this.BUFFER_SIZE) {
|
|
5077
|
+
const nextItem = this.selectNextItemToHydrate();
|
|
5078
|
+
if (!nextItem) {
|
|
5079
|
+
return;
|
|
5080
|
+
}
|
|
5081
|
+
try {
|
|
5082
|
+
if (this.failedCardCache.has(nextItem.cardID)) {
|
|
5083
|
+
const cachedCard = this.failedCardCache.get(nextItem.cardID);
|
|
5084
|
+
this.hydratedQ.add(cachedCard, cachedCard.item.cardID);
|
|
5085
|
+
this.failedCardCache.delete(nextItem.cardID);
|
|
5086
|
+
} else {
|
|
5087
|
+
const courseDB = this.getCourseDB(nextItem.courseID);
|
|
5088
|
+
const cardData = await courseDB.getCourseDoc(nextItem.cardID);
|
|
5089
|
+
if (!(0, import_common16.isCourseElo)(cardData.elo)) {
|
|
5090
|
+
cardData.elo = (0, import_common16.toCourseElo)(cardData.elo);
|
|
5091
|
+
}
|
|
5092
|
+
const view = this.getViewComponent(cardData.id_view);
|
|
5093
|
+
const dataDocs = await Promise.all(
|
|
5094
|
+
cardData.id_displayable_data.map(
|
|
5095
|
+
(id) => courseDB.getCourseDoc(id, {
|
|
5096
|
+
attachments: true,
|
|
5097
|
+
binary: true
|
|
5098
|
+
})
|
|
5099
|
+
)
|
|
5100
|
+
);
|
|
5101
|
+
const data = dataDocs.map(import_common16.displayableDataToViewData).reverse();
|
|
5102
|
+
this.hydratedQ.add(
|
|
5103
|
+
{
|
|
5104
|
+
item: nextItem,
|
|
5105
|
+
view,
|
|
5106
|
+
data
|
|
5107
|
+
},
|
|
5108
|
+
nextItem.cardID
|
|
5109
|
+
);
|
|
5110
|
+
}
|
|
5111
|
+
} catch (e) {
|
|
5112
|
+
logger.error(`Error hydrating card ${nextItem.cardID}:`, e);
|
|
5113
|
+
} finally {
|
|
5114
|
+
this.removeItemFromQueue(nextItem);
|
|
5115
|
+
}
|
|
5116
|
+
}
|
|
5117
|
+
} finally {
|
|
5118
|
+
this.hydrationInProgress = false;
|
|
5119
|
+
}
|
|
5120
|
+
}
|
|
5121
|
+
/**
|
|
5122
|
+
* Cache a failed card for quick re-access.
|
|
5123
|
+
*/
|
|
5124
|
+
cacheFailedCard(card) {
|
|
5125
|
+
this.failedCardCache.set(card.item.cardID, card);
|
|
5126
|
+
}
|
|
5127
|
+
};
|
|
5128
|
+
|
|
4493
5129
|
// src/study/SessionController.ts
|
|
4494
5130
|
init_couch();
|
|
4495
5131
|
|
|
@@ -5917,61 +6553,27 @@ init_dataDirectory();
|
|
|
5917
6553
|
init_tuiLogger();
|
|
5918
6554
|
|
|
5919
6555
|
// src/study/SessionController.ts
|
|
5920
|
-
var import_common15 = require("@vue-skuilder/common");
|
|
5921
6556
|
function randomInt(min, max) {
|
|
5922
6557
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
5923
6558
|
}
|
|
5924
|
-
var ItemQueue = class {
|
|
5925
|
-
q = [];
|
|
5926
|
-
seenCardIds = [];
|
|
5927
|
-
_dequeueCount = 0;
|
|
5928
|
-
get dequeueCount() {
|
|
5929
|
-
return this._dequeueCount;
|
|
5930
|
-
}
|
|
5931
|
-
add(item, cardId) {
|
|
5932
|
-
if (this.seenCardIds.find((d) => d === cardId)) {
|
|
5933
|
-
return;
|
|
5934
|
-
}
|
|
5935
|
-
this.seenCardIds.push(cardId);
|
|
5936
|
-
this.q.push(item);
|
|
5937
|
-
}
|
|
5938
|
-
addAll(items, cardIdExtractor) {
|
|
5939
|
-
items.forEach((i) => this.add(i, cardIdExtractor(i)));
|
|
5940
|
-
}
|
|
5941
|
-
get length() {
|
|
5942
|
-
return this.q.length;
|
|
5943
|
-
}
|
|
5944
|
-
peek(index) {
|
|
5945
|
-
return this.q[index];
|
|
5946
|
-
}
|
|
5947
|
-
dequeue() {
|
|
5948
|
-
if (this.q.length !== 0) {
|
|
5949
|
-
this._dequeueCount++;
|
|
5950
|
-
return this.q.splice(0, 1)[0];
|
|
5951
|
-
} else {
|
|
5952
|
-
return null;
|
|
5953
|
-
}
|
|
5954
|
-
}
|
|
5955
|
-
get toString() {
|
|
5956
|
-
return `${typeof this.q[0]}:
|
|
5957
|
-
` + this.q.map((i) => ` ${i.courseID}+${i.cardID}: ${i.status}`).join("\n");
|
|
5958
|
-
}
|
|
5959
|
-
};
|
|
5960
6559
|
var SessionController = class extends Loggable {
|
|
5961
6560
|
_className = "SessionController";
|
|
6561
|
+
services;
|
|
6562
|
+
srsService;
|
|
6563
|
+
eloService;
|
|
6564
|
+
hydrationService;
|
|
5962
6565
|
sources;
|
|
5963
|
-
dataLayer
|
|
5964
|
-
getViewComponent;
|
|
6566
|
+
// dataLayer and getViewComponent now injected into CardHydrationService
|
|
5965
6567
|
_sessionRecord = [];
|
|
5966
6568
|
set sessionRecord(r) {
|
|
5967
6569
|
this._sessionRecord = r;
|
|
5968
6570
|
}
|
|
6571
|
+
// Session card stores
|
|
6572
|
+
_currentCard = null;
|
|
5969
6573
|
reviewQ = new ItemQueue();
|
|
5970
6574
|
newQ = new ItemQueue();
|
|
5971
6575
|
failedQ = new ItemQueue();
|
|
5972
|
-
|
|
5973
|
-
_currentCard = null;
|
|
5974
|
-
hydration_in_progress = false;
|
|
6576
|
+
// END Session card stores
|
|
5975
6577
|
startTime;
|
|
5976
6578
|
endTime;
|
|
5977
6579
|
_secondsRemaining;
|
|
@@ -5991,19 +6593,29 @@ var SessionController = class extends Loggable {
|
|
|
5991
6593
|
*/
|
|
5992
6594
|
constructor(sources, time, dataLayer, getViewComponent) {
|
|
5993
6595
|
super();
|
|
6596
|
+
this.srsService = new SrsService(dataLayer.getUserDB());
|
|
6597
|
+
this.eloService = new EloService(dataLayer, dataLayer.getUserDB());
|
|
6598
|
+
this.hydrationService = new CardHydrationService(
|
|
6599
|
+
getViewComponent,
|
|
6600
|
+
(courseId) => dataLayer.getCourseDB(courseId),
|
|
6601
|
+
() => this._selectNextItemToHydrate(),
|
|
6602
|
+
(item) => this.removeItemFromQueue(item),
|
|
6603
|
+
() => this.hasAvailableCards()
|
|
6604
|
+
);
|
|
6605
|
+
this.services = {
|
|
6606
|
+
response: new ResponseProcessor(this.srsService, this.eloService)
|
|
6607
|
+
};
|
|
5994
6608
|
this.sources = sources;
|
|
5995
6609
|
this.startTime = /* @__PURE__ */ new Date();
|
|
5996
6610
|
this._secondsRemaining = time;
|
|
5997
6611
|
this.endTime = new Date(this.startTime.valueOf() + 1e3 * this._secondsRemaining);
|
|
5998
|
-
this.dataLayer = dataLayer;
|
|
5999
|
-
this.getViewComponent = getViewComponent;
|
|
6000
6612
|
this.log(`Session constructed:
|
|
6001
6613
|
startTime: ${this.startTime}
|
|
6002
6614
|
endTime: ${this.endTime}`);
|
|
6003
6615
|
}
|
|
6004
6616
|
tick() {
|
|
6005
6617
|
this._secondsRemaining = Math.floor((this.endTime.valueOf() - Date.now()) / 1e3);
|
|
6006
|
-
if (this._secondsRemaining
|
|
6618
|
+
if (this._secondsRemaining <= 0) {
|
|
6007
6619
|
clearInterval(this._intervalHandle);
|
|
6008
6620
|
}
|
|
6009
6621
|
}
|
|
@@ -6047,7 +6659,7 @@ var SessionController = class extends Loggable {
|
|
|
6047
6659
|
} catch (e) {
|
|
6048
6660
|
this.error("Error preparing study session:", e);
|
|
6049
6661
|
}
|
|
6050
|
-
await this.
|
|
6662
|
+
await this.hydrationService.ensureHydratedCards();
|
|
6051
6663
|
this._intervalHandle = setInterval(() => {
|
|
6052
6664
|
this.tick();
|
|
6053
6665
|
}, 1e3);
|
|
@@ -6108,7 +6720,7 @@ var SessionController = class extends Loggable {
|
|
|
6108
6720
|
}
|
|
6109
6721
|
}
|
|
6110
6722
|
}
|
|
6111
|
-
_selectNextItemToHydrate(
|
|
6723
|
+
_selectNextItemToHydrate() {
|
|
6112
6724
|
const choice = Math.random();
|
|
6113
6725
|
let newBound = 0.1;
|
|
6114
6726
|
let reviewBound = 0.75;
|
|
@@ -6141,9 +6753,6 @@ var SessionController = class extends Loggable {
|
|
|
6141
6753
|
newBound = 0.01;
|
|
6142
6754
|
reviewBound = 0.1;
|
|
6143
6755
|
}
|
|
6144
|
-
if (this.failedQ.length === 1 && action === "marked-failed") {
|
|
6145
|
-
reviewBound = 1;
|
|
6146
|
-
}
|
|
6147
6756
|
if (this.failedQ.length === 0) {
|
|
6148
6757
|
reviewBound = 1;
|
|
6149
6758
|
}
|
|
@@ -6163,36 +6772,73 @@ var SessionController = class extends Loggable {
|
|
|
6163
6772
|
}
|
|
6164
6773
|
async nextCard(action = "dismiss-success") {
|
|
6165
6774
|
this.dismissCurrentCard(action);
|
|
6166
|
-
|
|
6775
|
+
if (this._secondsRemaining <= 0 && this.failedQ.length === 0) {
|
|
6776
|
+
this._currentCard = null;
|
|
6777
|
+
return null;
|
|
6778
|
+
}
|
|
6779
|
+
let card = this.hydrationService.dequeueHydratedCard();
|
|
6167
6780
|
if (!card && this.hasAvailableCards()) {
|
|
6168
|
-
|
|
6169
|
-
card = await this.nextHydratedCard();
|
|
6781
|
+
card = await this.hydrationService.waitForHydratedCard();
|
|
6170
6782
|
}
|
|
6171
|
-
|
|
6172
|
-
|
|
6783
|
+
await this.hydrationService.ensureHydratedCards();
|
|
6784
|
+
if (card) {
|
|
6785
|
+
this._currentCard = card;
|
|
6786
|
+
} else {
|
|
6787
|
+
this._currentCard = null;
|
|
6173
6788
|
}
|
|
6174
6789
|
return card;
|
|
6175
6790
|
}
|
|
6791
|
+
/**
|
|
6792
|
+
* Public API for processing user responses to cards.
|
|
6793
|
+
* @param cardRecord User's response record
|
|
6794
|
+
* @param cardHistory Promise resolving to the card's history
|
|
6795
|
+
* @param courseRegistrationDoc User's course registration document
|
|
6796
|
+
* @param currentCard Current study session record
|
|
6797
|
+
* @param courseId Course identifier
|
|
6798
|
+
* @param cardId Card identifier
|
|
6799
|
+
* @param maxAttemptsPerView Maximum attempts allowed per view
|
|
6800
|
+
* @param maxSessionViews Maximum session views for this card
|
|
6801
|
+
* @param sessionViews Current number of session views
|
|
6802
|
+
* @returns ResponseResult with navigation and UI instructions
|
|
6803
|
+
*/
|
|
6804
|
+
async submitResponse(cardRecord, cardHistory, courseRegistrationDoc, currentCard, courseId, cardId, maxAttemptsPerView, maxSessionViews, sessionViews) {
|
|
6805
|
+
const studySessionItem = {
|
|
6806
|
+
...currentCard.item
|
|
6807
|
+
};
|
|
6808
|
+
return await this.services.response.processResponse(
|
|
6809
|
+
cardRecord,
|
|
6810
|
+
cardHistory,
|
|
6811
|
+
studySessionItem,
|
|
6812
|
+
courseRegistrationDoc,
|
|
6813
|
+
currentCard,
|
|
6814
|
+
courseId,
|
|
6815
|
+
cardId,
|
|
6816
|
+
maxAttemptsPerView,
|
|
6817
|
+
maxSessionViews,
|
|
6818
|
+
sessionViews
|
|
6819
|
+
);
|
|
6820
|
+
}
|
|
6176
6821
|
dismissCurrentCard(action = "dismiss-success") {
|
|
6177
6822
|
if (this._currentCard) {
|
|
6178
6823
|
if (action === "dismiss-success") {
|
|
6179
6824
|
} else if (action === "marked-failed") {
|
|
6825
|
+
this.hydrationService.cacheFailedCard(this._currentCard);
|
|
6180
6826
|
let failedItem;
|
|
6181
|
-
if (isReview(this._currentCard)) {
|
|
6827
|
+
if (isReview(this._currentCard.item)) {
|
|
6182
6828
|
failedItem = {
|
|
6183
|
-
cardID: this._currentCard.cardID,
|
|
6184
|
-
courseID: this._currentCard.courseID,
|
|
6185
|
-
contentSourceID: this._currentCard.contentSourceID,
|
|
6186
|
-
contentSourceType: this._currentCard.contentSourceType,
|
|
6829
|
+
cardID: this._currentCard.item.cardID,
|
|
6830
|
+
courseID: this._currentCard.item.courseID,
|
|
6831
|
+
contentSourceID: this._currentCard.item.contentSourceID,
|
|
6832
|
+
contentSourceType: this._currentCard.item.contentSourceType,
|
|
6187
6833
|
status: "failed-review",
|
|
6188
|
-
reviewID: this._currentCard.reviewID
|
|
6834
|
+
reviewID: this._currentCard.item.reviewID
|
|
6189
6835
|
};
|
|
6190
6836
|
} else {
|
|
6191
6837
|
failedItem = {
|
|
6192
|
-
cardID: this._currentCard.cardID,
|
|
6193
|
-
courseID: this._currentCard.courseID,
|
|
6194
|
-
contentSourceID: this._currentCard.contentSourceID,
|
|
6195
|
-
contentSourceType: this._currentCard.contentSourceType,
|
|
6838
|
+
cardID: this._currentCard.item.cardID,
|
|
6839
|
+
courseID: this._currentCard.item.courseID,
|
|
6840
|
+
contentSourceID: this._currentCard.item.contentSourceID,
|
|
6841
|
+
contentSourceType: this._currentCard.item.contentSourceType,
|
|
6196
6842
|
status: "failed-new"
|
|
6197
6843
|
};
|
|
6198
6844
|
}
|
|
@@ -6205,141 +6851,19 @@ var SessionController = class extends Loggable {
|
|
|
6205
6851
|
hasAvailableCards() {
|
|
6206
6852
|
return this.reviewQ.length > 0 || this.newQ.length > 0 || this.failedQ.length > 0;
|
|
6207
6853
|
}
|
|
6208
|
-
|
|
6209
|
-
|
|
6210
|
-
|
|
6211
|
-
|
|
6212
|
-
|
|
6213
|
-
|
|
6214
|
-
|
|
6215
|
-
|
|
6216
|
-
return;
|
|
6217
|
-
}
|
|
6218
|
-
const BUFFER_SIZE = 5;
|
|
6219
|
-
this.hydration_in_progress = true;
|
|
6220
|
-
while (this.hydratedQ.length < BUFFER_SIZE) {
|
|
6221
|
-
const nextItem = this._selectNextItemToHydrate();
|
|
6222
|
-
if (!nextItem) {
|
|
6223
|
-
return;
|
|
6224
|
-
}
|
|
6225
|
-
try {
|
|
6226
|
-
const cardData = await this.dataLayer.getCourseDB(nextItem.courseID).getCourseDoc(nextItem.cardID);
|
|
6227
|
-
if (!(0, import_common15.isCourseElo)(cardData.elo)) {
|
|
6228
|
-
cardData.elo = (0, import_common15.toCourseElo)(cardData.elo);
|
|
6229
|
-
}
|
|
6230
|
-
const view = this.getViewComponent(cardData.id_view);
|
|
6231
|
-
const dataDocs = await Promise.all(
|
|
6232
|
-
cardData.id_displayable_data.map(
|
|
6233
|
-
(id) => this.dataLayer.getCourseDB(nextItem.courseID).getCourseDoc(id, {
|
|
6234
|
-
attachments: true,
|
|
6235
|
-
binary: true
|
|
6236
|
-
})
|
|
6237
|
-
)
|
|
6238
|
-
);
|
|
6239
|
-
const data = dataDocs.map(import_common15.displayableDataToViewData).reverse();
|
|
6240
|
-
this.hydratedQ.add(
|
|
6241
|
-
{
|
|
6242
|
-
item: nextItem,
|
|
6243
|
-
view,
|
|
6244
|
-
data
|
|
6245
|
-
},
|
|
6246
|
-
nextItem.cardID
|
|
6247
|
-
);
|
|
6248
|
-
if (this.reviewQ.peek(0) === nextItem) {
|
|
6249
|
-
this.reviewQ.dequeue();
|
|
6250
|
-
} else if (this.newQ.peek(0) === nextItem) {
|
|
6251
|
-
this.newQ.dequeue();
|
|
6252
|
-
} else {
|
|
6253
|
-
this.failedQ.dequeue();
|
|
6254
|
-
}
|
|
6255
|
-
} catch (e) {
|
|
6256
|
-
this.error(`Error hydrating card ${nextItem.cardID}:`, e);
|
|
6257
|
-
if (this.reviewQ.peek(0) === nextItem) {
|
|
6258
|
-
this.reviewQ.dequeue();
|
|
6259
|
-
} else if (this.newQ.peek(0) === nextItem) {
|
|
6260
|
-
this.newQ.dequeue();
|
|
6261
|
-
} else {
|
|
6262
|
-
this.failedQ.dequeue();
|
|
6263
|
-
}
|
|
6264
|
-
}
|
|
6265
|
-
}
|
|
6266
|
-
this.hydration_in_progress = false;
|
|
6267
|
-
}
|
|
6268
|
-
};
|
|
6269
|
-
|
|
6270
|
-
// src/study/SpacedRepetition.ts
|
|
6271
|
-
init_util();
|
|
6272
|
-
var import_moment6 = __toESM(require("moment"));
|
|
6273
|
-
init_logger();
|
|
6274
|
-
var duration = import_moment6.default.duration;
|
|
6275
|
-
function newInterval(user, cardHistory) {
|
|
6276
|
-
if (areQuestionRecords(cardHistory)) {
|
|
6277
|
-
return newQuestionInterval(user, cardHistory);
|
|
6278
|
-
} else {
|
|
6279
|
-
return 1e5;
|
|
6280
|
-
}
|
|
6281
|
-
}
|
|
6282
|
-
function newQuestionInterval(user, cardHistory) {
|
|
6283
|
-
const records = cardHistory.records;
|
|
6284
|
-
const currentAttempt = records[records.length - 1];
|
|
6285
|
-
const lastInterval = lastSuccessfulInterval(records);
|
|
6286
|
-
if (lastInterval > cardHistory.bestInterval) {
|
|
6287
|
-
cardHistory.bestInterval = lastInterval;
|
|
6288
|
-
void user.update(cardHistory._id, {
|
|
6289
|
-
bestInterval: lastInterval
|
|
6290
|
-
});
|
|
6291
|
-
}
|
|
6292
|
-
if (currentAttempt.isCorrect) {
|
|
6293
|
-
const skill = Math.min(1, Math.max(0, currentAttempt.performance));
|
|
6294
|
-
logger.debug(`Demontrated skill: ${skill}`);
|
|
6295
|
-
const interval = lastInterval * (0.75 + skill);
|
|
6296
|
-
cardHistory.lapses = getLapses(cardHistory.records);
|
|
6297
|
-
cardHistory.streak = getStreak(cardHistory.records);
|
|
6298
|
-
if (cardHistory.lapses && cardHistory.streak && cardHistory.bestInterval && (cardHistory.lapses >= 0 || cardHistory.streak >= 0)) {
|
|
6299
|
-
const ret = (cardHistory.lapses * interval + cardHistory.streak * cardHistory.bestInterval) / (cardHistory.lapses + cardHistory.streak);
|
|
6300
|
-
logger.debug(`Weighted average interval calculation:
|
|
6301
|
-
(${cardHistory.lapses} * ${interval} + ${cardHistory.streak} * ${cardHistory.bestInterval}) / (${cardHistory.lapses} + ${cardHistory.streak}) = ${ret}`);
|
|
6302
|
-
return ret;
|
|
6854
|
+
/**
|
|
6855
|
+
* Helper method for CardHydrationService to remove items from appropriate queue.
|
|
6856
|
+
*/
|
|
6857
|
+
removeItemFromQueue(item) {
|
|
6858
|
+
if (this.reviewQ.peek(0) === item) {
|
|
6859
|
+
this.reviewQ.dequeue((queueItem) => queueItem.cardID);
|
|
6860
|
+
} else if (this.newQ.peek(0) === item) {
|
|
6861
|
+
this.newQ.dequeue((queueItem) => queueItem.cardID);
|
|
6303
6862
|
} else {
|
|
6304
|
-
|
|
6863
|
+
this.failedQ.dequeue((queueItem) => queueItem.cardID);
|
|
6305
6864
|
}
|
|
6306
|
-
} else {
|
|
6307
|
-
return 0;
|
|
6308
6865
|
}
|
|
6309
|
-
}
|
|
6310
|
-
function lastSuccessfulInterval(cardHistory) {
|
|
6311
|
-
for (let i = cardHistory.length - 1; i >= 1; i--) {
|
|
6312
|
-
if (cardHistory[i].priorAttemps === 0 && cardHistory[i].isCorrect) {
|
|
6313
|
-
const lastInterval = secondsBetween(cardHistory[i - 1].timeStamp, cardHistory[i].timeStamp);
|
|
6314
|
-
const ret = Math.max(lastInterval, 20 * 60 * 60);
|
|
6315
|
-
logger.debug(`Last interval w/ this card was: ${lastInterval}s, returning ${ret}s`);
|
|
6316
|
-
return ret;
|
|
6317
|
-
}
|
|
6318
|
-
}
|
|
6319
|
-
return getInitialInterval(cardHistory);
|
|
6320
|
-
}
|
|
6321
|
-
function getStreak(records) {
|
|
6322
|
-
let streak = 0;
|
|
6323
|
-
let index = records.length - 1;
|
|
6324
|
-
while (index >= 0 && records[index].isCorrect) {
|
|
6325
|
-
index--;
|
|
6326
|
-
streak++;
|
|
6327
|
-
}
|
|
6328
|
-
return streak;
|
|
6329
|
-
}
|
|
6330
|
-
function getLapses(records) {
|
|
6331
|
-
return records.filter((r) => r.isCorrect === false).length;
|
|
6332
|
-
}
|
|
6333
|
-
function getInitialInterval(cardHistory) {
|
|
6334
|
-
logger.warn(`history of length: ${cardHistory.length} ignored!`);
|
|
6335
|
-
return 60 * 60 * 24 * 3;
|
|
6336
|
-
}
|
|
6337
|
-
function secondsBetween(start, end) {
|
|
6338
|
-
start = (0, import_moment6.default)(start);
|
|
6339
|
-
end = (0, import_moment6.default)(end);
|
|
6340
|
-
const ret = duration(end.diff(start)).asSeconds();
|
|
6341
|
-
return ret;
|
|
6342
|
-
}
|
|
6866
|
+
};
|
|
6343
6867
|
|
|
6344
6868
|
// src/index.ts
|
|
6345
6869
|
init_factory();
|