@vue-skuilder/db 0.1.11-9 → 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 +358 -87
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +358 -87
- package/dist/core/index.mjs.map +1 -1
- package/dist/{dataLayerProvider-DqtNroSh.d.ts → dataLayerProvider-BiP3kWix.d.mts} +8 -1
- package/dist/{dataLayerProvider-BInqI_RF.d.mts → dataLayerProvider-DSdeyRT3.d.ts} +8 -1
- package/dist/impl/couch/index.d.mts +19 -7
- package/dist/impl/couch/index.d.ts +19 -7
- package/dist/impl/couch/index.js +375 -100
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +374 -99
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.d.mts +23 -8
- package/dist/impl/static/index.d.ts +23 -8
- package/dist/impl/static/index.js +289 -85
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +289 -85
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/{index-CUNnL38E.d.mts → index-Bmll7Xse.d.mts} +1 -1
- package/dist/{index-CLL31bEy.d.ts → index-CD8BZz2k.d.ts} +1 -1
- package/dist/index.d.mts +123 -20
- package/dist/index.d.ts +123 -20
- package/dist/index.js +1133 -343
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1137 -343
- package/dist/index.mjs.map +1 -1
- package/dist/pouch/index.d.mts +1 -0
- package/dist/pouch/index.d.ts +1 -0
- package/dist/pouch/index.js +49 -0
- package/dist/pouch/index.js.map +1 -0
- package/dist/pouch/index.mjs +16 -0
- package/dist/pouch/index.mjs.map +1 -0
- package/dist/{types-BefDGkKa.d.ts → types-CewsN87z.d.ts} +1 -1
- package/dist/{types-DC-ckZug.d.mts → types-Dbp5DaRR.d.mts} +1 -1
- package/dist/{types-legacy-Birv-Jx6.d.mts → types-legacy-6ettoclI.d.mts} +17 -2
- package/dist/{types-legacy-Birv-Jx6.d.ts → types-legacy-6ettoclI.d.ts} +17 -2
- package/dist/{userDB-DusL7OXe.d.ts → userDB-C4yyAnpp.d.mts} +89 -56
- package/dist/{userDB-C33Hzjgn.d.mts → userDB-CD6s6ZCp.d.ts} +89 -56
- 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/interfaces/contentSource.ts +3 -2
- package/src/core/interfaces/courseDB.ts +26 -3
- package/src/core/interfaces/dataLayerProvider.ts +9 -1
- package/src/core/interfaces/userDB.ts +80 -64
- package/src/core/navigators/elo.ts +10 -7
- package/src/core/navigators/hardcodedOrder.ts +64 -0
- package/src/core/navigators/index.ts +2 -1
- package/src/core/types/contentNavigationStrategy.ts +2 -1
- package/src/core/types/types-legacy.ts +7 -2
- package/src/impl/common/BaseUserDB.ts +60 -14
- package/src/impl/couch/CouchDBSyncStrategy.ts +2 -2
- package/src/impl/couch/PouchDataLayerProvider.ts +21 -0
- package/src/impl/couch/adminDB.ts +2 -2
- package/src/impl/couch/auth.ts +13 -4
- package/src/impl/couch/classroomDB.ts +10 -12
- package/src/impl/couch/courseAPI.ts +2 -2
- package/src/impl/couch/courseDB.ts +204 -38
- package/src/impl/couch/courseLookupDB.ts +4 -3
- package/src/impl/couch/index.ts +36 -4
- package/src/impl/couch/pouchdb-setup.ts +3 -3
- package/src/impl/couch/updateQueue.ts +59 -36
- package/src/impl/static/StaticDataLayerProvider.ts +68 -17
- package/src/impl/static/courseDB.ts +64 -20
- package/src/impl/static/coursesDB.ts +10 -6
- package/src/pouch/index.ts +2 -0
- package/src/study/ItemQueue.ts +58 -0
- package/src/study/SessionController.ts +182 -111
- package/src/study/SpacedRepetition.ts +1 -1
- 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/tsup.config.ts +1 -0
package/dist/index.js
CHANGED
|
@@ -187,9 +187,9 @@ var init_pouchdb_setup = __esm({
|
|
|
187
187
|
import_pouchdb.default.plugin(import_pouchdb_find.default);
|
|
188
188
|
import_pouchdb.default.plugin(import_pouchdb_authentication.default);
|
|
189
189
|
import_pouchdb.default.defaults({
|
|
190
|
-
ajax: {
|
|
191
|
-
|
|
192
|
-
}
|
|
190
|
+
// ajax: {
|
|
191
|
+
// timeout: 60000,
|
|
192
|
+
// },
|
|
193
193
|
});
|
|
194
194
|
pouchdb_setup_default = import_pouchdb.default;
|
|
195
195
|
}
|
|
@@ -468,42 +468,58 @@ 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) {
|
|
475
477
|
this.inprogressUpdates[id] = true;
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
478
|
+
const MAX_RETRIES = 5;
|
|
479
|
+
for (let i = 0; i < MAX_RETRIES; i++) {
|
|
480
|
+
try {
|
|
481
|
+
const doc = await this.readDB.get(id);
|
|
482
|
+
logger.debug(`Retrieved doc: ${id}`);
|
|
483
|
+
let updatedDoc = { ...doc };
|
|
484
|
+
const updatesToApply = [...this.pendingUpdates[id]];
|
|
485
|
+
for (const update of updatesToApply) {
|
|
486
|
+
if (typeof update === "function") {
|
|
487
|
+
updatedDoc = { ...updatedDoc, ...update(updatedDoc) };
|
|
488
|
+
} else {
|
|
489
|
+
updatedDoc = {
|
|
490
|
+
...updatedDoc,
|
|
491
|
+
...update
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
await this.writeDB.put(updatedDoc);
|
|
496
|
+
logger.debug(`Put doc: ${id}`);
|
|
497
|
+
this.pendingUpdates[id].splice(0, updatesToApply.length);
|
|
498
|
+
if (this.pendingUpdates[id].length === 0) {
|
|
499
|
+
this.inprogressUpdates[id] = false;
|
|
500
|
+
delete this.inprogressUpdates[id];
|
|
483
501
|
} else {
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
502
|
+
return this.applyUpdates(id);
|
|
503
|
+
}
|
|
504
|
+
return updatedDoc;
|
|
505
|
+
} catch (e) {
|
|
506
|
+
if (e.name === "conflict" && i < MAX_RETRIES - 1) {
|
|
507
|
+
logger.warn(`Conflict on update for doc ${id}, retry #${i + 1}`);
|
|
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;
|
|
512
|
+
} else {
|
|
513
|
+
delete this.inprogressUpdates[id];
|
|
514
|
+
if (this.pendingUpdates[id]) {
|
|
515
|
+
delete this.pendingUpdates[id];
|
|
516
|
+
}
|
|
517
|
+
logger.error(`Error on attemped update (retry ${i}): ${JSON.stringify(e)}`);
|
|
518
|
+
throw e;
|
|
488
519
|
}
|
|
489
520
|
}
|
|
490
|
-
await this.writeDB.put(doc);
|
|
491
|
-
logger.debug(`Put doc: ${id}`);
|
|
492
|
-
if (this.pendingUpdates[id].length === 0) {
|
|
493
|
-
this.inprogressUpdates[id] = false;
|
|
494
|
-
delete this.inprogressUpdates[id];
|
|
495
|
-
} else {
|
|
496
|
-
return this.applyUpdates(id);
|
|
497
|
-
}
|
|
498
|
-
return doc;
|
|
499
|
-
} catch (e) {
|
|
500
|
-
delete this.inprogressUpdates[id];
|
|
501
|
-
if (this.pendingUpdates[id]) {
|
|
502
|
-
delete this.pendingUpdates[id];
|
|
503
|
-
}
|
|
504
|
-
logger.error(`Error on attemped update: ${JSON.stringify(e)}`);
|
|
505
|
-
throw e;
|
|
506
521
|
}
|
|
522
|
+
throw new Error(`UpdateQueue failed for doc ${id} after ${MAX_RETRIES} retries.`);
|
|
507
523
|
} else {
|
|
508
524
|
throw new Error(`Empty Updates Queue Triggered`);
|
|
509
525
|
}
|
|
@@ -754,7 +770,7 @@ function getCourseDB(courseID) {
|
|
|
754
770
|
const dbName = `coursedb-${courseID}`;
|
|
755
771
|
return new pouchdb_setup_default(
|
|
756
772
|
ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + dbName,
|
|
757
|
-
|
|
773
|
+
createPouchDBConfig()
|
|
758
774
|
);
|
|
759
775
|
}
|
|
760
776
|
var import_common, import_common2, import_common3, import_uuid, AlreadyTaggedErr;
|
|
@@ -880,6 +896,7 @@ var init_courseLookupDB = __esm({
|
|
|
880
896
|
const doc = await _CourseLookup._db.get(courseID);
|
|
881
897
|
return await _CourseLookup._db.remove(doc);
|
|
882
898
|
}
|
|
899
|
+
// [ ] rename to allCourses()
|
|
883
900
|
static async allCourseWare() {
|
|
884
901
|
const resp = await _CourseLookup._db.allDocs({
|
|
885
902
|
include_docs: true
|
|
@@ -950,13 +967,16 @@ var init_elo = __esm({
|
|
|
950
967
|
}
|
|
951
968
|
async getNewCards(limit = 99) {
|
|
952
969
|
const activeCards = await this.user.getActiveCards();
|
|
953
|
-
return (await this.course.getCardsCenteredAtELO(
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
970
|
+
return (await this.course.getCardsCenteredAtELO(
|
|
971
|
+
{ limit, elo: "user" },
|
|
972
|
+
(c) => {
|
|
973
|
+
if (activeCards.some((ac) => c.cardID === ac.cardID)) {
|
|
974
|
+
return false;
|
|
975
|
+
} else {
|
|
976
|
+
return true;
|
|
977
|
+
}
|
|
958
978
|
}
|
|
959
|
-
|
|
979
|
+
)).map((c) => {
|
|
960
980
|
return {
|
|
961
981
|
...c,
|
|
962
982
|
status: "new"
|
|
@@ -967,12 +987,74 @@ var init_elo = __esm({
|
|
|
967
987
|
}
|
|
968
988
|
});
|
|
969
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
|
+
|
|
970
1051
|
// import("./**/*") in src/core/navigators/index.ts
|
|
971
1052
|
var globImport;
|
|
972
1053
|
var init_ = __esm({
|
|
973
1054
|
'import("./**/*") in src/core/navigators/index.ts'() {
|
|
974
1055
|
globImport = __glob({
|
|
975
1056
|
"./elo.ts": () => Promise.resolve().then(() => (init_elo(), elo_exports)),
|
|
1057
|
+
"./hardcodedOrder.ts": () => Promise.resolve().then(() => (init_hardcodedOrder(), hardcodedOrder_exports)),
|
|
976
1058
|
"./index.ts": () => Promise.resolve().then(() => (init_navigators(), navigators_exports))
|
|
977
1059
|
});
|
|
978
1060
|
}
|
|
@@ -992,6 +1074,7 @@ var init_navigators = __esm({
|
|
|
992
1074
|
init_();
|
|
993
1075
|
Navigators = /* @__PURE__ */ ((Navigators2) => {
|
|
994
1076
|
Navigators2["ELO"] = "elo";
|
|
1077
|
+
Navigators2["HARDCODED"] = "hardcodedOrder";
|
|
995
1078
|
return Navigators2;
|
|
996
1079
|
})(Navigators || {});
|
|
997
1080
|
ContentNavigator = class {
|
|
@@ -1004,7 +1087,7 @@ var init_navigators = __esm({
|
|
|
1004
1087
|
static async create(user, course, strategyData) {
|
|
1005
1088
|
const implementingClass = strategyData.implementingClass;
|
|
1006
1089
|
let NavigatorImpl;
|
|
1007
|
-
const variations = ["", ".js", "
|
|
1090
|
+
const variations = [".ts", ".js", ""];
|
|
1008
1091
|
for (const ext of variations) {
|
|
1009
1092
|
try {
|
|
1010
1093
|
const module2 = await globImport(`./${implementingClass}${ext}`);
|
|
@@ -1261,6 +1344,23 @@ var init_courseDB = __esm({
|
|
|
1261
1344
|
if (!doc.docType || !(doc.docType === "CARD" /* CARD */)) {
|
|
1262
1345
|
throw new Error(`failed to remove ${id} from course ${this.id}. id does not point to a card`);
|
|
1263
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
|
+
}
|
|
1264
1364
|
return this.db.remove(doc);
|
|
1265
1365
|
}
|
|
1266
1366
|
async getCardDisplayableDataIDs(id) {
|
|
@@ -1308,7 +1408,13 @@ var init_courseDB = __esm({
|
|
|
1308
1408
|
} else {
|
|
1309
1409
|
return s;
|
|
1310
1410
|
}
|
|
1311
|
-
}).map((c) =>
|
|
1411
|
+
}).map((c) => {
|
|
1412
|
+
return {
|
|
1413
|
+
courseID: this.id,
|
|
1414
|
+
cardID: c.id,
|
|
1415
|
+
elo: c.key
|
|
1416
|
+
};
|
|
1417
|
+
});
|
|
1312
1418
|
const str = `below:
|
|
1313
1419
|
${below.rows.map((r) => ` ${r.id}-${r.key}
|
|
1314
1420
|
`)}
|
|
@@ -1363,7 +1469,13 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
1363
1469
|
}
|
|
1364
1470
|
}
|
|
1365
1471
|
async addTagToCard(cardId, tagId, updateELO) {
|
|
1366
|
-
return await addTagToCard(
|
|
1472
|
+
return await addTagToCard(
|
|
1473
|
+
this.id,
|
|
1474
|
+
cardId,
|
|
1475
|
+
tagId,
|
|
1476
|
+
(await this._getCurrentUser()).getUsername(),
|
|
1477
|
+
updateELO
|
|
1478
|
+
);
|
|
1367
1479
|
}
|
|
1368
1480
|
async removeTagFromCard(cardId, tagId) {
|
|
1369
1481
|
return await removeTagFromCard(this.id, cardId, tagId);
|
|
@@ -1432,23 +1544,9 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
1432
1544
|
////////////////////////////////////
|
|
1433
1545
|
getNavigationStrategy(id) {
|
|
1434
1546
|
logger.debug(`[courseDB] Getting navigation strategy: ${id}`);
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
name: "ELO",
|
|
1439
|
-
description: "ELO-based navigation strategy for ordering content by difficulty",
|
|
1440
|
-
implementingClass: "elo" /* ELO */,
|
|
1441
|
-
course: this.id,
|
|
1442
|
-
serializedData: ""
|
|
1443
|
-
// serde is a noop for ELO navigator.
|
|
1444
|
-
};
|
|
1445
|
-
return Promise.resolve(strategy);
|
|
1446
|
-
}
|
|
1447
|
-
getAllNavigationStrategies() {
|
|
1448
|
-
logger.debug("[courseDB] Returning hard-coded navigation strategies");
|
|
1449
|
-
const strategies = [
|
|
1450
|
-
{
|
|
1451
|
-
id: "ELO",
|
|
1547
|
+
if (id == "") {
|
|
1548
|
+
const strategy = {
|
|
1549
|
+
_id: "NAVIGATION_STRATEGY-ELO",
|
|
1452
1550
|
docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
|
|
1453
1551
|
name: "ELO",
|
|
1454
1552
|
description: "ELO-based navigation strategy for ordering content by difficulty",
|
|
@@ -1456,14 +1554,25 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
1456
1554
|
course: this.id,
|
|
1457
1555
|
serializedData: ""
|
|
1458
1556
|
// serde is a noop for ELO navigator.
|
|
1459
|
-
}
|
|
1460
|
-
|
|
1461
|
-
|
|
1557
|
+
};
|
|
1558
|
+
return Promise.resolve(strategy);
|
|
1559
|
+
} else {
|
|
1560
|
+
return this.db.get(id);
|
|
1561
|
+
}
|
|
1462
1562
|
}
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
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
|
+
});
|
|
1467
1576
|
}
|
|
1468
1577
|
updateNavigationStrategy(id, data) {
|
|
1469
1578
|
logger.debug(`[courseDB] Updating navigation strategy: ${id}`);
|
|
@@ -1471,9 +1580,32 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
1471
1580
|
return Promise.resolve();
|
|
1472
1581
|
}
|
|
1473
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
|
+
}
|
|
1474
1606
|
logger.warn(`Returning hard-coded default ELO navigator`);
|
|
1475
1607
|
const ret = {
|
|
1476
|
-
|
|
1608
|
+
_id: "NAVIGATION_STRATEGY-ELO",
|
|
1477
1609
|
docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
|
|
1478
1610
|
name: "ELO",
|
|
1479
1611
|
description: "ELO-based navigation strategy",
|
|
@@ -1556,17 +1688,93 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
1556
1688
|
selectedCards.push(card);
|
|
1557
1689
|
}
|
|
1558
1690
|
return selectedCards.map((c) => {
|
|
1559
|
-
const split = c.split("-");
|
|
1560
1691
|
return {
|
|
1561
1692
|
courseID: this.id,
|
|
1562
|
-
cardID:
|
|
1563
|
-
qualifiedID: `${split[0]}-${split[1]}`,
|
|
1693
|
+
cardID: c.cardID,
|
|
1564
1694
|
contentSourceType: "course",
|
|
1565
1695
|
contentSourceID: this.id,
|
|
1696
|
+
elo: c.elo,
|
|
1566
1697
|
status: "new"
|
|
1567
1698
|
};
|
|
1568
1699
|
});
|
|
1569
1700
|
}
|
|
1701
|
+
// Admin search methods
|
|
1702
|
+
async searchCards(query) {
|
|
1703
|
+
logger.log(`[CourseDB ${this.id}] Searching for: "${query}"`);
|
|
1704
|
+
let displayableData;
|
|
1705
|
+
try {
|
|
1706
|
+
displayableData = await this.db.find({
|
|
1707
|
+
selector: {
|
|
1708
|
+
docType: "DISPLAYABLE_DATA",
|
|
1709
|
+
"data.0.data": { $regex: `.*${query}.*` }
|
|
1710
|
+
}
|
|
1711
|
+
});
|
|
1712
|
+
logger.log(`[CourseDB ${this.id}] Regex search on data[0].data successful`);
|
|
1713
|
+
} catch (regexError) {
|
|
1714
|
+
logger.log(
|
|
1715
|
+
`[CourseDB ${this.id}] Regex search failed, falling back to manual search:`,
|
|
1716
|
+
regexError
|
|
1717
|
+
);
|
|
1718
|
+
const allDisplayable = await this.db.find({
|
|
1719
|
+
selector: {
|
|
1720
|
+
docType: "DISPLAYABLE_DATA"
|
|
1721
|
+
}
|
|
1722
|
+
});
|
|
1723
|
+
logger.log(
|
|
1724
|
+
`[CourseDB ${this.id}] Retrieved ${allDisplayable.docs.length} documents for manual filtering`
|
|
1725
|
+
);
|
|
1726
|
+
displayableData = {
|
|
1727
|
+
docs: allDisplayable.docs.filter((doc) => {
|
|
1728
|
+
const docString = JSON.stringify(doc).toLowerCase();
|
|
1729
|
+
const match = docString.includes(query.toLowerCase());
|
|
1730
|
+
if (match) {
|
|
1731
|
+
logger.log(`[CourseDB ${this.id}] Manual match found in document: ${doc._id}`);
|
|
1732
|
+
}
|
|
1733
|
+
return match;
|
|
1734
|
+
})
|
|
1735
|
+
};
|
|
1736
|
+
}
|
|
1737
|
+
logger.log(
|
|
1738
|
+
`[CourseDB ${this.id}] Found ${displayableData.docs.length} displayable data documents`
|
|
1739
|
+
);
|
|
1740
|
+
if (displayableData.docs.length === 0) {
|
|
1741
|
+
const allDisplayableData = await this.db.find({
|
|
1742
|
+
selector: {
|
|
1743
|
+
docType: "DISPLAYABLE_DATA"
|
|
1744
|
+
},
|
|
1745
|
+
limit: 5
|
|
1746
|
+
// Just sample a few
|
|
1747
|
+
});
|
|
1748
|
+
logger.log(
|
|
1749
|
+
`[CourseDB ${this.id}] Sample displayable data:`,
|
|
1750
|
+
allDisplayableData.docs.map((d) => ({
|
|
1751
|
+
id: d._id,
|
|
1752
|
+
docType: d.docType,
|
|
1753
|
+
dataStructure: d.data ? Object.keys(d.data) : "no data field",
|
|
1754
|
+
dataContent: d.data,
|
|
1755
|
+
fullDoc: d
|
|
1756
|
+
}))
|
|
1757
|
+
);
|
|
1758
|
+
}
|
|
1759
|
+
const allResults = [];
|
|
1760
|
+
for (const dd of displayableData.docs) {
|
|
1761
|
+
const cards = await this.db.find({
|
|
1762
|
+
selector: {
|
|
1763
|
+
docType: "CARD",
|
|
1764
|
+
id_displayable_data: { $in: [dd._id] }
|
|
1765
|
+
}
|
|
1766
|
+
});
|
|
1767
|
+
logger.log(
|
|
1768
|
+
`[CourseDB ${this.id}] Displayable data ${dd._id} linked to ${cards.docs.length} cards`
|
|
1769
|
+
);
|
|
1770
|
+
allResults.push(...cards.docs);
|
|
1771
|
+
}
|
|
1772
|
+
logger.log(`[CourseDB ${this.id}] Total cards found: ${allResults.length}`);
|
|
1773
|
+
return allResults;
|
|
1774
|
+
}
|
|
1775
|
+
async find(request) {
|
|
1776
|
+
return this.db.find(request);
|
|
1777
|
+
}
|
|
1570
1778
|
};
|
|
1571
1779
|
}
|
|
1572
1780
|
});
|
|
@@ -1632,7 +1840,7 @@ var init_classroomDB2 = __esm({
|
|
|
1632
1840
|
const dbName = `classdb-student-${this._id}`;
|
|
1633
1841
|
this._db = new pouchdb_setup_default(
|
|
1634
1842
|
ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + dbName,
|
|
1635
|
-
|
|
1843
|
+
createPouchDBConfig()
|
|
1636
1844
|
);
|
|
1637
1845
|
try {
|
|
1638
1846
|
const cfg = await this._db.get(CLASSROOM_CONFIG);
|
|
@@ -1701,9 +1909,11 @@ var init_classroomDB2 = __esm({
|
|
|
1701
1909
|
ret.push(await getCourseDB2(content.courseID).get(content.cardID));
|
|
1702
1910
|
}
|
|
1703
1911
|
}
|
|
1704
|
-
logger.info(
|
|
1912
|
+
logger.info(
|
|
1913
|
+
`New Cards from classroom ${this._cfg.name}: ${ret.map((c) => `${c.courseID}-${c.cardID}`)}`
|
|
1914
|
+
);
|
|
1705
1915
|
return ret.filter((c) => {
|
|
1706
|
-
if (activeCards.some((ac) => c.
|
|
1916
|
+
if (activeCards.some((ac) => c.cardID === ac.cardID)) {
|
|
1707
1917
|
return false;
|
|
1708
1918
|
} else {
|
|
1709
1919
|
return true;
|
|
@@ -1722,11 +1932,11 @@ var init_classroomDB2 = __esm({
|
|
|
1722
1932
|
const stuDbName = `classdb-student-${this._id}`;
|
|
1723
1933
|
this._db = new pouchdb_setup_default(
|
|
1724
1934
|
ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + dbName,
|
|
1725
|
-
|
|
1935
|
+
createPouchDBConfig()
|
|
1726
1936
|
);
|
|
1727
1937
|
this._stuDb = new pouchdb_setup_default(
|
|
1728
1938
|
ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + stuDbName,
|
|
1729
|
-
|
|
1939
|
+
createPouchDBConfig()
|
|
1730
1940
|
);
|
|
1731
1941
|
try {
|
|
1732
1942
|
return this._db.get(CLASSROOM_CONFIG).then((cfg) => {
|
|
@@ -1811,7 +2021,7 @@ var init_adminDB2 = __esm({
|
|
|
1811
2021
|
constructor() {
|
|
1812
2022
|
this.usersDB = new pouchdb_setup_default(
|
|
1813
2023
|
ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + "_users",
|
|
1814
|
-
|
|
2024
|
+
createPouchDBConfig()
|
|
1815
2025
|
);
|
|
1816
2026
|
}
|
|
1817
2027
|
async getUsers() {
|
|
@@ -1872,9 +2082,10 @@ var init_adminDB2 = __esm({
|
|
|
1872
2082
|
async function getCurrentSession() {
|
|
1873
2083
|
try {
|
|
1874
2084
|
if (ENV.COUCHDB_SERVER_URL === NOT_SET || ENV.COUCHDB_SERVER_PROTOCOL === NOT_SET) {
|
|
1875
|
-
throw new Error(
|
|
2085
|
+
throw new Error(`CouchDB server configuration not properly initialized. Protocol: "${ENV.COUCHDB_SERVER_PROTOCOL}", URL: "${ENV.COUCHDB_SERVER_URL}"`);
|
|
1876
2086
|
}
|
|
1877
|
-
const
|
|
2087
|
+
const baseUrl = ENV.COUCHDB_SERVER_URL.endsWith("/") ? ENV.COUCHDB_SERVER_URL.slice(0, -1) : ENV.COUCHDB_SERVER_URL;
|
|
2088
|
+
const url = `${ENV.COUCHDB_SERVER_PROTOCOL}://${baseUrl}/_session`;
|
|
1878
2089
|
logger.debug(`Attempting session check at: ${url}`);
|
|
1879
2090
|
const response = await (0, import_cross_fetch.default)(url, {
|
|
1880
2091
|
method: "GET",
|
|
@@ -1886,8 +2097,10 @@ async function getCurrentSession() {
|
|
|
1886
2097
|
const resp = await response.json();
|
|
1887
2098
|
return resp;
|
|
1888
2099
|
} catch (error) {
|
|
1889
|
-
|
|
1890
|
-
|
|
2100
|
+
const baseUrl = ENV.COUCHDB_SERVER_URL.endsWith("/") ? ENV.COUCHDB_SERVER_URL.slice(0, -1) : ENV.COUCHDB_SERVER_URL;
|
|
2101
|
+
const url = `${ENV.COUCHDB_SERVER_PROTOCOL}://${baseUrl}/_session`;
|
|
2102
|
+
logger.error(`Session check error attempting to connect to: ${url} - ${error}`);
|
|
2103
|
+
throw new Error(`Session check failed connecting to ${url}: ${error}`);
|
|
1891
2104
|
}
|
|
1892
2105
|
}
|
|
1893
2106
|
async function getLoggedInUsername() {
|
|
@@ -2082,7 +2295,7 @@ var init_CouchDBSyncStrategy = __esm({
|
|
|
2082
2295
|
log3(`Fetching user database: ${dbName} (${username})`);
|
|
2083
2296
|
const ret = new pouchdb_setup_default(
|
|
2084
2297
|
ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + dbName,
|
|
2085
|
-
|
|
2298
|
+
createPouchDBConfig()
|
|
2086
2299
|
);
|
|
2087
2300
|
if (guestAccount) {
|
|
2088
2301
|
updateGuestAccountExpirationDate(ret);
|
|
@@ -2094,10 +2307,29 @@ var init_CouchDBSyncStrategy = __esm({
|
|
|
2094
2307
|
});
|
|
2095
2308
|
|
|
2096
2309
|
// src/impl/couch/index.ts
|
|
2310
|
+
function createPouchDBConfig() {
|
|
2311
|
+
const hasExplicitCredentials = ENV.COUCHDB_USERNAME && ENV.COUCHDB_PASSWORD;
|
|
2312
|
+
const isNodeEnvironment2 = typeof window === "undefined";
|
|
2313
|
+
if (hasExplicitCredentials && isNodeEnvironment2) {
|
|
2314
|
+
return {
|
|
2315
|
+
fetch(url, opts = {}) {
|
|
2316
|
+
const basicAuth = btoa(`${ENV.COUCHDB_USERNAME}:${ENV.COUCHDB_PASSWORD}`);
|
|
2317
|
+
const headers = new Headers(opts.headers || {});
|
|
2318
|
+
headers.set("Authorization", `Basic ${basicAuth}`);
|
|
2319
|
+
const newOpts = {
|
|
2320
|
+
...opts,
|
|
2321
|
+
headers
|
|
2322
|
+
};
|
|
2323
|
+
return pouchdb_setup_default.fetch(url, newOpts);
|
|
2324
|
+
}
|
|
2325
|
+
};
|
|
2326
|
+
}
|
|
2327
|
+
return pouchDBincludeCredentialsConfig;
|
|
2328
|
+
}
|
|
2097
2329
|
function getCourseDB2(courseID) {
|
|
2098
2330
|
return new pouchdb_setup_default(
|
|
2099
2331
|
ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + "coursedb-" + courseID,
|
|
2100
|
-
|
|
2332
|
+
createPouchDBConfig()
|
|
2101
2333
|
);
|
|
2102
2334
|
}
|
|
2103
2335
|
function getCourseDocs(courseID, docIDs, options = {}) {
|
|
@@ -2389,6 +2621,9 @@ Currently logged-in as ${this._username}.`
|
|
|
2389
2621
|
await this.init();
|
|
2390
2622
|
return ret;
|
|
2391
2623
|
}
|
|
2624
|
+
async get(id) {
|
|
2625
|
+
return this.localDB.get(id);
|
|
2626
|
+
}
|
|
2392
2627
|
update(id, update) {
|
|
2393
2628
|
return this.updateQueue.update(id, update);
|
|
2394
2629
|
}
|
|
@@ -2435,7 +2670,12 @@ Currently logged-in as ${this._username}.`
|
|
|
2435
2670
|
endkey: keys.endkey,
|
|
2436
2671
|
include_docs: true
|
|
2437
2672
|
});
|
|
2438
|
-
return reviews.rows.map((r) =>
|
|
2673
|
+
return reviews.rows.map((r) => {
|
|
2674
|
+
return {
|
|
2675
|
+
courseID: r.doc.courseId,
|
|
2676
|
+
cardID: r.doc.cardId
|
|
2677
|
+
};
|
|
2678
|
+
});
|
|
2439
2679
|
}
|
|
2440
2680
|
async getActivityRecords() {
|
|
2441
2681
|
try {
|
|
@@ -2715,8 +2955,18 @@ Currently logged-in as ${this._username}.`
|
|
|
2715
2955
|
}
|
|
2716
2956
|
this.setDBandQ();
|
|
2717
2957
|
this.syncStrategy.startSync(this.localDB, this.remoteDB);
|
|
2718
|
-
|
|
2719
|
-
|
|
2958
|
+
this.applyDesignDocs().catch((error) => {
|
|
2959
|
+
log4(`Error in applyDesignDocs background task: ${error}`);
|
|
2960
|
+
if (error && typeof error === "object") {
|
|
2961
|
+
log4(`Full error details in applyDesignDocs: ${JSON.stringify(error)}`);
|
|
2962
|
+
}
|
|
2963
|
+
});
|
|
2964
|
+
this.deduplicateReviews().catch((error) => {
|
|
2965
|
+
log4(`Error in deduplicateReviews background task: ${error}`);
|
|
2966
|
+
if (error && typeof error === "object") {
|
|
2967
|
+
log4(`Full error details in background task: ${JSON.stringify(error)}`);
|
|
2968
|
+
}
|
|
2969
|
+
});
|
|
2720
2970
|
_BaseUser._initialized = true;
|
|
2721
2971
|
}
|
|
2722
2972
|
static designDocs = [
|
|
@@ -2734,10 +2984,15 @@ Currently logged-in as ${this._username}.`
|
|
|
2734
2984
|
}
|
|
2735
2985
|
];
|
|
2736
2986
|
async applyDesignDocs() {
|
|
2987
|
+
log4(`Starting applyDesignDocs for user: ${this._username}`);
|
|
2988
|
+
log4(`Remote DB name: ${this.remoteDB.name || "unknown"}`);
|
|
2737
2989
|
if (this._username === "admin") {
|
|
2990
|
+
log4("Skipping design docs for admin user");
|
|
2738
2991
|
return;
|
|
2739
2992
|
}
|
|
2993
|
+
log4(`Applying ${_BaseUser.designDocs.length} design docs`);
|
|
2740
2994
|
for (const doc of _BaseUser.designDocs) {
|
|
2995
|
+
log4(`Applying design doc: ${doc._id}`);
|
|
2741
2996
|
try {
|
|
2742
2997
|
try {
|
|
2743
2998
|
const existingDoc = await this.remoteDB.get(doc._id);
|
|
@@ -2814,17 +3069,21 @@ Currently logged-in as ${this._username}.`
|
|
|
2814
3069
|
} catch (e) {
|
|
2815
3070
|
const reason = e;
|
|
2816
3071
|
if (reason.status === 404) {
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
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
|
+
}
|
|
2828
3087
|
} else {
|
|
2829
3088
|
throw new Error(`putCardRecord failed because of:
|
|
2830
3089
|
name:${reason.name}
|
|
@@ -2836,8 +3095,13 @@ Currently logged-in as ${this._username}.`
|
|
|
2836
3095
|
async deduplicateReviews() {
|
|
2837
3096
|
try {
|
|
2838
3097
|
log4("Starting deduplication of scheduled reviews...");
|
|
3098
|
+
log4(`Remote DB name: ${this.remoteDB.name || "unknown"}`);
|
|
3099
|
+
log4(`Write DB name: ${this.writeDB.name || "unknown"}`);
|
|
2839
3100
|
const reviewsMap = {};
|
|
2840
3101
|
const duplicateDocIds = [];
|
|
3102
|
+
log4(
|
|
3103
|
+
`Attempting to query remoteDB for reviewCards/reviewCards. Database: ${this.remoteDB.name || "unknown"}`
|
|
3104
|
+
);
|
|
2841
3105
|
const scheduledReviews = await this.remoteDB.query("reviewCards/reviewCards");
|
|
2842
3106
|
log4(`Found ${scheduledReviews.rows.length} scheduled reviews to process`);
|
|
2843
3107
|
scheduledReviews.rows.forEach((r) => {
|
|
@@ -2872,6 +3136,17 @@ Currently logged-in as ${this._username}.`
|
|
|
2872
3136
|
}
|
|
2873
3137
|
} catch (error) {
|
|
2874
3138
|
log4(`Error during review deduplication: ${error}`);
|
|
3139
|
+
if (error && typeof error === "object" && "status" in error && error.status === 404) {
|
|
3140
|
+
log4(
|
|
3141
|
+
`Database not found (404) during review deduplication. Database: ${this.remoteDB.name || "unknown"}`
|
|
3142
|
+
);
|
|
3143
|
+
log4(
|
|
3144
|
+
`This might indicate the user database doesn't exist or the reviewCards view isn't available`
|
|
3145
|
+
);
|
|
3146
|
+
}
|
|
3147
|
+
if (error && typeof error === "object") {
|
|
3148
|
+
log4(`Full error details: ${JSON.stringify(error)}`);
|
|
3149
|
+
}
|
|
2875
3150
|
}
|
|
2876
3151
|
}
|
|
2877
3152
|
/**
|
|
@@ -3105,6 +3380,16 @@ var init_PouchDataLayerProvider = __esm({
|
|
|
3105
3380
|
getAdminDB() {
|
|
3106
3381
|
return new AdminDB();
|
|
3107
3382
|
}
|
|
3383
|
+
async createUserReaderForUser(targetUsername) {
|
|
3384
|
+
const requestingUsername = await getLoggedInUsername();
|
|
3385
|
+
if (requestingUsername !== "admin") {
|
|
3386
|
+
throw new Error("Unauthorized: Only admin users can access other users' data");
|
|
3387
|
+
}
|
|
3388
|
+
logger.info(`Admin user '${requestingUsername}' requesting UserDBReader for '${targetUsername}'`);
|
|
3389
|
+
const syncStrategy = new CouchDBSyncStrategy();
|
|
3390
|
+
const targetUserDB = await BaseUser.instance(syncStrategy, targetUsername);
|
|
3391
|
+
return targetUserDB;
|
|
3392
|
+
}
|
|
3108
3393
|
isReadOnly() {
|
|
3109
3394
|
return false;
|
|
3110
3395
|
}
|
|
@@ -3557,7 +3842,10 @@ var init_courseDB2 = __esm({
|
|
|
3557
3842
|
};
|
|
3558
3843
|
}
|
|
3559
3844
|
async getCardsByELO(elo, limit) {
|
|
3560
|
-
return this.unpacker.queryByElo(elo, limit || 25)
|
|
3845
|
+
return (await this.unpacker.queryByElo(elo, limit || 25)).map((card) => {
|
|
3846
|
+
const [courseID, cardID, elo2] = card.split("-");
|
|
3847
|
+
return { courseID, cardID, elo: elo2 ? parseInt(elo2) : void 0 };
|
|
3848
|
+
});
|
|
3561
3849
|
}
|
|
3562
3850
|
async getCardEloData(cardIds) {
|
|
3563
3851
|
const results = await Promise.all(
|
|
@@ -3575,16 +3863,20 @@ var init_courseDB2 = __esm({
|
|
|
3575
3863
|
async updateCardElo(cardId, _elo) {
|
|
3576
3864
|
return { ok: true, id: cardId, rev: "1-static" };
|
|
3577
3865
|
}
|
|
3578
|
-
async getNewCards(limit) {
|
|
3579
|
-
const
|
|
3580
|
-
return
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
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
|
+
});
|
|
3588
3880
|
}
|
|
3589
3881
|
async getCardsCenteredAtELO(options, filter) {
|
|
3590
3882
|
let targetElo = typeof options.elo === "number" ? options.elo : 1e3;
|
|
@@ -3601,14 +3893,19 @@ var init_courseDB2 = __esm({
|
|
|
3601
3893
|
} else if (options.elo === "random") {
|
|
3602
3894
|
targetElo = 800 + Math.random() * 400;
|
|
3603
3895
|
}
|
|
3604
|
-
let cardIds = await this.unpacker.queryByElo(targetElo, options.limit * 2)
|
|
3896
|
+
let cardIds = (await this.unpacker.queryByElo(targetElo, options.limit * 2)).map((c) => {
|
|
3897
|
+
return {
|
|
3898
|
+
cardID: c,
|
|
3899
|
+
courseID: this.courseId
|
|
3900
|
+
};
|
|
3901
|
+
});
|
|
3605
3902
|
if (filter) {
|
|
3606
3903
|
cardIds = cardIds.filter(filter);
|
|
3607
3904
|
}
|
|
3608
|
-
return cardIds.slice(0, options.limit).map((
|
|
3905
|
+
return cardIds.slice(0, options.limit).map((card) => ({
|
|
3609
3906
|
status: "new",
|
|
3610
|
-
qualifiedID: `${this.courseId}-${cardId}`,
|
|
3611
|
-
cardID:
|
|
3907
|
+
// qualifiedID: `${this.courseId}-${cardId}`,
|
|
3908
|
+
cardID: card.cardID,
|
|
3612
3909
|
contentSourceType: "course",
|
|
3613
3910
|
contentSourceID: this.courseId,
|
|
3614
3911
|
courseID: this.courseId
|
|
@@ -3760,7 +4057,7 @@ var init_courseDB2 = __esm({
|
|
|
3760
4057
|
// Navigation Strategy Manager implementation
|
|
3761
4058
|
async getNavigationStrategy(_id) {
|
|
3762
4059
|
return {
|
|
3763
|
-
|
|
4060
|
+
_id: "NAVIGATION_STRATEGY-ELO",
|
|
3764
4061
|
docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
|
|
3765
4062
|
name: "ELO",
|
|
3766
4063
|
description: "ELO-based navigation strategy",
|
|
@@ -3800,6 +4097,16 @@ var init_courseDB2 = __esm({
|
|
|
3800
4097
|
async getAttachmentBlob(docId, attachmentName) {
|
|
3801
4098
|
return this.unpacker.getAttachmentBlob(docId, attachmentName);
|
|
3802
4099
|
}
|
|
4100
|
+
// Admin search methods
|
|
4101
|
+
async searchCards(_query) {
|
|
4102
|
+
return [];
|
|
4103
|
+
}
|
|
4104
|
+
async find(_request) {
|
|
4105
|
+
return {
|
|
4106
|
+
docs: [],
|
|
4107
|
+
warning: "Find operations not supported in static mode"
|
|
4108
|
+
};
|
|
4109
|
+
}
|
|
3803
4110
|
};
|
|
3804
4111
|
}
|
|
3805
4112
|
});
|
|
@@ -3815,11 +4122,17 @@ var init_coursesDB = __esm({
|
|
|
3815
4122
|
this.manifests = manifests;
|
|
3816
4123
|
}
|
|
3817
4124
|
async getCourseConfig(courseId) {
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
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}`);
|
|
3821
4135
|
}
|
|
3822
|
-
return {};
|
|
3823
4136
|
}
|
|
3824
4137
|
async getCourseList() {
|
|
3825
4138
|
return Object.keys(this.manifests).map(
|
|
@@ -3911,23 +4224,49 @@ var init_StaticDataLayerProvider = __esm({
|
|
|
3911
4224
|
config;
|
|
3912
4225
|
initialized = false;
|
|
3913
4226
|
courseUnpackers = /* @__PURE__ */ new Map();
|
|
4227
|
+
manifests = {};
|
|
3914
4228
|
constructor(config) {
|
|
3915
4229
|
this.config = {
|
|
3916
|
-
staticContentPath: config.staticContentPath || "/static-courses",
|
|
3917
4230
|
localStoragePrefix: config.localStoragePrefix || "skuilder-static",
|
|
3918
|
-
|
|
4231
|
+
rootManifest: config.rootManifest || { dependencies: {} },
|
|
4232
|
+
rootManifestUrl: config.rootManifestUrl || "/"
|
|
3919
4233
|
};
|
|
3920
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
|
+
}
|
|
3921
4266
|
async initialize() {
|
|
3922
4267
|
if (this.initialized) return;
|
|
3923
4268
|
logger.info("Initializing static data layer provider");
|
|
3924
|
-
|
|
3925
|
-
const unpacker = new StaticDataUnpacker(
|
|
3926
|
-
manifest,
|
|
3927
|
-
`${this.config.staticContentPath}/${courseId}`
|
|
3928
|
-
);
|
|
3929
|
-
this.courseUnpackers.set(courseId, unpacker);
|
|
3930
|
-
}
|
|
4269
|
+
await this.resolveCourseDependencies();
|
|
3931
4270
|
this.initialized = true;
|
|
3932
4271
|
}
|
|
3933
4272
|
async teardown() {
|
|
@@ -3941,13 +4280,13 @@ var init_StaticDataLayerProvider = __esm({
|
|
|
3941
4280
|
getCourseDB(courseId) {
|
|
3942
4281
|
const unpacker = this.courseUnpackers.get(courseId);
|
|
3943
4282
|
if (!unpacker) {
|
|
3944
|
-
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.`);
|
|
3945
4284
|
}
|
|
3946
|
-
const manifest = this.
|
|
4285
|
+
const manifest = this.manifests[courseId];
|
|
3947
4286
|
return new StaticCourseDB(courseId, unpacker, this.getUserDB(), manifest);
|
|
3948
4287
|
}
|
|
3949
4288
|
getCoursesDB() {
|
|
3950
|
-
return new StaticCoursesDB(this.
|
|
4289
|
+
return new StaticCoursesDB(this.manifests);
|
|
3951
4290
|
}
|
|
3952
4291
|
async getClassroomDB(_classId, _type) {
|
|
3953
4292
|
throw new Error("Classrooms not supported in static mode");
|
|
@@ -3955,6 +4294,12 @@ var init_StaticDataLayerProvider = __esm({
|
|
|
3955
4294
|
getAdminDB() {
|
|
3956
4295
|
throw new Error("Admin functions not supported in static mode");
|
|
3957
4296
|
}
|
|
4297
|
+
async createUserReaderForUser(targetUsername) {
|
|
4298
|
+
logger.warn(`StaticDataLayerProvider: Multi-user access not supported in static mode`);
|
|
4299
|
+
logger.warn(`Request: trying to access data for ${targetUsername}`);
|
|
4300
|
+
logger.warn(`Returning current user's data instead`);
|
|
4301
|
+
return this.getUserDB();
|
|
4302
|
+
}
|
|
3958
4303
|
isReadOnly() {
|
|
3959
4304
|
return true;
|
|
3960
4305
|
}
|
|
@@ -4290,66 +4635,557 @@ module.exports = __toCommonJS(index_exports);
|
|
|
4290
4635
|
init_core();
|
|
4291
4636
|
init_courseLookupDB();
|
|
4292
4637
|
|
|
4293
|
-
// src/study/
|
|
4638
|
+
// src/study/services/SrsService.ts
|
|
4639
|
+
var import_moment7 = __toESM(require("moment"));
|
|
4294
4640
|
init_couch();
|
|
4295
4641
|
|
|
4296
|
-
// src/
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
// src/util/packer/CouchDBToStaticPacker.ts
|
|
4300
|
-
init_types_legacy();
|
|
4642
|
+
// src/study/SpacedRepetition.ts
|
|
4643
|
+
init_util();
|
|
4644
|
+
var import_moment6 = __toESM(require("moment"));
|
|
4301
4645
|
init_logger();
|
|
4302
|
-
var
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
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
|
+
|
|
5129
|
+
// src/study/SessionController.ts
|
|
5130
|
+
init_couch();
|
|
5131
|
+
|
|
5132
|
+
// src/util/index.ts
|
|
5133
|
+
init_Loggable();
|
|
5134
|
+
|
|
5135
|
+
// src/util/packer/CouchDBToStaticPacker.ts
|
|
5136
|
+
init_types_legacy();
|
|
5137
|
+
init_logger();
|
|
5138
|
+
var CouchDBToStaticPacker = class {
|
|
5139
|
+
config;
|
|
5140
|
+
sourceDB = null;
|
|
5141
|
+
constructor(config = {}) {
|
|
5142
|
+
this.config = {
|
|
5143
|
+
chunkSize: 1e3,
|
|
5144
|
+
includeAttachments: true,
|
|
5145
|
+
...config
|
|
5146
|
+
};
|
|
5147
|
+
}
|
|
5148
|
+
/**
|
|
5149
|
+
* Pack a CouchDB course database into static data structures
|
|
5150
|
+
*/
|
|
5151
|
+
async packCourse(sourceDB, courseId) {
|
|
5152
|
+
logger.info(`Starting static pack for course: ${courseId}`);
|
|
5153
|
+
this.sourceDB = sourceDB;
|
|
5154
|
+
const manifest = {
|
|
5155
|
+
version: "1.0.0",
|
|
5156
|
+
courseId,
|
|
5157
|
+
courseName: "",
|
|
5158
|
+
courseConfig: null,
|
|
5159
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5160
|
+
documentCount: 0,
|
|
5161
|
+
chunks: [],
|
|
5162
|
+
indices: [],
|
|
5163
|
+
designDocs: []
|
|
5164
|
+
};
|
|
5165
|
+
const courseConfig = await this.extractCourseConfig(sourceDB);
|
|
5166
|
+
manifest.courseName = courseConfig.name;
|
|
5167
|
+
manifest.courseConfig = courseConfig;
|
|
5168
|
+
manifest.designDocs = await this.extractDesignDocs(sourceDB);
|
|
5169
|
+
const docsByType = await this.extractDocumentsByType(sourceDB);
|
|
5170
|
+
const attachments = /* @__PURE__ */ new Map();
|
|
5171
|
+
if (this.config.includeAttachments) {
|
|
5172
|
+
await this.extractAllAttachments(docsByType, attachments);
|
|
5173
|
+
}
|
|
5174
|
+
const chunks = /* @__PURE__ */ new Map();
|
|
5175
|
+
for (const [docType, docs] of Object.entries(docsByType)) {
|
|
5176
|
+
const chunkMetadata = this.createChunks(docs, docType);
|
|
5177
|
+
manifest.chunks.push(...chunkMetadata);
|
|
5178
|
+
manifest.documentCount += docs.length;
|
|
5179
|
+
this.prepareChunkData(chunkMetadata, docs, chunks);
|
|
5180
|
+
}
|
|
5181
|
+
const indices = /* @__PURE__ */ new Map();
|
|
5182
|
+
manifest.indices = await this.buildIndices(docsByType, manifest.designDocs, indices);
|
|
5183
|
+
return {
|
|
5184
|
+
manifest,
|
|
5185
|
+
chunks,
|
|
5186
|
+
indices,
|
|
5187
|
+
attachments
|
|
5188
|
+
};
|
|
4353
5189
|
}
|
|
4354
5190
|
/**
|
|
4355
5191
|
* Pack a CouchDB course database and write the static files to disk
|
|
@@ -5720,58 +6556,24 @@ init_tuiLogger();
|
|
|
5720
6556
|
function randomInt(min, max) {
|
|
5721
6557
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
5722
6558
|
}
|
|
5723
|
-
var ItemQueue = class {
|
|
5724
|
-
q = [];
|
|
5725
|
-
seenCardIds = [];
|
|
5726
|
-
_dequeueCount = 0;
|
|
5727
|
-
get dequeueCount() {
|
|
5728
|
-
return this._dequeueCount;
|
|
5729
|
-
}
|
|
5730
|
-
add(item) {
|
|
5731
|
-
if (this.seenCardIds.find((d) => d === item.cardID)) {
|
|
5732
|
-
return;
|
|
5733
|
-
}
|
|
5734
|
-
this.seenCardIds.push(item.cardID);
|
|
5735
|
-
this.q.push(item);
|
|
5736
|
-
}
|
|
5737
|
-
addAll(items) {
|
|
5738
|
-
items.forEach((i) => this.add(i));
|
|
5739
|
-
}
|
|
5740
|
-
get length() {
|
|
5741
|
-
return this.q.length;
|
|
5742
|
-
}
|
|
5743
|
-
peek(index) {
|
|
5744
|
-
return this.q[index];
|
|
5745
|
-
}
|
|
5746
|
-
dequeue() {
|
|
5747
|
-
if (this.q.length !== 0) {
|
|
5748
|
-
this._dequeueCount++;
|
|
5749
|
-
return this.q.splice(0, 1)[0];
|
|
5750
|
-
} else {
|
|
5751
|
-
return null;
|
|
5752
|
-
}
|
|
5753
|
-
}
|
|
5754
|
-
get toString() {
|
|
5755
|
-
return `${typeof this.q[0]}:
|
|
5756
|
-
` + this.q.map((i) => ` ${i.qualifiedID}: ${i.status}`).join("\n");
|
|
5757
|
-
}
|
|
5758
|
-
};
|
|
5759
6559
|
var SessionController = class extends Loggable {
|
|
5760
6560
|
_className = "SessionController";
|
|
6561
|
+
services;
|
|
6562
|
+
srsService;
|
|
6563
|
+
eloService;
|
|
6564
|
+
hydrationService;
|
|
5761
6565
|
sources;
|
|
6566
|
+
// dataLayer and getViewComponent now injected into CardHydrationService
|
|
5762
6567
|
_sessionRecord = [];
|
|
5763
6568
|
set sessionRecord(r) {
|
|
5764
6569
|
this._sessionRecord = r;
|
|
5765
6570
|
}
|
|
6571
|
+
// Session card stores
|
|
6572
|
+
_currentCard = null;
|
|
5766
6573
|
reviewQ = new ItemQueue();
|
|
5767
6574
|
newQ = new ItemQueue();
|
|
5768
6575
|
failedQ = new ItemQueue();
|
|
5769
|
-
|
|
5770
|
-
/**
|
|
5771
|
-
* Indicates whether the session has been initialized - eg, the
|
|
5772
|
-
* queues have been populated.
|
|
5773
|
-
*/
|
|
5774
|
-
_isInitialized = false;
|
|
6576
|
+
// END Session card stores
|
|
5775
6577
|
startTime;
|
|
5776
6578
|
endTime;
|
|
5777
6579
|
_secondsRemaining;
|
|
@@ -5789,8 +6591,20 @@ var SessionController = class extends Loggable {
|
|
|
5789
6591
|
/**
|
|
5790
6592
|
*
|
|
5791
6593
|
*/
|
|
5792
|
-
constructor(sources, time) {
|
|
6594
|
+
constructor(sources, time, dataLayer, getViewComponent) {
|
|
5793
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
|
+
};
|
|
5794
6608
|
this.sources = sources;
|
|
5795
6609
|
this.startTime = /* @__PURE__ */ new Date();
|
|
5796
6610
|
this._secondsRemaining = time;
|
|
@@ -5801,7 +6615,7 @@ var SessionController = class extends Loggable {
|
|
|
5801
6615
|
}
|
|
5802
6616
|
tick() {
|
|
5803
6617
|
this._secondsRemaining = Math.floor((this.endTime.valueOf() - Date.now()) / 1e3);
|
|
5804
|
-
if (this._secondsRemaining
|
|
6618
|
+
if (this._secondsRemaining <= 0) {
|
|
5805
6619
|
clearInterval(this._intervalHandle);
|
|
5806
6620
|
}
|
|
5807
6621
|
}
|
|
@@ -5845,7 +6659,7 @@ var SessionController = class extends Loggable {
|
|
|
5845
6659
|
} catch (e) {
|
|
5846
6660
|
this.error("Error preparing study session:", e);
|
|
5847
6661
|
}
|
|
5848
|
-
this.
|
|
6662
|
+
await this.hydrationService.ensureHydratedCards();
|
|
5849
6663
|
this._intervalHandle = setInterval(() => {
|
|
5850
6664
|
this.tick();
|
|
5851
6665
|
}, 1e3);
|
|
@@ -5883,12 +6697,8 @@ var SessionController = class extends Loggable {
|
|
|
5883
6697
|
}
|
|
5884
6698
|
}
|
|
5885
6699
|
let report = "Review session created with:\n";
|
|
5886
|
-
|
|
5887
|
-
|
|
5888
|
-
this.reviewQ.add(card);
|
|
5889
|
-
report += ` ${card.qualifiedID}}
|
|
5890
|
-
`;
|
|
5891
|
-
}
|
|
6700
|
+
this.reviewQ.addAll(dueCards, (c) => c.cardID);
|
|
6701
|
+
report += dueCards.map((card) => `Card ${card.courseID}::${card.cardID} `).join("\n");
|
|
5892
6702
|
this.log(report);
|
|
5893
6703
|
}
|
|
5894
6704
|
async getNewCards(n = 10) {
|
|
@@ -5903,36 +6713,32 @@ var SessionController = class extends Loggable {
|
|
|
5903
6713
|
for (let i = 0; i < newContent.length; i++) {
|
|
5904
6714
|
if (newContent[i].length > 0) {
|
|
5905
6715
|
const item = newContent[i].splice(0, 1)[0];
|
|
5906
|
-
this.log(`Adding new card: ${item.
|
|
5907
|
-
this.newQ.add(item);
|
|
6716
|
+
this.log(`Adding new card: ${item.courseID}::${item.cardID}`);
|
|
6717
|
+
this.newQ.add(item, item.cardID);
|
|
5908
6718
|
n--;
|
|
5909
6719
|
}
|
|
5910
6720
|
}
|
|
5911
6721
|
}
|
|
5912
6722
|
}
|
|
5913
|
-
|
|
5914
|
-
const item = this.newQ.dequeue();
|
|
5915
|
-
if (this._isInitialized && this.newQ.length < 5) {
|
|
5916
|
-
void this.getNewCards();
|
|
5917
|
-
}
|
|
5918
|
-
return item;
|
|
5919
|
-
}
|
|
5920
|
-
nextCard(action = "dismiss-success") {
|
|
5921
|
-
this.dismissCurrentCard(action);
|
|
6723
|
+
_selectNextItemToHydrate() {
|
|
5922
6724
|
const choice = Math.random();
|
|
5923
6725
|
let newBound = 0.1;
|
|
5924
6726
|
let reviewBound = 0.75;
|
|
5925
6727
|
if (this.reviewQ.length === 0 && this.failedQ.length === 0 && this.newQ.length === 0) {
|
|
5926
|
-
|
|
5927
|
-
return this._currentCard;
|
|
6728
|
+
return null;
|
|
5928
6729
|
}
|
|
5929
6730
|
if (this._secondsRemaining < 2 && this.failedQ.length === 0) {
|
|
5930
|
-
|
|
5931
|
-
|
|
6731
|
+
return null;
|
|
6732
|
+
}
|
|
6733
|
+
if (this._secondsRemaining <= 0) {
|
|
6734
|
+
if (this.failedQ.length > 0) {
|
|
6735
|
+
return this.failedQ.peek(0);
|
|
6736
|
+
} else {
|
|
6737
|
+
return null;
|
|
6738
|
+
}
|
|
5932
6739
|
}
|
|
5933
6740
|
if (this.newQ.dequeueCount < this.sources.length && this.newQ.length) {
|
|
5934
|
-
|
|
5935
|
-
return this._currentCard;
|
|
6741
|
+
return this.newQ.peek(0);
|
|
5936
6742
|
}
|
|
5937
6743
|
const cleanupTime = this.estimateCleanupTime();
|
|
5938
6744
|
const reviewTime = this.estimateReviewTime();
|
|
@@ -5947,9 +6753,6 @@ var SessionController = class extends Loggable {
|
|
|
5947
6753
|
newBound = 0.01;
|
|
5948
6754
|
reviewBound = 0.1;
|
|
5949
6755
|
}
|
|
5950
|
-
if (this.failedQ.length === 1 && action === "marked-failed") {
|
|
5951
|
-
reviewBound = 1;
|
|
5952
|
-
}
|
|
5953
6756
|
if (this.failedQ.length === 0) {
|
|
5954
6757
|
reviewBound = 1;
|
|
5955
6758
|
}
|
|
@@ -5957,123 +6760,110 @@ var SessionController = class extends Loggable {
|
|
|
5957
6760
|
newBound = reviewBound;
|
|
5958
6761
|
}
|
|
5959
6762
|
if (choice < newBound && this.newQ.length) {
|
|
5960
|
-
|
|
6763
|
+
return this.newQ.peek(0);
|
|
5961
6764
|
} else if (choice < reviewBound && this.reviewQ.length) {
|
|
5962
|
-
|
|
6765
|
+
return this.reviewQ.peek(0);
|
|
5963
6766
|
} else if (this.failedQ.length) {
|
|
5964
|
-
|
|
6767
|
+
return this.failedQ.peek(0);
|
|
5965
6768
|
} else {
|
|
5966
6769
|
this.log(`No more cards available for the session!`);
|
|
6770
|
+
return null;
|
|
6771
|
+
}
|
|
6772
|
+
}
|
|
6773
|
+
async nextCard(action = "dismiss-success") {
|
|
6774
|
+
this.dismissCurrentCard(action);
|
|
6775
|
+
if (this._secondsRemaining <= 0 && this.failedQ.length === 0) {
|
|
5967
6776
|
this._currentCard = null;
|
|
6777
|
+
return null;
|
|
6778
|
+
}
|
|
6779
|
+
let card = this.hydrationService.dequeueHydratedCard();
|
|
6780
|
+
if (!card && this.hasAvailableCards()) {
|
|
6781
|
+
card = await this.hydrationService.waitForHydratedCard();
|
|
5968
6782
|
}
|
|
5969
|
-
|
|
6783
|
+
await this.hydrationService.ensureHydratedCards();
|
|
6784
|
+
if (card) {
|
|
6785
|
+
this._currentCard = card;
|
|
6786
|
+
} else {
|
|
6787
|
+
this._currentCard = null;
|
|
6788
|
+
}
|
|
6789
|
+
return card;
|
|
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
|
+
);
|
|
5970
6820
|
}
|
|
5971
6821
|
dismissCurrentCard(action = "dismiss-success") {
|
|
5972
6822
|
if (this._currentCard) {
|
|
5973
6823
|
if (action === "dismiss-success") {
|
|
5974
6824
|
} else if (action === "marked-failed") {
|
|
6825
|
+
this.hydrationService.cacheFailedCard(this._currentCard);
|
|
5975
6826
|
let failedItem;
|
|
5976
|
-
if (isReview(this._currentCard)) {
|
|
6827
|
+
if (isReview(this._currentCard.item)) {
|
|
5977
6828
|
failedItem = {
|
|
5978
|
-
cardID: this._currentCard.cardID,
|
|
5979
|
-
courseID: this._currentCard.courseID,
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
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,
|
|
5983
6833
|
status: "failed-review",
|
|
5984
|
-
reviewID: this._currentCard.reviewID
|
|
6834
|
+
reviewID: this._currentCard.item.reviewID
|
|
5985
6835
|
};
|
|
5986
6836
|
} else {
|
|
5987
6837
|
failedItem = {
|
|
5988
|
-
cardID: this._currentCard.cardID,
|
|
5989
|
-
courseID: this._currentCard.courseID,
|
|
5990
|
-
|
|
5991
|
-
|
|
5992
|
-
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,
|
|
5993
6842
|
status: "failed-new"
|
|
5994
6843
|
};
|
|
5995
6844
|
}
|
|
5996
|
-
this.failedQ.add(failedItem);
|
|
6845
|
+
this.failedQ.add(failedItem, failedItem.cardID);
|
|
5997
6846
|
} else if (action === "dismiss-error") {
|
|
5998
6847
|
} else if (action === "dismiss-failed") {
|
|
5999
6848
|
}
|
|
6000
6849
|
}
|
|
6001
6850
|
}
|
|
6002
|
-
|
|
6003
|
-
|
|
6004
|
-
// src/study/SpacedRepetition.ts
|
|
6005
|
-
init_util();
|
|
6006
|
-
var import_moment6 = __toESM(require("moment"));
|
|
6007
|
-
init_logger();
|
|
6008
|
-
var duration = import_moment6.default.duration;
|
|
6009
|
-
function newInterval(user, cardHistory) {
|
|
6010
|
-
if (areQuestionRecords(cardHistory)) {
|
|
6011
|
-
return newQuestionInterval(user, cardHistory);
|
|
6012
|
-
} else {
|
|
6013
|
-
return 1e5;
|
|
6014
|
-
}
|
|
6015
|
-
}
|
|
6016
|
-
function newQuestionInterval(user, cardHistory) {
|
|
6017
|
-
const records = cardHistory.records;
|
|
6018
|
-
const currentAttempt = records[records.length - 1];
|
|
6019
|
-
const lastInterval = lastSuccessfulInterval(records);
|
|
6020
|
-
if (lastInterval > cardHistory.bestInterval) {
|
|
6021
|
-
cardHistory.bestInterval = lastInterval;
|
|
6022
|
-
void user.update(cardHistory._id, {
|
|
6023
|
-
bestInterval: lastInterval
|
|
6024
|
-
});
|
|
6851
|
+
hasAvailableCards() {
|
|
6852
|
+
return this.reviewQ.length > 0 || this.newQ.length > 0 || this.failedQ.length > 0;
|
|
6025
6853
|
}
|
|
6026
|
-
|
|
6027
|
-
|
|
6028
|
-
|
|
6029
|
-
|
|
6030
|
-
|
|
6031
|
-
|
|
6032
|
-
if (
|
|
6033
|
-
|
|
6034
|
-
logger.debug(`Weighted average interval calculation:
|
|
6035
|
-
(${cardHistory.lapses} * ${interval} + ${cardHistory.streak} * ${cardHistory.bestInterval}) / (${cardHistory.lapses} + ${cardHistory.streak}) = ${ret}`);
|
|
6036
|
-
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);
|
|
6037
6862
|
} else {
|
|
6038
|
-
|
|
6039
|
-
}
|
|
6040
|
-
} else {
|
|
6041
|
-
return 0;
|
|
6042
|
-
}
|
|
6043
|
-
}
|
|
6044
|
-
function lastSuccessfulInterval(cardHistory) {
|
|
6045
|
-
for (let i = cardHistory.length - 1; i >= 1; i--) {
|
|
6046
|
-
if (cardHistory[i].priorAttemps === 0 && cardHistory[i].isCorrect) {
|
|
6047
|
-
const lastInterval = secondsBetween(cardHistory[i - 1].timeStamp, cardHistory[i].timeStamp);
|
|
6048
|
-
const ret = Math.max(lastInterval, 20 * 60 * 60);
|
|
6049
|
-
logger.debug(`Last interval w/ this card was: ${lastInterval}s, returning ${ret}s`);
|
|
6050
|
-
return ret;
|
|
6863
|
+
this.failedQ.dequeue((queueItem) => queueItem.cardID);
|
|
6051
6864
|
}
|
|
6052
6865
|
}
|
|
6053
|
-
|
|
6054
|
-
}
|
|
6055
|
-
function getStreak(records) {
|
|
6056
|
-
let streak = 0;
|
|
6057
|
-
let index = records.length - 1;
|
|
6058
|
-
while (index >= 0 && records[index].isCorrect) {
|
|
6059
|
-
index--;
|
|
6060
|
-
streak++;
|
|
6061
|
-
}
|
|
6062
|
-
return streak;
|
|
6063
|
-
}
|
|
6064
|
-
function getLapses(records) {
|
|
6065
|
-
return records.filter((r) => r.isCorrect === false).length;
|
|
6066
|
-
}
|
|
6067
|
-
function getInitialInterval(cardHistory) {
|
|
6068
|
-
logger.warn(`history of length: ${cardHistory.length} ignored!`);
|
|
6069
|
-
return 60 * 60 * 24 * 3;
|
|
6070
|
-
}
|
|
6071
|
-
function secondsBetween(start, end) {
|
|
6072
|
-
start = (0, import_moment6.default)(start);
|
|
6073
|
-
end = (0, import_moment6.default)(end);
|
|
6074
|
-
const ret = duration(end.diff(start)).asSeconds();
|
|
6075
|
-
return ret;
|
|
6076
|
-
}
|
|
6866
|
+
};
|
|
6077
6867
|
|
|
6078
6868
|
// src/index.ts
|
|
6079
6869
|
init_factory();
|