@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.mjs
CHANGED
|
@@ -165,9 +165,9 @@ var init_pouchdb_setup = __esm({
|
|
|
165
165
|
PouchDB.plugin(PouchDBFind);
|
|
166
166
|
PouchDB.plugin(PouchDBAuth);
|
|
167
167
|
PouchDB.defaults({
|
|
168
|
-
ajax: {
|
|
169
|
-
|
|
170
|
-
}
|
|
168
|
+
// ajax: {
|
|
169
|
+
// timeout: 60000,
|
|
170
|
+
// },
|
|
171
171
|
});
|
|
172
172
|
pouchdb_setup_default = PouchDB;
|
|
173
173
|
}
|
|
@@ -445,42 +445,58 @@ var init_updateQueue = __esm({
|
|
|
445
445
|
async applyUpdates(id) {
|
|
446
446
|
logger.debug(`Applying updates on doc: ${id}`);
|
|
447
447
|
if (this.inprogressUpdates[id]) {
|
|
448
|
-
|
|
448
|
+
while (this.inprogressUpdates[id]) {
|
|
449
|
+
await new Promise((resolve) => setTimeout(resolve, Math.random() * 50));
|
|
450
|
+
}
|
|
449
451
|
return this.applyUpdates(id);
|
|
450
452
|
} else {
|
|
451
453
|
if (this.pendingUpdates[id] && this.pendingUpdates[id].length > 0) {
|
|
452
454
|
this.inprogressUpdates[id] = true;
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
455
|
+
const MAX_RETRIES = 5;
|
|
456
|
+
for (let i = 0; i < MAX_RETRIES; i++) {
|
|
457
|
+
try {
|
|
458
|
+
const doc = await this.readDB.get(id);
|
|
459
|
+
logger.debug(`Retrieved doc: ${id}`);
|
|
460
|
+
let updatedDoc = { ...doc };
|
|
461
|
+
const updatesToApply = [...this.pendingUpdates[id]];
|
|
462
|
+
for (const update of updatesToApply) {
|
|
463
|
+
if (typeof update === "function") {
|
|
464
|
+
updatedDoc = { ...updatedDoc, ...update(updatedDoc) };
|
|
465
|
+
} else {
|
|
466
|
+
updatedDoc = {
|
|
467
|
+
...updatedDoc,
|
|
468
|
+
...update
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
await this.writeDB.put(updatedDoc);
|
|
473
|
+
logger.debug(`Put doc: ${id}`);
|
|
474
|
+
this.pendingUpdates[id].splice(0, updatesToApply.length);
|
|
475
|
+
if (this.pendingUpdates[id].length === 0) {
|
|
476
|
+
this.inprogressUpdates[id] = false;
|
|
477
|
+
delete this.inprogressUpdates[id];
|
|
460
478
|
} else {
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
479
|
+
return this.applyUpdates(id);
|
|
480
|
+
}
|
|
481
|
+
return updatedDoc;
|
|
482
|
+
} catch (e) {
|
|
483
|
+
if (e.name === "conflict" && i < MAX_RETRIES - 1) {
|
|
484
|
+
logger.warn(`Conflict on update for doc ${id}, retry #${i + 1}`);
|
|
485
|
+
await new Promise((res) => setTimeout(res, 50 * Math.random()));
|
|
486
|
+
} else if (e.name === "not_found" && i === 0) {
|
|
487
|
+
logger.warn(`Update failed for ${id} - does not exist. Throwing to caller.`);
|
|
488
|
+
throw e;
|
|
489
|
+
} else {
|
|
490
|
+
delete this.inprogressUpdates[id];
|
|
491
|
+
if (this.pendingUpdates[id]) {
|
|
492
|
+
delete this.pendingUpdates[id];
|
|
493
|
+
}
|
|
494
|
+
logger.error(`Error on attemped update (retry ${i}): ${JSON.stringify(e)}`);
|
|
495
|
+
throw e;
|
|
465
496
|
}
|
|
466
497
|
}
|
|
467
|
-
await this.writeDB.put(doc);
|
|
468
|
-
logger.debug(`Put doc: ${id}`);
|
|
469
|
-
if (this.pendingUpdates[id].length === 0) {
|
|
470
|
-
this.inprogressUpdates[id] = false;
|
|
471
|
-
delete this.inprogressUpdates[id];
|
|
472
|
-
} else {
|
|
473
|
-
return this.applyUpdates(id);
|
|
474
|
-
}
|
|
475
|
-
return doc;
|
|
476
|
-
} catch (e) {
|
|
477
|
-
delete this.inprogressUpdates[id];
|
|
478
|
-
if (this.pendingUpdates[id]) {
|
|
479
|
-
delete this.pendingUpdates[id];
|
|
480
|
-
}
|
|
481
|
-
logger.error(`Error on attemped update: ${JSON.stringify(e)}`);
|
|
482
|
-
throw e;
|
|
483
498
|
}
|
|
499
|
+
throw new Error(`UpdateQueue failed for doc ${id} after ${MAX_RETRIES} retries.`);
|
|
484
500
|
} else {
|
|
485
501
|
throw new Error(`Empty Updates Queue Triggered`);
|
|
486
502
|
}
|
|
@@ -735,7 +751,7 @@ function getCourseDB(courseID) {
|
|
|
735
751
|
const dbName = `coursedb-${courseID}`;
|
|
736
752
|
return new pouchdb_setup_default(
|
|
737
753
|
ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + dbName,
|
|
738
|
-
|
|
754
|
+
createPouchDBConfig()
|
|
739
755
|
);
|
|
740
756
|
}
|
|
741
757
|
var AlreadyTaggedErr;
|
|
@@ -857,6 +873,7 @@ var init_courseLookupDB = __esm({
|
|
|
857
873
|
const doc = await _CourseLookup._db.get(courseID);
|
|
858
874
|
return await _CourseLookup._db.remove(doc);
|
|
859
875
|
}
|
|
876
|
+
// [ ] rename to allCourses()
|
|
860
877
|
static async allCourseWare() {
|
|
861
878
|
const resp = await _CourseLookup._db.allDocs({
|
|
862
879
|
include_docs: true
|
|
@@ -927,13 +944,16 @@ var init_elo = __esm({
|
|
|
927
944
|
}
|
|
928
945
|
async getNewCards(limit = 99) {
|
|
929
946
|
const activeCards = await this.user.getActiveCards();
|
|
930
|
-
return (await this.course.getCardsCenteredAtELO(
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
947
|
+
return (await this.course.getCardsCenteredAtELO(
|
|
948
|
+
{ limit, elo: "user" },
|
|
949
|
+
(c) => {
|
|
950
|
+
if (activeCards.some((ac) => c.cardID === ac.cardID)) {
|
|
951
|
+
return false;
|
|
952
|
+
} else {
|
|
953
|
+
return true;
|
|
954
|
+
}
|
|
935
955
|
}
|
|
936
|
-
|
|
956
|
+
)).map((c) => {
|
|
937
957
|
return {
|
|
938
958
|
...c,
|
|
939
959
|
status: "new"
|
|
@@ -944,12 +964,74 @@ var init_elo = __esm({
|
|
|
944
964
|
}
|
|
945
965
|
});
|
|
946
966
|
|
|
967
|
+
// src/core/navigators/hardcodedOrder.ts
|
|
968
|
+
var hardcodedOrder_exports = {};
|
|
969
|
+
__export(hardcodedOrder_exports, {
|
|
970
|
+
default: () => HardcodedOrderNavigator
|
|
971
|
+
});
|
|
972
|
+
var HardcodedOrderNavigator;
|
|
973
|
+
var init_hardcodedOrder = __esm({
|
|
974
|
+
"src/core/navigators/hardcodedOrder.ts"() {
|
|
975
|
+
"use strict";
|
|
976
|
+
init_navigators();
|
|
977
|
+
init_logger();
|
|
978
|
+
HardcodedOrderNavigator = class extends ContentNavigator {
|
|
979
|
+
orderedCardIds = [];
|
|
980
|
+
user;
|
|
981
|
+
course;
|
|
982
|
+
constructor(user, course, strategyData) {
|
|
983
|
+
super();
|
|
984
|
+
this.user = user;
|
|
985
|
+
this.course = course;
|
|
986
|
+
if (strategyData.serializedData) {
|
|
987
|
+
try {
|
|
988
|
+
this.orderedCardIds = JSON.parse(strategyData.serializedData);
|
|
989
|
+
} catch (e) {
|
|
990
|
+
logger.error("Failed to parse serializedData for HardcodedOrderNavigator", e);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
async getPendingReviews() {
|
|
995
|
+
const reviews = await this.user.getPendingReviews(this.course.getCourseID());
|
|
996
|
+
return reviews.map((r) => {
|
|
997
|
+
return {
|
|
998
|
+
...r,
|
|
999
|
+
contentSourceType: "course",
|
|
1000
|
+
contentSourceID: this.course.getCourseID(),
|
|
1001
|
+
cardID: r.cardId,
|
|
1002
|
+
courseID: r.courseId,
|
|
1003
|
+
reviewID: r._id,
|
|
1004
|
+
status: "review"
|
|
1005
|
+
};
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
async getNewCards(limit = 99) {
|
|
1009
|
+
const activeCardIds = (await this.user.getActiveCards()).map((c) => c.cardID);
|
|
1010
|
+
const newCardIds = this.orderedCardIds.filter(
|
|
1011
|
+
(cardId) => !activeCardIds.includes(cardId)
|
|
1012
|
+
);
|
|
1013
|
+
const cardsToReturn = newCardIds.slice(0, limit);
|
|
1014
|
+
return cardsToReturn.map((cardId) => {
|
|
1015
|
+
return {
|
|
1016
|
+
cardID: cardId,
|
|
1017
|
+
courseID: this.course.getCourseID(),
|
|
1018
|
+
contentSourceType: "course",
|
|
1019
|
+
contentSourceID: this.course.getCourseID(),
|
|
1020
|
+
status: "new"
|
|
1021
|
+
};
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
});
|
|
1027
|
+
|
|
947
1028
|
// import("./**/*") in src/core/navigators/index.ts
|
|
948
1029
|
var globImport;
|
|
949
1030
|
var init_ = __esm({
|
|
950
1031
|
'import("./**/*") in src/core/navigators/index.ts'() {
|
|
951
1032
|
globImport = __glob({
|
|
952
1033
|
"./elo.ts": () => Promise.resolve().then(() => (init_elo(), elo_exports)),
|
|
1034
|
+
"./hardcodedOrder.ts": () => Promise.resolve().then(() => (init_hardcodedOrder(), hardcodedOrder_exports)),
|
|
953
1035
|
"./index.ts": () => Promise.resolve().then(() => (init_navigators(), navigators_exports))
|
|
954
1036
|
});
|
|
955
1037
|
}
|
|
@@ -969,6 +1051,7 @@ var init_navigators = __esm({
|
|
|
969
1051
|
init_();
|
|
970
1052
|
Navigators = /* @__PURE__ */ ((Navigators2) => {
|
|
971
1053
|
Navigators2["ELO"] = "elo";
|
|
1054
|
+
Navigators2["HARDCODED"] = "hardcodedOrder";
|
|
972
1055
|
return Navigators2;
|
|
973
1056
|
})(Navigators || {});
|
|
974
1057
|
ContentNavigator = class {
|
|
@@ -981,7 +1064,7 @@ var init_navigators = __esm({
|
|
|
981
1064
|
static async create(user, course, strategyData) {
|
|
982
1065
|
const implementingClass = strategyData.implementingClass;
|
|
983
1066
|
let NavigatorImpl;
|
|
984
|
-
const variations = ["", ".js", "
|
|
1067
|
+
const variations = [".ts", ".js", ""];
|
|
985
1068
|
for (const ext of variations) {
|
|
986
1069
|
try {
|
|
987
1070
|
const module = await globImport(`./${implementingClass}${ext}`);
|
|
@@ -1243,6 +1326,23 @@ var init_courseDB = __esm({
|
|
|
1243
1326
|
if (!doc.docType || !(doc.docType === "CARD" /* CARD */)) {
|
|
1244
1327
|
throw new Error(`failed to remove ${id} from course ${this.id}. id does not point to a card`);
|
|
1245
1328
|
}
|
|
1329
|
+
try {
|
|
1330
|
+
const appliedTags = await this.getAppliedTags(id);
|
|
1331
|
+
const results = await Promise.allSettled(
|
|
1332
|
+
appliedTags.rows.map(async (tagRow) => {
|
|
1333
|
+
const tagId = tagRow.id;
|
|
1334
|
+
await this.removeTagFromCard(id, tagId);
|
|
1335
|
+
})
|
|
1336
|
+
);
|
|
1337
|
+
results.forEach((result, index) => {
|
|
1338
|
+
if (result.status === "rejected") {
|
|
1339
|
+
const tagId = appliedTags.rows[index].id;
|
|
1340
|
+
logger.error(`Failed to remove card ${id} from tag ${tagId}: ${result.reason}`);
|
|
1341
|
+
}
|
|
1342
|
+
});
|
|
1343
|
+
} catch (error) {
|
|
1344
|
+
logger.error(`Error removing card ${id} from tags: ${error}`);
|
|
1345
|
+
}
|
|
1246
1346
|
return this.db.remove(doc);
|
|
1247
1347
|
}
|
|
1248
1348
|
async getCardDisplayableDataIDs(id) {
|
|
@@ -1290,7 +1390,13 @@ var init_courseDB = __esm({
|
|
|
1290
1390
|
} else {
|
|
1291
1391
|
return s;
|
|
1292
1392
|
}
|
|
1293
|
-
}).map((c) =>
|
|
1393
|
+
}).map((c) => {
|
|
1394
|
+
return {
|
|
1395
|
+
courseID: this.id,
|
|
1396
|
+
cardID: c.id,
|
|
1397
|
+
elo: c.key
|
|
1398
|
+
};
|
|
1399
|
+
});
|
|
1294
1400
|
const str = `below:
|
|
1295
1401
|
${below.rows.map((r) => ` ${r.id}-${r.key}
|
|
1296
1402
|
`)}
|
|
@@ -1345,7 +1451,13 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
1345
1451
|
}
|
|
1346
1452
|
}
|
|
1347
1453
|
async addTagToCard(cardId, tagId, updateELO) {
|
|
1348
|
-
return await addTagToCard(
|
|
1454
|
+
return await addTagToCard(
|
|
1455
|
+
this.id,
|
|
1456
|
+
cardId,
|
|
1457
|
+
tagId,
|
|
1458
|
+
(await this._getCurrentUser()).getUsername(),
|
|
1459
|
+
updateELO
|
|
1460
|
+
);
|
|
1349
1461
|
}
|
|
1350
1462
|
async removeTagFromCard(cardId, tagId) {
|
|
1351
1463
|
return await removeTagFromCard(this.id, cardId, tagId);
|
|
@@ -1414,23 +1526,9 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
1414
1526
|
////////////////////////////////////
|
|
1415
1527
|
getNavigationStrategy(id) {
|
|
1416
1528
|
logger.debug(`[courseDB] Getting navigation strategy: ${id}`);
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
name: "ELO",
|
|
1421
|
-
description: "ELO-based navigation strategy for ordering content by difficulty",
|
|
1422
|
-
implementingClass: "elo" /* ELO */,
|
|
1423
|
-
course: this.id,
|
|
1424
|
-
serializedData: ""
|
|
1425
|
-
// serde is a noop for ELO navigator.
|
|
1426
|
-
};
|
|
1427
|
-
return Promise.resolve(strategy);
|
|
1428
|
-
}
|
|
1429
|
-
getAllNavigationStrategies() {
|
|
1430
|
-
logger.debug("[courseDB] Returning hard-coded navigation strategies");
|
|
1431
|
-
const strategies = [
|
|
1432
|
-
{
|
|
1433
|
-
id: "ELO",
|
|
1529
|
+
if (id == "") {
|
|
1530
|
+
const strategy = {
|
|
1531
|
+
_id: "NAVIGATION_STRATEGY-ELO",
|
|
1434
1532
|
docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
|
|
1435
1533
|
name: "ELO",
|
|
1436
1534
|
description: "ELO-based navigation strategy for ordering content by difficulty",
|
|
@@ -1438,14 +1536,25 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
1438
1536
|
course: this.id,
|
|
1439
1537
|
serializedData: ""
|
|
1440
1538
|
// serde is a noop for ELO navigator.
|
|
1441
|
-
}
|
|
1442
|
-
|
|
1443
|
-
|
|
1539
|
+
};
|
|
1540
|
+
return Promise.resolve(strategy);
|
|
1541
|
+
} else {
|
|
1542
|
+
return this.db.get(id);
|
|
1543
|
+
}
|
|
1444
1544
|
}
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1545
|
+
async getAllNavigationStrategies() {
|
|
1546
|
+
const prefix = DocTypePrefixes["NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */];
|
|
1547
|
+
const result = await this.db.allDocs({
|
|
1548
|
+
startkey: prefix,
|
|
1549
|
+
endkey: `${prefix}\uFFF0`,
|
|
1550
|
+
include_docs: true
|
|
1551
|
+
});
|
|
1552
|
+
return result.rows.map((row) => row.doc);
|
|
1553
|
+
}
|
|
1554
|
+
async addNavigationStrategy(data) {
|
|
1555
|
+
logger.debug(`[courseDB] Adding navigation strategy: ${data._id}`);
|
|
1556
|
+
return this.db.put(data).then(() => {
|
|
1557
|
+
});
|
|
1449
1558
|
}
|
|
1450
1559
|
updateNavigationStrategy(id, data) {
|
|
1451
1560
|
logger.debug(`[courseDB] Updating navigation strategy: ${id}`);
|
|
@@ -1453,9 +1562,32 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
1453
1562
|
return Promise.resolve();
|
|
1454
1563
|
}
|
|
1455
1564
|
async surfaceNavigationStrategy() {
|
|
1565
|
+
try {
|
|
1566
|
+
const config = await this.getCourseConfig();
|
|
1567
|
+
if (config.defaultNavigationStrategyId) {
|
|
1568
|
+
try {
|
|
1569
|
+
const strategy = await this.getNavigationStrategy(config.defaultNavigationStrategyId);
|
|
1570
|
+
if (strategy) {
|
|
1571
|
+
logger.debug(`Surfacing strategy ${strategy.name} from course config`);
|
|
1572
|
+
return strategy;
|
|
1573
|
+
}
|
|
1574
|
+
} catch (e) {
|
|
1575
|
+
logger.warn(
|
|
1576
|
+
// @ts-expect-error tmp: defaultNavigationStrategyId property does not yet exist
|
|
1577
|
+
`Failed to load strategy '${config.defaultNavigationStrategyId}' specified in course config. Falling back to ELO.`,
|
|
1578
|
+
e
|
|
1579
|
+
);
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
} catch (e) {
|
|
1583
|
+
logger.warn(
|
|
1584
|
+
"Could not retrieve course config to determine navigation strategy. Falling back to ELO.",
|
|
1585
|
+
e
|
|
1586
|
+
);
|
|
1587
|
+
}
|
|
1456
1588
|
logger.warn(`Returning hard-coded default ELO navigator`);
|
|
1457
1589
|
const ret = {
|
|
1458
|
-
|
|
1590
|
+
_id: "NAVIGATION_STRATEGY-ELO",
|
|
1459
1591
|
docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
|
|
1460
1592
|
name: "ELO",
|
|
1461
1593
|
description: "ELO-based navigation strategy",
|
|
@@ -1538,17 +1670,93 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
1538
1670
|
selectedCards.push(card);
|
|
1539
1671
|
}
|
|
1540
1672
|
return selectedCards.map((c) => {
|
|
1541
|
-
const split = c.split("-");
|
|
1542
1673
|
return {
|
|
1543
1674
|
courseID: this.id,
|
|
1544
|
-
cardID:
|
|
1545
|
-
qualifiedID: `${split[0]}-${split[1]}`,
|
|
1675
|
+
cardID: c.cardID,
|
|
1546
1676
|
contentSourceType: "course",
|
|
1547
1677
|
contentSourceID: this.id,
|
|
1678
|
+
elo: c.elo,
|
|
1548
1679
|
status: "new"
|
|
1549
1680
|
};
|
|
1550
1681
|
});
|
|
1551
1682
|
}
|
|
1683
|
+
// Admin search methods
|
|
1684
|
+
async searchCards(query) {
|
|
1685
|
+
logger.log(`[CourseDB ${this.id}] Searching for: "${query}"`);
|
|
1686
|
+
let displayableData;
|
|
1687
|
+
try {
|
|
1688
|
+
displayableData = await this.db.find({
|
|
1689
|
+
selector: {
|
|
1690
|
+
docType: "DISPLAYABLE_DATA",
|
|
1691
|
+
"data.0.data": { $regex: `.*${query}.*` }
|
|
1692
|
+
}
|
|
1693
|
+
});
|
|
1694
|
+
logger.log(`[CourseDB ${this.id}] Regex search on data[0].data successful`);
|
|
1695
|
+
} catch (regexError) {
|
|
1696
|
+
logger.log(
|
|
1697
|
+
`[CourseDB ${this.id}] Regex search failed, falling back to manual search:`,
|
|
1698
|
+
regexError
|
|
1699
|
+
);
|
|
1700
|
+
const allDisplayable = await this.db.find({
|
|
1701
|
+
selector: {
|
|
1702
|
+
docType: "DISPLAYABLE_DATA"
|
|
1703
|
+
}
|
|
1704
|
+
});
|
|
1705
|
+
logger.log(
|
|
1706
|
+
`[CourseDB ${this.id}] Retrieved ${allDisplayable.docs.length} documents for manual filtering`
|
|
1707
|
+
);
|
|
1708
|
+
displayableData = {
|
|
1709
|
+
docs: allDisplayable.docs.filter((doc) => {
|
|
1710
|
+
const docString = JSON.stringify(doc).toLowerCase();
|
|
1711
|
+
const match = docString.includes(query.toLowerCase());
|
|
1712
|
+
if (match) {
|
|
1713
|
+
logger.log(`[CourseDB ${this.id}] Manual match found in document: ${doc._id}`);
|
|
1714
|
+
}
|
|
1715
|
+
return match;
|
|
1716
|
+
})
|
|
1717
|
+
};
|
|
1718
|
+
}
|
|
1719
|
+
logger.log(
|
|
1720
|
+
`[CourseDB ${this.id}] Found ${displayableData.docs.length} displayable data documents`
|
|
1721
|
+
);
|
|
1722
|
+
if (displayableData.docs.length === 0) {
|
|
1723
|
+
const allDisplayableData = await this.db.find({
|
|
1724
|
+
selector: {
|
|
1725
|
+
docType: "DISPLAYABLE_DATA"
|
|
1726
|
+
},
|
|
1727
|
+
limit: 5
|
|
1728
|
+
// Just sample a few
|
|
1729
|
+
});
|
|
1730
|
+
logger.log(
|
|
1731
|
+
`[CourseDB ${this.id}] Sample displayable data:`,
|
|
1732
|
+
allDisplayableData.docs.map((d) => ({
|
|
1733
|
+
id: d._id,
|
|
1734
|
+
docType: d.docType,
|
|
1735
|
+
dataStructure: d.data ? Object.keys(d.data) : "no data field",
|
|
1736
|
+
dataContent: d.data,
|
|
1737
|
+
fullDoc: d
|
|
1738
|
+
}))
|
|
1739
|
+
);
|
|
1740
|
+
}
|
|
1741
|
+
const allResults = [];
|
|
1742
|
+
for (const dd of displayableData.docs) {
|
|
1743
|
+
const cards = await this.db.find({
|
|
1744
|
+
selector: {
|
|
1745
|
+
docType: "CARD",
|
|
1746
|
+
id_displayable_data: { $in: [dd._id] }
|
|
1747
|
+
}
|
|
1748
|
+
});
|
|
1749
|
+
logger.log(
|
|
1750
|
+
`[CourseDB ${this.id}] Displayable data ${dd._id} linked to ${cards.docs.length} cards`
|
|
1751
|
+
);
|
|
1752
|
+
allResults.push(...cards.docs);
|
|
1753
|
+
}
|
|
1754
|
+
logger.log(`[CourseDB ${this.id}] Total cards found: ${allResults.length}`);
|
|
1755
|
+
return allResults;
|
|
1756
|
+
}
|
|
1757
|
+
async find(request) {
|
|
1758
|
+
return this.db.find(request);
|
|
1759
|
+
}
|
|
1552
1760
|
};
|
|
1553
1761
|
}
|
|
1554
1762
|
});
|
|
@@ -1614,7 +1822,7 @@ var init_classroomDB2 = __esm({
|
|
|
1614
1822
|
const dbName = `classdb-student-${this._id}`;
|
|
1615
1823
|
this._db = new pouchdb_setup_default(
|
|
1616
1824
|
ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + dbName,
|
|
1617
|
-
|
|
1825
|
+
createPouchDBConfig()
|
|
1618
1826
|
);
|
|
1619
1827
|
try {
|
|
1620
1828
|
const cfg = await this._db.get(CLASSROOM_CONFIG);
|
|
@@ -1683,9 +1891,11 @@ var init_classroomDB2 = __esm({
|
|
|
1683
1891
|
ret.push(await getCourseDB2(content.courseID).get(content.cardID));
|
|
1684
1892
|
}
|
|
1685
1893
|
}
|
|
1686
|
-
logger.info(
|
|
1894
|
+
logger.info(
|
|
1895
|
+
`New Cards from classroom ${this._cfg.name}: ${ret.map((c) => `${c.courseID}-${c.cardID}`)}`
|
|
1896
|
+
);
|
|
1687
1897
|
return ret.filter((c) => {
|
|
1688
|
-
if (activeCards.some((ac) => c.
|
|
1898
|
+
if (activeCards.some((ac) => c.cardID === ac.cardID)) {
|
|
1689
1899
|
return false;
|
|
1690
1900
|
} else {
|
|
1691
1901
|
return true;
|
|
@@ -1704,11 +1914,11 @@ var init_classroomDB2 = __esm({
|
|
|
1704
1914
|
const stuDbName = `classdb-student-${this._id}`;
|
|
1705
1915
|
this._db = new pouchdb_setup_default(
|
|
1706
1916
|
ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + dbName,
|
|
1707
|
-
|
|
1917
|
+
createPouchDBConfig()
|
|
1708
1918
|
);
|
|
1709
1919
|
this._stuDb = new pouchdb_setup_default(
|
|
1710
1920
|
ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + stuDbName,
|
|
1711
|
-
|
|
1921
|
+
createPouchDBConfig()
|
|
1712
1922
|
);
|
|
1713
1923
|
try {
|
|
1714
1924
|
return this._db.get(CLASSROOM_CONFIG).then((cfg) => {
|
|
@@ -1793,7 +2003,7 @@ var init_adminDB2 = __esm({
|
|
|
1793
2003
|
constructor() {
|
|
1794
2004
|
this.usersDB = new pouchdb_setup_default(
|
|
1795
2005
|
ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + "_users",
|
|
1796
|
-
|
|
2006
|
+
createPouchDBConfig()
|
|
1797
2007
|
);
|
|
1798
2008
|
}
|
|
1799
2009
|
async getUsers() {
|
|
@@ -1855,9 +2065,10 @@ import fetch2 from "cross-fetch";
|
|
|
1855
2065
|
async function getCurrentSession() {
|
|
1856
2066
|
try {
|
|
1857
2067
|
if (ENV.COUCHDB_SERVER_URL === NOT_SET || ENV.COUCHDB_SERVER_PROTOCOL === NOT_SET) {
|
|
1858
|
-
throw new Error(
|
|
2068
|
+
throw new Error(`CouchDB server configuration not properly initialized. Protocol: "${ENV.COUCHDB_SERVER_PROTOCOL}", URL: "${ENV.COUCHDB_SERVER_URL}"`);
|
|
1859
2069
|
}
|
|
1860
|
-
const
|
|
2070
|
+
const baseUrl = ENV.COUCHDB_SERVER_URL.endsWith("/") ? ENV.COUCHDB_SERVER_URL.slice(0, -1) : ENV.COUCHDB_SERVER_URL;
|
|
2071
|
+
const url = `${ENV.COUCHDB_SERVER_PROTOCOL}://${baseUrl}/_session`;
|
|
1861
2072
|
logger.debug(`Attempting session check at: ${url}`);
|
|
1862
2073
|
const response = await fetch2(url, {
|
|
1863
2074
|
method: "GET",
|
|
@@ -1869,8 +2080,10 @@ async function getCurrentSession() {
|
|
|
1869
2080
|
const resp = await response.json();
|
|
1870
2081
|
return resp;
|
|
1871
2082
|
} catch (error) {
|
|
1872
|
-
|
|
1873
|
-
|
|
2083
|
+
const baseUrl = ENV.COUCHDB_SERVER_URL.endsWith("/") ? ENV.COUCHDB_SERVER_URL.slice(0, -1) : ENV.COUCHDB_SERVER_URL;
|
|
2084
|
+
const url = `${ENV.COUCHDB_SERVER_PROTOCOL}://${baseUrl}/_session`;
|
|
2085
|
+
logger.error(`Session check error attempting to connect to: ${url} - ${error}`);
|
|
2086
|
+
throw new Error(`Session check failed connecting to ${url}: ${error}`);
|
|
1874
2087
|
}
|
|
1875
2088
|
}
|
|
1876
2089
|
async function getLoggedInUsername() {
|
|
@@ -2063,7 +2276,7 @@ var init_CouchDBSyncStrategy = __esm({
|
|
|
2063
2276
|
log3(`Fetching user database: ${dbName} (${username})`);
|
|
2064
2277
|
const ret = new pouchdb_setup_default(
|
|
2065
2278
|
ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + dbName,
|
|
2066
|
-
|
|
2279
|
+
createPouchDBConfig()
|
|
2067
2280
|
);
|
|
2068
2281
|
if (guestAccount) {
|
|
2069
2282
|
updateGuestAccountExpirationDate(ret);
|
|
@@ -2078,10 +2291,29 @@ var init_CouchDBSyncStrategy = __esm({
|
|
|
2078
2291
|
import fetch3 from "cross-fetch";
|
|
2079
2292
|
import moment4 from "moment";
|
|
2080
2293
|
import process2 from "process";
|
|
2294
|
+
function createPouchDBConfig() {
|
|
2295
|
+
const hasExplicitCredentials = ENV.COUCHDB_USERNAME && ENV.COUCHDB_PASSWORD;
|
|
2296
|
+
const isNodeEnvironment2 = typeof window === "undefined";
|
|
2297
|
+
if (hasExplicitCredentials && isNodeEnvironment2) {
|
|
2298
|
+
return {
|
|
2299
|
+
fetch(url, opts = {}) {
|
|
2300
|
+
const basicAuth = btoa(`${ENV.COUCHDB_USERNAME}:${ENV.COUCHDB_PASSWORD}`);
|
|
2301
|
+
const headers = new Headers(opts.headers || {});
|
|
2302
|
+
headers.set("Authorization", `Basic ${basicAuth}`);
|
|
2303
|
+
const newOpts = {
|
|
2304
|
+
...opts,
|
|
2305
|
+
headers
|
|
2306
|
+
};
|
|
2307
|
+
return pouchdb_setup_default.fetch(url, newOpts);
|
|
2308
|
+
}
|
|
2309
|
+
};
|
|
2310
|
+
}
|
|
2311
|
+
return pouchDBincludeCredentialsConfig;
|
|
2312
|
+
}
|
|
2081
2313
|
function getCourseDB2(courseID) {
|
|
2082
2314
|
return new pouchdb_setup_default(
|
|
2083
2315
|
ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + "coursedb-" + courseID,
|
|
2084
|
-
|
|
2316
|
+
createPouchDBConfig()
|
|
2085
2317
|
);
|
|
2086
2318
|
}
|
|
2087
2319
|
function getCourseDocs(courseID, docIDs, options = {}) {
|
|
@@ -2370,6 +2602,9 @@ Currently logged-in as ${this._username}.`
|
|
|
2370
2602
|
await this.init();
|
|
2371
2603
|
return ret;
|
|
2372
2604
|
}
|
|
2605
|
+
async get(id) {
|
|
2606
|
+
return this.localDB.get(id);
|
|
2607
|
+
}
|
|
2373
2608
|
update(id, update) {
|
|
2374
2609
|
return this.updateQueue.update(id, update);
|
|
2375
2610
|
}
|
|
@@ -2416,7 +2651,12 @@ Currently logged-in as ${this._username}.`
|
|
|
2416
2651
|
endkey: keys.endkey,
|
|
2417
2652
|
include_docs: true
|
|
2418
2653
|
});
|
|
2419
|
-
return reviews.rows.map((r) =>
|
|
2654
|
+
return reviews.rows.map((r) => {
|
|
2655
|
+
return {
|
|
2656
|
+
courseID: r.doc.courseId,
|
|
2657
|
+
cardID: r.doc.cardId
|
|
2658
|
+
};
|
|
2659
|
+
});
|
|
2420
2660
|
}
|
|
2421
2661
|
async getActivityRecords() {
|
|
2422
2662
|
try {
|
|
@@ -2696,8 +2936,18 @@ Currently logged-in as ${this._username}.`
|
|
|
2696
2936
|
}
|
|
2697
2937
|
this.setDBandQ();
|
|
2698
2938
|
this.syncStrategy.startSync(this.localDB, this.remoteDB);
|
|
2699
|
-
|
|
2700
|
-
|
|
2939
|
+
this.applyDesignDocs().catch((error) => {
|
|
2940
|
+
log4(`Error in applyDesignDocs background task: ${error}`);
|
|
2941
|
+
if (error && typeof error === "object") {
|
|
2942
|
+
log4(`Full error details in applyDesignDocs: ${JSON.stringify(error)}`);
|
|
2943
|
+
}
|
|
2944
|
+
});
|
|
2945
|
+
this.deduplicateReviews().catch((error) => {
|
|
2946
|
+
log4(`Error in deduplicateReviews background task: ${error}`);
|
|
2947
|
+
if (error && typeof error === "object") {
|
|
2948
|
+
log4(`Full error details in background task: ${JSON.stringify(error)}`);
|
|
2949
|
+
}
|
|
2950
|
+
});
|
|
2701
2951
|
_BaseUser._initialized = true;
|
|
2702
2952
|
}
|
|
2703
2953
|
static designDocs = [
|
|
@@ -2715,10 +2965,15 @@ Currently logged-in as ${this._username}.`
|
|
|
2715
2965
|
}
|
|
2716
2966
|
];
|
|
2717
2967
|
async applyDesignDocs() {
|
|
2968
|
+
log4(`Starting applyDesignDocs for user: ${this._username}`);
|
|
2969
|
+
log4(`Remote DB name: ${this.remoteDB.name || "unknown"}`);
|
|
2718
2970
|
if (this._username === "admin") {
|
|
2971
|
+
log4("Skipping design docs for admin user");
|
|
2719
2972
|
return;
|
|
2720
2973
|
}
|
|
2974
|
+
log4(`Applying ${_BaseUser.designDocs.length} design docs`);
|
|
2721
2975
|
for (const doc of _BaseUser.designDocs) {
|
|
2976
|
+
log4(`Applying design doc: ${doc._id}`);
|
|
2722
2977
|
try {
|
|
2723
2978
|
try {
|
|
2724
2979
|
const existingDoc = await this.remoteDB.get(doc._id);
|
|
@@ -2795,17 +3050,21 @@ Currently logged-in as ${this._username}.`
|
|
|
2795
3050
|
} catch (e) {
|
|
2796
3051
|
const reason = e;
|
|
2797
3052
|
if (reason.status === 404) {
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
3053
|
+
try {
|
|
3054
|
+
const initCardHistory = {
|
|
3055
|
+
_id: cardHistoryID,
|
|
3056
|
+
cardID: record.cardID,
|
|
3057
|
+
courseID: record.courseID,
|
|
3058
|
+
records: [record],
|
|
3059
|
+
lapses: 0,
|
|
3060
|
+
streak: 0,
|
|
3061
|
+
bestInterval: 0
|
|
3062
|
+
};
|
|
3063
|
+
const putResult = await this.writeDB.put(initCardHistory);
|
|
3064
|
+
return { ...initCardHistory, _rev: putResult.rev };
|
|
3065
|
+
} catch (creationError) {
|
|
3066
|
+
throw new Error(`Failed to create CardHistory for ${cardHistoryID}. Reason: ${creationError}`);
|
|
3067
|
+
}
|
|
2809
3068
|
} else {
|
|
2810
3069
|
throw new Error(`putCardRecord failed because of:
|
|
2811
3070
|
name:${reason.name}
|
|
@@ -2817,8 +3076,13 @@ Currently logged-in as ${this._username}.`
|
|
|
2817
3076
|
async deduplicateReviews() {
|
|
2818
3077
|
try {
|
|
2819
3078
|
log4("Starting deduplication of scheduled reviews...");
|
|
3079
|
+
log4(`Remote DB name: ${this.remoteDB.name || "unknown"}`);
|
|
3080
|
+
log4(`Write DB name: ${this.writeDB.name || "unknown"}`);
|
|
2820
3081
|
const reviewsMap = {};
|
|
2821
3082
|
const duplicateDocIds = [];
|
|
3083
|
+
log4(
|
|
3084
|
+
`Attempting to query remoteDB for reviewCards/reviewCards. Database: ${this.remoteDB.name || "unknown"}`
|
|
3085
|
+
);
|
|
2822
3086
|
const scheduledReviews = await this.remoteDB.query("reviewCards/reviewCards");
|
|
2823
3087
|
log4(`Found ${scheduledReviews.rows.length} scheduled reviews to process`);
|
|
2824
3088
|
scheduledReviews.rows.forEach((r) => {
|
|
@@ -2853,6 +3117,17 @@ Currently logged-in as ${this._username}.`
|
|
|
2853
3117
|
}
|
|
2854
3118
|
} catch (error) {
|
|
2855
3119
|
log4(`Error during review deduplication: ${error}`);
|
|
3120
|
+
if (error && typeof error === "object" && "status" in error && error.status === 404) {
|
|
3121
|
+
log4(
|
|
3122
|
+
`Database not found (404) during review deduplication. Database: ${this.remoteDB.name || "unknown"}`
|
|
3123
|
+
);
|
|
3124
|
+
log4(
|
|
3125
|
+
`This might indicate the user database doesn't exist or the reviewCards view isn't available`
|
|
3126
|
+
);
|
|
3127
|
+
}
|
|
3128
|
+
if (error && typeof error === "object") {
|
|
3129
|
+
log4(`Full error details: ${JSON.stringify(error)}`);
|
|
3130
|
+
}
|
|
2856
3131
|
}
|
|
2857
3132
|
}
|
|
2858
3133
|
/**
|
|
@@ -3086,6 +3361,16 @@ var init_PouchDataLayerProvider = __esm({
|
|
|
3086
3361
|
getAdminDB() {
|
|
3087
3362
|
return new AdminDB();
|
|
3088
3363
|
}
|
|
3364
|
+
async createUserReaderForUser(targetUsername) {
|
|
3365
|
+
const requestingUsername = await getLoggedInUsername();
|
|
3366
|
+
if (requestingUsername !== "admin") {
|
|
3367
|
+
throw new Error("Unauthorized: Only admin users can access other users' data");
|
|
3368
|
+
}
|
|
3369
|
+
logger.info(`Admin user '${requestingUsername}' requesting UserDBReader for '${targetUsername}'`);
|
|
3370
|
+
const syncStrategy = new CouchDBSyncStrategy();
|
|
3371
|
+
const targetUserDB = await BaseUser.instance(syncStrategy, targetUsername);
|
|
3372
|
+
return targetUserDB;
|
|
3373
|
+
}
|
|
3089
3374
|
isReadOnly() {
|
|
3090
3375
|
return false;
|
|
3091
3376
|
}
|
|
@@ -3538,7 +3823,10 @@ var init_courseDB2 = __esm({
|
|
|
3538
3823
|
};
|
|
3539
3824
|
}
|
|
3540
3825
|
async getCardsByELO(elo, limit) {
|
|
3541
|
-
return this.unpacker.queryByElo(elo, limit || 25)
|
|
3826
|
+
return (await this.unpacker.queryByElo(elo, limit || 25)).map((card) => {
|
|
3827
|
+
const [courseID, cardID, elo2] = card.split("-");
|
|
3828
|
+
return { courseID, cardID, elo: elo2 ? parseInt(elo2) : void 0 };
|
|
3829
|
+
});
|
|
3542
3830
|
}
|
|
3543
3831
|
async getCardEloData(cardIds) {
|
|
3544
3832
|
const results = await Promise.all(
|
|
@@ -3556,16 +3844,20 @@ var init_courseDB2 = __esm({
|
|
|
3556
3844
|
async updateCardElo(cardId, _elo) {
|
|
3557
3845
|
return { ok: true, id: cardId, rev: "1-static" };
|
|
3558
3846
|
}
|
|
3559
|
-
async getNewCards(limit) {
|
|
3560
|
-
const
|
|
3561
|
-
return
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3847
|
+
async getNewCards(limit = 99) {
|
|
3848
|
+
const activeCards = await this.userDB.getActiveCards();
|
|
3849
|
+
return (await this.getCardsCenteredAtELO({ limit, elo: "user" }, (c) => {
|
|
3850
|
+
if (activeCards.some((ac) => c.cardID === ac.cardID)) {
|
|
3851
|
+
return false;
|
|
3852
|
+
} else {
|
|
3853
|
+
return true;
|
|
3854
|
+
}
|
|
3855
|
+
})).map((c) => {
|
|
3856
|
+
return {
|
|
3857
|
+
...c,
|
|
3858
|
+
status: "new"
|
|
3859
|
+
};
|
|
3860
|
+
});
|
|
3569
3861
|
}
|
|
3570
3862
|
async getCardsCenteredAtELO(options, filter) {
|
|
3571
3863
|
let targetElo = typeof options.elo === "number" ? options.elo : 1e3;
|
|
@@ -3582,14 +3874,19 @@ var init_courseDB2 = __esm({
|
|
|
3582
3874
|
} else if (options.elo === "random") {
|
|
3583
3875
|
targetElo = 800 + Math.random() * 400;
|
|
3584
3876
|
}
|
|
3585
|
-
let cardIds = await this.unpacker.queryByElo(targetElo, options.limit * 2)
|
|
3877
|
+
let cardIds = (await this.unpacker.queryByElo(targetElo, options.limit * 2)).map((c) => {
|
|
3878
|
+
return {
|
|
3879
|
+
cardID: c,
|
|
3880
|
+
courseID: this.courseId
|
|
3881
|
+
};
|
|
3882
|
+
});
|
|
3586
3883
|
if (filter) {
|
|
3587
3884
|
cardIds = cardIds.filter(filter);
|
|
3588
3885
|
}
|
|
3589
|
-
return cardIds.slice(0, options.limit).map((
|
|
3886
|
+
return cardIds.slice(0, options.limit).map((card) => ({
|
|
3590
3887
|
status: "new",
|
|
3591
|
-
qualifiedID: `${this.courseId}-${cardId}`,
|
|
3592
|
-
cardID:
|
|
3888
|
+
// qualifiedID: `${this.courseId}-${cardId}`,
|
|
3889
|
+
cardID: card.cardID,
|
|
3593
3890
|
contentSourceType: "course",
|
|
3594
3891
|
contentSourceID: this.courseId,
|
|
3595
3892
|
courseID: this.courseId
|
|
@@ -3741,7 +4038,7 @@ var init_courseDB2 = __esm({
|
|
|
3741
4038
|
// Navigation Strategy Manager implementation
|
|
3742
4039
|
async getNavigationStrategy(_id) {
|
|
3743
4040
|
return {
|
|
3744
|
-
|
|
4041
|
+
_id: "NAVIGATION_STRATEGY-ELO",
|
|
3745
4042
|
docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
|
|
3746
4043
|
name: "ELO",
|
|
3747
4044
|
description: "ELO-based navigation strategy",
|
|
@@ -3781,6 +4078,16 @@ var init_courseDB2 = __esm({
|
|
|
3781
4078
|
async getAttachmentBlob(docId, attachmentName) {
|
|
3782
4079
|
return this.unpacker.getAttachmentBlob(docId, attachmentName);
|
|
3783
4080
|
}
|
|
4081
|
+
// Admin search methods
|
|
4082
|
+
async searchCards(_query) {
|
|
4083
|
+
return [];
|
|
4084
|
+
}
|
|
4085
|
+
async find(_request) {
|
|
4086
|
+
return {
|
|
4087
|
+
docs: [],
|
|
4088
|
+
warning: "Find operations not supported in static mode"
|
|
4089
|
+
};
|
|
4090
|
+
}
|
|
3784
4091
|
};
|
|
3785
4092
|
}
|
|
3786
4093
|
});
|
|
@@ -3796,11 +4103,17 @@ var init_coursesDB = __esm({
|
|
|
3796
4103
|
this.manifests = manifests;
|
|
3797
4104
|
}
|
|
3798
4105
|
async getCourseConfig(courseId) {
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
4106
|
+
const manifest = this.manifests[courseId];
|
|
4107
|
+
if (!manifest) {
|
|
4108
|
+
logger.warn(`Course manifest for ${courseId} not found`);
|
|
4109
|
+
throw new Error(`Course ${courseId} not found`);
|
|
4110
|
+
}
|
|
4111
|
+
if (manifest.courseConfig) {
|
|
4112
|
+
return manifest.courseConfig;
|
|
4113
|
+
} else {
|
|
4114
|
+
logger.warn(`Course config not found in manifest for course ${courseId}`);
|
|
4115
|
+
throw new Error(`Course config not found for course ${courseId}`);
|
|
3802
4116
|
}
|
|
3803
|
-
return {};
|
|
3804
4117
|
}
|
|
3805
4118
|
async getCourseList() {
|
|
3806
4119
|
return Object.keys(this.manifests).map(
|
|
@@ -3892,23 +4205,49 @@ var init_StaticDataLayerProvider = __esm({
|
|
|
3892
4205
|
config;
|
|
3893
4206
|
initialized = false;
|
|
3894
4207
|
courseUnpackers = /* @__PURE__ */ new Map();
|
|
4208
|
+
manifests = {};
|
|
3895
4209
|
constructor(config) {
|
|
3896
4210
|
this.config = {
|
|
3897
|
-
staticContentPath: config.staticContentPath || "/static-courses",
|
|
3898
4211
|
localStoragePrefix: config.localStoragePrefix || "skuilder-static",
|
|
3899
|
-
|
|
4212
|
+
rootManifest: config.rootManifest || { dependencies: {} },
|
|
4213
|
+
rootManifestUrl: config.rootManifestUrl || "/"
|
|
3900
4214
|
};
|
|
3901
4215
|
}
|
|
4216
|
+
async resolveCourseDependencies() {
|
|
4217
|
+
logger.info("[StaticDataLayerProvider] Starting course dependency resolution...");
|
|
4218
|
+
const rootManifest = this.config.rootManifest;
|
|
4219
|
+
for (const [courseName, courseUrl] of Object.entries(rootManifest.dependencies || {})) {
|
|
4220
|
+
try {
|
|
4221
|
+
logger.debug(`[StaticDataLayerProvider] Resolving dependency: ${courseName} from ${courseUrl}`);
|
|
4222
|
+
const courseManifestUrl = new URL(courseUrl, this.config.rootManifestUrl).href;
|
|
4223
|
+
const courseJsonResponse = await fetch(courseManifestUrl);
|
|
4224
|
+
if (!courseJsonResponse.ok) {
|
|
4225
|
+
throw new Error(`Failed to fetch course manifest for ${courseName}`);
|
|
4226
|
+
}
|
|
4227
|
+
const courseJson = await courseJsonResponse.json();
|
|
4228
|
+
if (courseJson.content && courseJson.content.manifest) {
|
|
4229
|
+
const baseUrl = new URL(".", courseManifestUrl).href;
|
|
4230
|
+
const finalManifestUrl = new URL(courseJson.content.manifest, courseManifestUrl).href;
|
|
4231
|
+
const finalManifestResponse = await fetch(finalManifestUrl);
|
|
4232
|
+
if (!finalManifestResponse.ok) {
|
|
4233
|
+
throw new Error(`Failed to fetch final content manifest for ${courseName} at ${finalManifestUrl}`);
|
|
4234
|
+
}
|
|
4235
|
+
const finalManifest = await finalManifestResponse.json();
|
|
4236
|
+
this.manifests[courseName] = finalManifest;
|
|
4237
|
+
const unpacker = new StaticDataUnpacker(finalManifest, baseUrl);
|
|
4238
|
+
this.courseUnpackers.set(courseName, unpacker);
|
|
4239
|
+
logger.info(`[StaticDataLayerProvider] Successfully resolved and prepared course: ${courseName}`);
|
|
4240
|
+
}
|
|
4241
|
+
} catch (e) {
|
|
4242
|
+
logger.error(`[StaticDataLayerProvider] Failed to resolve dependency ${courseName}:`, e);
|
|
4243
|
+
}
|
|
4244
|
+
}
|
|
4245
|
+
logger.info("[StaticDataLayerProvider] Course dependency resolution complete.");
|
|
4246
|
+
}
|
|
3902
4247
|
async initialize() {
|
|
3903
4248
|
if (this.initialized) return;
|
|
3904
4249
|
logger.info("Initializing static data layer provider");
|
|
3905
|
-
|
|
3906
|
-
const unpacker = new StaticDataUnpacker(
|
|
3907
|
-
manifest,
|
|
3908
|
-
`${this.config.staticContentPath}/${courseId}`
|
|
3909
|
-
);
|
|
3910
|
-
this.courseUnpackers.set(courseId, unpacker);
|
|
3911
|
-
}
|
|
4250
|
+
await this.resolveCourseDependencies();
|
|
3912
4251
|
this.initialized = true;
|
|
3913
4252
|
}
|
|
3914
4253
|
async teardown() {
|
|
@@ -3922,13 +4261,13 @@ var init_StaticDataLayerProvider = __esm({
|
|
|
3922
4261
|
getCourseDB(courseId) {
|
|
3923
4262
|
const unpacker = this.courseUnpackers.get(courseId);
|
|
3924
4263
|
if (!unpacker) {
|
|
3925
|
-
throw new Error(`Course ${courseId} not found in static data
|
|
4264
|
+
throw new Error(`Course ${courseId} not found or failed to initialize in static data layer.`);
|
|
3926
4265
|
}
|
|
3927
|
-
const manifest = this.
|
|
4266
|
+
const manifest = this.manifests[courseId];
|
|
3928
4267
|
return new StaticCourseDB(courseId, unpacker, this.getUserDB(), manifest);
|
|
3929
4268
|
}
|
|
3930
4269
|
getCoursesDB() {
|
|
3931
|
-
return new StaticCoursesDB(this.
|
|
4270
|
+
return new StaticCoursesDB(this.manifests);
|
|
3932
4271
|
}
|
|
3933
4272
|
async getClassroomDB(_classId, _type) {
|
|
3934
4273
|
throw new Error("Classrooms not supported in static mode");
|
|
@@ -3936,6 +4275,12 @@ var init_StaticDataLayerProvider = __esm({
|
|
|
3936
4275
|
getAdminDB() {
|
|
3937
4276
|
throw new Error("Admin functions not supported in static mode");
|
|
3938
4277
|
}
|
|
4278
|
+
async createUserReaderForUser(targetUsername) {
|
|
4279
|
+
logger.warn(`StaticDataLayerProvider: Multi-user access not supported in static mode`);
|
|
4280
|
+
logger.warn(`Request: trying to access data for ${targetUsername}`);
|
|
4281
|
+
logger.warn(`Returning current user's data instead`);
|
|
4282
|
+
return this.getUserDB();
|
|
4283
|
+
}
|
|
3939
4284
|
isReadOnly() {
|
|
3940
4285
|
return true;
|
|
3941
4286
|
}
|
|
@@ -4228,66 +4573,561 @@ var init_core = __esm({
|
|
|
4228
4573
|
init_core();
|
|
4229
4574
|
init_courseLookupDB();
|
|
4230
4575
|
|
|
4231
|
-
// src/study/
|
|
4576
|
+
// src/study/services/SrsService.ts
|
|
4232
4577
|
init_couch();
|
|
4578
|
+
import moment7 from "moment";
|
|
4233
4579
|
|
|
4234
|
-
// src/
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
// src/util/packer/CouchDBToStaticPacker.ts
|
|
4238
|
-
init_types_legacy();
|
|
4580
|
+
// src/study/SpacedRepetition.ts
|
|
4581
|
+
init_util();
|
|
4239
4582
|
init_logger();
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4583
|
+
import moment6 from "moment";
|
|
4584
|
+
var duration = moment6.duration;
|
|
4585
|
+
function newInterval(user, cardHistory) {
|
|
4586
|
+
if (areQuestionRecords(cardHistory)) {
|
|
4587
|
+
return newQuestionInterval(user, cardHistory);
|
|
4588
|
+
} else {
|
|
4589
|
+
return 1e5;
|
|
4590
|
+
}
|
|
4591
|
+
}
|
|
4592
|
+
function newQuestionInterval(user, cardHistory) {
|
|
4593
|
+
const records = cardHistory.records;
|
|
4594
|
+
const currentAttempt = records[records.length - 1];
|
|
4595
|
+
const lastInterval = lastSuccessfulInterval(records);
|
|
4596
|
+
if (lastInterval > cardHistory.bestInterval) {
|
|
4597
|
+
cardHistory.bestInterval = lastInterval;
|
|
4598
|
+
void user.update(cardHistory._id, {
|
|
4599
|
+
bestInterval: lastInterval
|
|
4600
|
+
});
|
|
4601
|
+
}
|
|
4602
|
+
if (currentAttempt.isCorrect) {
|
|
4603
|
+
const skill = Math.min(1, Math.max(0, currentAttempt.performance));
|
|
4604
|
+
logger.debug(`Demontrated skill: ${skill}`);
|
|
4605
|
+
const interval = lastInterval * (0.75 + skill);
|
|
4606
|
+
cardHistory.lapses = getLapses(cardHistory.records);
|
|
4607
|
+
cardHistory.streak = getStreak(cardHistory.records);
|
|
4608
|
+
if (cardHistory.lapses && cardHistory.streak && cardHistory.bestInterval && (cardHistory.lapses >= 0 || cardHistory.streak >= 0)) {
|
|
4609
|
+
const ret = (cardHistory.lapses * interval + cardHistory.streak * cardHistory.bestInterval) / (cardHistory.lapses + cardHistory.streak);
|
|
4610
|
+
logger.debug(`Weighted average interval calculation:
|
|
4611
|
+
(${cardHistory.lapses} * ${interval} + ${cardHistory.streak} * ${cardHistory.bestInterval}) / (${cardHistory.lapses} + ${cardHistory.streak}) = ${ret}`);
|
|
4612
|
+
return ret;
|
|
4613
|
+
} else {
|
|
4614
|
+
return interval;
|
|
4615
|
+
}
|
|
4616
|
+
} else {
|
|
4617
|
+
return 0;
|
|
4618
|
+
}
|
|
4619
|
+
}
|
|
4620
|
+
function lastSuccessfulInterval(cardHistory) {
|
|
4621
|
+
for (let i = cardHistory.length - 1; i >= 1; i--) {
|
|
4622
|
+
if (cardHistory[i].priorAttemps === 0 && cardHistory[i].isCorrect) {
|
|
4623
|
+
const lastInterval = secondsBetween(cardHistory[i - 1].timeStamp, cardHistory[i].timeStamp);
|
|
4624
|
+
const ret = Math.max(lastInterval, 20 * 60 * 60);
|
|
4625
|
+
logger.debug(`Last interval w/ this card was: ${lastInterval}s, returning ${ret}s`);
|
|
4626
|
+
return ret;
|
|
4627
|
+
}
|
|
4628
|
+
}
|
|
4629
|
+
return getInitialInterval(cardHistory);
|
|
4630
|
+
}
|
|
4631
|
+
function getStreak(records) {
|
|
4632
|
+
let streak = 0;
|
|
4633
|
+
let index = records.length - 1;
|
|
4634
|
+
while (index >= 0 && records[index].isCorrect) {
|
|
4635
|
+
index--;
|
|
4636
|
+
streak++;
|
|
4637
|
+
}
|
|
4638
|
+
return streak;
|
|
4639
|
+
}
|
|
4640
|
+
function getLapses(records) {
|
|
4641
|
+
return records.filter((r) => r.isCorrect === false).length;
|
|
4642
|
+
}
|
|
4643
|
+
function getInitialInterval(cardHistory) {
|
|
4644
|
+
logger.warn(`history of length: ${cardHistory.length} ignored!`);
|
|
4645
|
+
return 60 * 60 * 24 * 3;
|
|
4646
|
+
}
|
|
4647
|
+
function secondsBetween(start, end) {
|
|
4648
|
+
start = moment6(start);
|
|
4649
|
+
end = moment6(end);
|
|
4650
|
+
const ret = duration(end.diff(start)).asSeconds();
|
|
4651
|
+
return ret;
|
|
4652
|
+
}
|
|
4653
|
+
|
|
4654
|
+
// src/study/services/SrsService.ts
|
|
4655
|
+
init_logger();
|
|
4656
|
+
var SrsService = class {
|
|
4657
|
+
user;
|
|
4658
|
+
constructor(user) {
|
|
4659
|
+
this.user = user;
|
|
4660
|
+
}
|
|
4661
|
+
/**
|
|
4662
|
+
* Calculates the next review time for a card based on its history and
|
|
4663
|
+
* schedules it in the user's database.
|
|
4664
|
+
* @param history The full history of the card.
|
|
4665
|
+
* @param item The study session item, used to determine if a previous review needs to be cleared.
|
|
4666
|
+
*/
|
|
4667
|
+
async scheduleReview(history, item) {
|
|
4668
|
+
const nextInterval = newInterval(this.user, history);
|
|
4669
|
+
const nextReviewTime = moment7.utc().add(nextInterval, "seconds");
|
|
4670
|
+
if (isReview(item)) {
|
|
4671
|
+
logger.info(`[SrsService] Removing previously scheduled review for: ${item.cardID}`);
|
|
4672
|
+
void this.user.removeScheduledCardReview(item.reviewID);
|
|
4673
|
+
}
|
|
4674
|
+
void this.user.scheduleCardReview({
|
|
4675
|
+
user: this.user.getUsername(),
|
|
4676
|
+
course_id: history.courseID,
|
|
4677
|
+
card_id: history.cardID,
|
|
4678
|
+
time: nextReviewTime,
|
|
4679
|
+
scheduledFor: item.contentSourceType,
|
|
4680
|
+
schedulingAgentId: item.contentSourceID
|
|
4681
|
+
});
|
|
4682
|
+
}
|
|
4683
|
+
};
|
|
4684
|
+
|
|
4685
|
+
// src/study/services/EloService.ts
|
|
4686
|
+
init_logger();
|
|
4687
|
+
import { adjustCourseScores, toCourseElo as toCourseElo3 } from "@vue-skuilder/common";
|
|
4688
|
+
var EloService = class {
|
|
4689
|
+
dataLayer;
|
|
4690
|
+
user;
|
|
4691
|
+
constructor(dataLayer, user) {
|
|
4692
|
+
this.dataLayer = dataLayer;
|
|
4693
|
+
this.user = user;
|
|
4694
|
+
}
|
|
4695
|
+
/**
|
|
4696
|
+
* Updates both user and card ELO ratings based on user performance.
|
|
4697
|
+
* @param userScore Score between 0-1 representing user performance
|
|
4698
|
+
* @param course_id Course identifier
|
|
4699
|
+
* @param card_id Card identifier
|
|
4700
|
+
* @param userCourseRegDoc User's course registration document (will be mutated)
|
|
4701
|
+
* @param currentCard Current card session record
|
|
4702
|
+
* @param k Optional K-factor for ELO calculation
|
|
4703
|
+
*/
|
|
4704
|
+
async updateUserAndCardElo(userScore, course_id, card_id, userCourseRegDoc, currentCard, k) {
|
|
4705
|
+
if (k) {
|
|
4706
|
+
logger.warn(`k value interpretation not currently implemented`);
|
|
4707
|
+
}
|
|
4708
|
+
const courseDB = this.dataLayer.getCourseDB(currentCard.card.course_id);
|
|
4709
|
+
const userElo = toCourseElo3(userCourseRegDoc.courses.find((c) => c.courseID === course_id).elo);
|
|
4710
|
+
const cardElo = (await courseDB.getCardEloData([currentCard.card.card_id]))[0];
|
|
4711
|
+
if (cardElo && userElo) {
|
|
4712
|
+
const eloUpdate = adjustCourseScores(userElo, cardElo, userScore);
|
|
4713
|
+
userCourseRegDoc.courses.find((c) => c.courseID === course_id).elo = eloUpdate.userElo;
|
|
4714
|
+
const results = await Promise.allSettled([
|
|
4715
|
+
this.user.updateUserElo(course_id, eloUpdate.userElo),
|
|
4716
|
+
courseDB.updateCardElo(card_id, eloUpdate.cardElo)
|
|
4717
|
+
]);
|
|
4718
|
+
const userEloStatus = results[0].status === "fulfilled";
|
|
4719
|
+
const cardEloStatus = results[1].status === "fulfilled";
|
|
4720
|
+
if (userEloStatus && cardEloStatus) {
|
|
4721
|
+
const user = results[0].value;
|
|
4722
|
+
const card = results[1].value;
|
|
4723
|
+
if (user.ok && card && card.ok) {
|
|
4724
|
+
logger.info(
|
|
4725
|
+
`[EloService] Updated ELOS:
|
|
4726
|
+
User: ${JSON.stringify(eloUpdate.userElo)})
|
|
4727
|
+
Card: ${JSON.stringify(eloUpdate.cardElo)})
|
|
4728
|
+
`
|
|
4729
|
+
);
|
|
4730
|
+
}
|
|
4731
|
+
} else {
|
|
4732
|
+
logger.warn(
|
|
4733
|
+
`[EloService] Partial ELO update:
|
|
4734
|
+
User ELO update: ${userEloStatus ? "SUCCESS" : "FAILED"}
|
|
4735
|
+
Card ELO update: ${cardEloStatus ? "SUCCESS" : "FAILED"}`
|
|
4736
|
+
);
|
|
4737
|
+
if (!userEloStatus && results[0].status === "rejected") {
|
|
4738
|
+
logger.error("[EloService] User ELO update error:", results[0].reason);
|
|
4739
|
+
}
|
|
4740
|
+
if (!cardEloStatus && results[1].status === "rejected") {
|
|
4741
|
+
logger.error("[EloService] Card ELO update error:", results[1].reason);
|
|
4742
|
+
}
|
|
4743
|
+
}
|
|
4744
|
+
}
|
|
4745
|
+
}
|
|
4746
|
+
};
|
|
4747
|
+
|
|
4748
|
+
// src/study/services/ResponseProcessor.ts
|
|
4749
|
+
init_core();
|
|
4750
|
+
init_logger();
|
|
4751
|
+
var ResponseProcessor = class {
|
|
4752
|
+
srsService;
|
|
4753
|
+
eloService;
|
|
4754
|
+
constructor(srsService, eloService) {
|
|
4755
|
+
this.srsService = srsService;
|
|
4756
|
+
this.eloService = eloService;
|
|
4757
|
+
}
|
|
4758
|
+
/**
|
|
4759
|
+
* Processes a user's response to a card, handling SRS scheduling and ELO updates.
|
|
4760
|
+
* @param cardRecord User's response record
|
|
4761
|
+
* @param cardHistory Promise resolving to the card's history
|
|
4762
|
+
* @param studySessionItem Current study session item
|
|
4763
|
+
* @param courseRegistrationDoc User's course registration (for ELO updates)
|
|
4764
|
+
* @param currentCard Current study session record
|
|
4765
|
+
* @param courseId Course identifier
|
|
4766
|
+
* @param cardId Card identifier
|
|
4767
|
+
* @param maxAttemptsPerView Maximum attempts allowed per view
|
|
4768
|
+
* @param maxSessionViews Maximum session views for this card
|
|
4769
|
+
* @param sessionViews Current number of session views
|
|
4770
|
+
* @returns ResponseResult with navigation and UI instructions
|
|
4771
|
+
*/
|
|
4772
|
+
async processResponse(cardRecord, cardHistory, studySessionItem, courseRegistrationDoc, currentCard, courseId, cardId, maxAttemptsPerView, maxSessionViews, sessionViews) {
|
|
4773
|
+
if (!isQuestionRecord(cardRecord)) {
|
|
4774
|
+
return {
|
|
4775
|
+
nextCardAction: "dismiss-success",
|
|
4776
|
+
shouldLoadNextCard: true,
|
|
4777
|
+
isCorrect: true,
|
|
4778
|
+
// non-question records are considered "correct"
|
|
4779
|
+
shouldClearFeedbackShadow: true
|
|
4780
|
+
};
|
|
4781
|
+
}
|
|
4782
|
+
const history = await cardHistory;
|
|
4783
|
+
if (cardRecord.isCorrect) {
|
|
4784
|
+
return this.processCorrectResponse(
|
|
4785
|
+
cardRecord,
|
|
4786
|
+
history,
|
|
4787
|
+
studySessionItem,
|
|
4788
|
+
courseRegistrationDoc,
|
|
4789
|
+
currentCard,
|
|
4790
|
+
courseId,
|
|
4791
|
+
cardId
|
|
4792
|
+
);
|
|
4793
|
+
} else {
|
|
4794
|
+
return this.processIncorrectResponse(
|
|
4795
|
+
cardRecord,
|
|
4796
|
+
history,
|
|
4797
|
+
courseRegistrationDoc,
|
|
4798
|
+
currentCard,
|
|
4799
|
+
courseId,
|
|
4800
|
+
cardId,
|
|
4801
|
+
maxAttemptsPerView,
|
|
4802
|
+
maxSessionViews,
|
|
4803
|
+
sessionViews
|
|
4804
|
+
);
|
|
4805
|
+
}
|
|
4806
|
+
}
|
|
4807
|
+
/**
|
|
4808
|
+
* Handles processing for correct responses: SRS scheduling and ELO updates.
|
|
4809
|
+
*/
|
|
4810
|
+
processCorrectResponse(cardRecord, history, studySessionItem, courseRegistrationDoc, currentCard, courseId, cardId) {
|
|
4811
|
+
if (cardRecord.priorAttemps === 0) {
|
|
4812
|
+
void this.srsService.scheduleReview(history, studySessionItem);
|
|
4813
|
+
if (history.records.length === 1) {
|
|
4814
|
+
const userScore = 0.5 + cardRecord.performance / 2;
|
|
4815
|
+
void this.eloService.updateUserAndCardElo(
|
|
4816
|
+
userScore,
|
|
4817
|
+
courseId,
|
|
4818
|
+
cardId,
|
|
4819
|
+
courseRegistrationDoc,
|
|
4820
|
+
currentCard
|
|
4821
|
+
);
|
|
4822
|
+
} else {
|
|
4823
|
+
const k = Math.ceil(32 / history.records.length);
|
|
4824
|
+
const userScore = 0.5 + cardRecord.performance / 2;
|
|
4825
|
+
void this.eloService.updateUserAndCardElo(
|
|
4826
|
+
userScore,
|
|
4827
|
+
courseId,
|
|
4828
|
+
cardId,
|
|
4829
|
+
courseRegistrationDoc,
|
|
4830
|
+
currentCard,
|
|
4831
|
+
k
|
|
4832
|
+
);
|
|
4833
|
+
}
|
|
4834
|
+
logger.info(
|
|
4835
|
+
"[ResponseProcessor] Processed correct response with SRS scheduling and ELO update"
|
|
4836
|
+
);
|
|
4837
|
+
return {
|
|
4838
|
+
nextCardAction: "dismiss-success",
|
|
4839
|
+
shouldLoadNextCard: true,
|
|
4840
|
+
isCorrect: true,
|
|
4841
|
+
performanceScore: cardRecord.performance,
|
|
4842
|
+
shouldClearFeedbackShadow: true
|
|
4843
|
+
};
|
|
4844
|
+
} else {
|
|
4845
|
+
logger.info(
|
|
4846
|
+
"[ResponseProcessor] Processed correct response (retry attempt - no scheduling/ELO)"
|
|
4847
|
+
);
|
|
4848
|
+
return {
|
|
4849
|
+
nextCardAction: "marked-failed",
|
|
4850
|
+
shouldLoadNextCard: true,
|
|
4851
|
+
isCorrect: true,
|
|
4852
|
+
performanceScore: cardRecord.performance,
|
|
4853
|
+
shouldClearFeedbackShadow: true
|
|
4854
|
+
};
|
|
4855
|
+
}
|
|
4856
|
+
}
|
|
4857
|
+
/**
|
|
4858
|
+
* Handles processing for incorrect responses: ELO updates only.
|
|
4859
|
+
*/
|
|
4860
|
+
processIncorrectResponse(cardRecord, history, courseRegistrationDoc, currentCard, courseId, cardId, maxAttemptsPerView, maxSessionViews, sessionViews) {
|
|
4861
|
+
if (history.records.length !== 1 && cardRecord.priorAttemps === 0) {
|
|
4862
|
+
void this.eloService.updateUserAndCardElo(
|
|
4863
|
+
0,
|
|
4864
|
+
// Failed response = 0 score
|
|
4865
|
+
courseId,
|
|
4866
|
+
cardId,
|
|
4867
|
+
courseRegistrationDoc,
|
|
4868
|
+
currentCard
|
|
4869
|
+
);
|
|
4870
|
+
logger.info("[ResponseProcessor] Processed incorrect response with ELO update");
|
|
4871
|
+
} else {
|
|
4872
|
+
logger.info("[ResponseProcessor] Processed incorrect response (no ELO update needed)");
|
|
4873
|
+
}
|
|
4874
|
+
if (currentCard.records.length >= maxAttemptsPerView) {
|
|
4875
|
+
if (sessionViews >= maxSessionViews) {
|
|
4876
|
+
void this.eloService.updateUserAndCardElo(
|
|
4877
|
+
0,
|
|
4878
|
+
courseId,
|
|
4879
|
+
cardId,
|
|
4880
|
+
courseRegistrationDoc,
|
|
4881
|
+
currentCard
|
|
4882
|
+
);
|
|
4883
|
+
return {
|
|
4884
|
+
nextCardAction: "dismiss-failed",
|
|
4885
|
+
shouldLoadNextCard: true,
|
|
4886
|
+
isCorrect: false,
|
|
4887
|
+
shouldClearFeedbackShadow: true
|
|
4888
|
+
};
|
|
4889
|
+
} else {
|
|
4890
|
+
return {
|
|
4891
|
+
nextCardAction: "marked-failed",
|
|
4892
|
+
shouldLoadNextCard: true,
|
|
4893
|
+
isCorrect: false,
|
|
4894
|
+
shouldClearFeedbackShadow: true
|
|
4895
|
+
};
|
|
4896
|
+
}
|
|
4897
|
+
} else {
|
|
4898
|
+
return {
|
|
4899
|
+
nextCardAction: "none",
|
|
4900
|
+
shouldLoadNextCard: false,
|
|
4901
|
+
isCorrect: false,
|
|
4902
|
+
shouldClearFeedbackShadow: true
|
|
4903
|
+
};
|
|
4904
|
+
}
|
|
4905
|
+
}
|
|
4906
|
+
};
|
|
4907
|
+
|
|
4908
|
+
// src/study/services/CardHydrationService.ts
|
|
4909
|
+
init_logger();
|
|
4910
|
+
import {
|
|
4911
|
+
displayableDataToViewData,
|
|
4912
|
+
isCourseElo,
|
|
4913
|
+
toCourseElo as toCourseElo4
|
|
4914
|
+
} from "@vue-skuilder/common";
|
|
4915
|
+
|
|
4916
|
+
// src/study/ItemQueue.ts
|
|
4917
|
+
var ItemQueue = class {
|
|
4918
|
+
q = [];
|
|
4919
|
+
seenCardIds = [];
|
|
4920
|
+
_dequeueCount = 0;
|
|
4921
|
+
get dequeueCount() {
|
|
4922
|
+
return this._dequeueCount;
|
|
4923
|
+
}
|
|
4924
|
+
add(item, cardId) {
|
|
4925
|
+
if (this.seenCardIds.find((d) => d === cardId)) {
|
|
4926
|
+
return;
|
|
4927
|
+
}
|
|
4928
|
+
this.seenCardIds.push(cardId);
|
|
4929
|
+
this.q.push(item);
|
|
4930
|
+
}
|
|
4931
|
+
addAll(items, cardIdExtractor) {
|
|
4932
|
+
items.forEach((i) => this.add(i, cardIdExtractor(i)));
|
|
4933
|
+
}
|
|
4934
|
+
get length() {
|
|
4935
|
+
return this.q.length;
|
|
4936
|
+
}
|
|
4937
|
+
peek(index) {
|
|
4938
|
+
return this.q[index];
|
|
4939
|
+
}
|
|
4940
|
+
dequeue(cardIdExtractor) {
|
|
4941
|
+
if (this.q.length !== 0) {
|
|
4942
|
+
this._dequeueCount++;
|
|
4943
|
+
const item = this.q.splice(0, 1)[0];
|
|
4944
|
+
if (cardIdExtractor) {
|
|
4945
|
+
const cardId = cardIdExtractor(item);
|
|
4946
|
+
const index = this.seenCardIds.indexOf(cardId);
|
|
4947
|
+
if (index > -1) {
|
|
4948
|
+
this.seenCardIds.splice(index, 1);
|
|
4949
|
+
}
|
|
4950
|
+
}
|
|
4951
|
+
return item;
|
|
4952
|
+
} else {
|
|
4953
|
+
return null;
|
|
4954
|
+
}
|
|
4955
|
+
}
|
|
4956
|
+
get toString() {
|
|
4957
|
+
return `${typeof this.q[0]}:
|
|
4958
|
+
` + this.q.map((i) => ` ${i.courseID}+${i.cardID}: ${i.status}`).join("\n");
|
|
4959
|
+
}
|
|
4960
|
+
};
|
|
4961
|
+
|
|
4962
|
+
// src/study/services/CardHydrationService.ts
|
|
4963
|
+
var CardHydrationService = class {
|
|
4964
|
+
constructor(getViewComponent, getCourseDB3, selectNextItemToHydrate, removeItemFromQueue, hasAvailableCards) {
|
|
4965
|
+
this.getViewComponent = getViewComponent;
|
|
4966
|
+
this.getCourseDB = getCourseDB3;
|
|
4967
|
+
this.selectNextItemToHydrate = selectNextItemToHydrate;
|
|
4968
|
+
this.removeItemFromQueue = removeItemFromQueue;
|
|
4969
|
+
this.hasAvailableCards = hasAvailableCards;
|
|
4970
|
+
}
|
|
4971
|
+
hydratedQ = new ItemQueue();
|
|
4972
|
+
failedCardCache = /* @__PURE__ */ new Map();
|
|
4973
|
+
hydrationInProgress = false;
|
|
4974
|
+
BUFFER_SIZE = 5;
|
|
4975
|
+
/**
|
|
4976
|
+
* Get the next hydrated card from the queue.
|
|
4977
|
+
* @returns Hydrated card or null if none available
|
|
4978
|
+
*/
|
|
4979
|
+
dequeueHydratedCard() {
|
|
4980
|
+
return this.hydratedQ.dequeue((item) => item.item.cardID);
|
|
4981
|
+
}
|
|
4982
|
+
/**
|
|
4983
|
+
* Check if hydration should be triggered and start background hydration if needed.
|
|
4984
|
+
*/
|
|
4985
|
+
async ensureHydratedCards() {
|
|
4986
|
+
if (this.hydratedQ.length < 3) {
|
|
4987
|
+
void this.fillHydratedQueue();
|
|
4988
|
+
}
|
|
4989
|
+
}
|
|
4990
|
+
/**
|
|
4991
|
+
* Wait for a hydrated card to become available.
|
|
4992
|
+
* @returns Promise that resolves to a hydrated card or null
|
|
4993
|
+
*/
|
|
4994
|
+
async waitForHydratedCard() {
|
|
4995
|
+
if (this.hydratedQ.length === 0 && this.hasAvailableCards()) {
|
|
4996
|
+
void this.fillHydratedQueue();
|
|
4997
|
+
}
|
|
4998
|
+
while (this.hydratedQ.length === 0 && this.hasAvailableCards()) {
|
|
4999
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
5000
|
+
}
|
|
5001
|
+
return this.dequeueHydratedCard();
|
|
5002
|
+
}
|
|
5003
|
+
/**
|
|
5004
|
+
* Get current hydrated queue length.
|
|
5005
|
+
*/
|
|
5006
|
+
get hydratedCount() {
|
|
5007
|
+
return this.hydratedQ.length;
|
|
5008
|
+
}
|
|
5009
|
+
/**
|
|
5010
|
+
* Fill the hydrated queue up to BUFFER_SIZE with pre-fetched cards.
|
|
5011
|
+
*/
|
|
5012
|
+
async fillHydratedQueue() {
|
|
5013
|
+
if (this.hydrationInProgress) {
|
|
5014
|
+
return;
|
|
5015
|
+
}
|
|
5016
|
+
this.hydrationInProgress = true;
|
|
5017
|
+
try {
|
|
5018
|
+
while (this.hydratedQ.length < this.BUFFER_SIZE) {
|
|
5019
|
+
const nextItem = this.selectNextItemToHydrate();
|
|
5020
|
+
if (!nextItem) {
|
|
5021
|
+
return;
|
|
5022
|
+
}
|
|
5023
|
+
try {
|
|
5024
|
+
if (this.failedCardCache.has(nextItem.cardID)) {
|
|
5025
|
+
const cachedCard = this.failedCardCache.get(nextItem.cardID);
|
|
5026
|
+
this.hydratedQ.add(cachedCard, cachedCard.item.cardID);
|
|
5027
|
+
this.failedCardCache.delete(nextItem.cardID);
|
|
5028
|
+
} else {
|
|
5029
|
+
const courseDB = this.getCourseDB(nextItem.courseID);
|
|
5030
|
+
const cardData = await courseDB.getCourseDoc(nextItem.cardID);
|
|
5031
|
+
if (!isCourseElo(cardData.elo)) {
|
|
5032
|
+
cardData.elo = toCourseElo4(cardData.elo);
|
|
5033
|
+
}
|
|
5034
|
+
const view = this.getViewComponent(cardData.id_view);
|
|
5035
|
+
const dataDocs = await Promise.all(
|
|
5036
|
+
cardData.id_displayable_data.map(
|
|
5037
|
+
(id) => courseDB.getCourseDoc(id, {
|
|
5038
|
+
attachments: true,
|
|
5039
|
+
binary: true
|
|
5040
|
+
})
|
|
5041
|
+
)
|
|
5042
|
+
);
|
|
5043
|
+
const data = dataDocs.map(displayableDataToViewData).reverse();
|
|
5044
|
+
this.hydratedQ.add(
|
|
5045
|
+
{
|
|
5046
|
+
item: nextItem,
|
|
5047
|
+
view,
|
|
5048
|
+
data
|
|
5049
|
+
},
|
|
5050
|
+
nextItem.cardID
|
|
5051
|
+
);
|
|
5052
|
+
}
|
|
5053
|
+
} catch (e) {
|
|
5054
|
+
logger.error(`Error hydrating card ${nextItem.cardID}:`, e);
|
|
5055
|
+
} finally {
|
|
5056
|
+
this.removeItemFromQueue(nextItem);
|
|
5057
|
+
}
|
|
5058
|
+
}
|
|
5059
|
+
} finally {
|
|
5060
|
+
this.hydrationInProgress = false;
|
|
5061
|
+
}
|
|
5062
|
+
}
|
|
5063
|
+
/**
|
|
5064
|
+
* Cache a failed card for quick re-access.
|
|
5065
|
+
*/
|
|
5066
|
+
cacheFailedCard(card) {
|
|
5067
|
+
this.failedCardCache.set(card.item.cardID, card);
|
|
5068
|
+
}
|
|
5069
|
+
};
|
|
5070
|
+
|
|
5071
|
+
// src/study/SessionController.ts
|
|
5072
|
+
init_couch();
|
|
5073
|
+
|
|
5074
|
+
// src/util/index.ts
|
|
5075
|
+
init_Loggable();
|
|
5076
|
+
|
|
5077
|
+
// src/util/packer/CouchDBToStaticPacker.ts
|
|
5078
|
+
init_types_legacy();
|
|
5079
|
+
init_logger();
|
|
5080
|
+
var CouchDBToStaticPacker = class {
|
|
5081
|
+
config;
|
|
5082
|
+
sourceDB = null;
|
|
5083
|
+
constructor(config = {}) {
|
|
5084
|
+
this.config = {
|
|
5085
|
+
chunkSize: 1e3,
|
|
5086
|
+
includeAttachments: true,
|
|
5087
|
+
...config
|
|
5088
|
+
};
|
|
5089
|
+
}
|
|
5090
|
+
/**
|
|
5091
|
+
* Pack a CouchDB course database into static data structures
|
|
5092
|
+
*/
|
|
5093
|
+
async packCourse(sourceDB, courseId) {
|
|
5094
|
+
logger.info(`Starting static pack for course: ${courseId}`);
|
|
5095
|
+
this.sourceDB = sourceDB;
|
|
5096
|
+
const manifest = {
|
|
5097
|
+
version: "1.0.0",
|
|
5098
|
+
courseId,
|
|
5099
|
+
courseName: "",
|
|
5100
|
+
courseConfig: null,
|
|
5101
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5102
|
+
documentCount: 0,
|
|
5103
|
+
chunks: [],
|
|
5104
|
+
indices: [],
|
|
5105
|
+
designDocs: []
|
|
5106
|
+
};
|
|
5107
|
+
const courseConfig = await this.extractCourseConfig(sourceDB);
|
|
5108
|
+
manifest.courseName = courseConfig.name;
|
|
5109
|
+
manifest.courseConfig = courseConfig;
|
|
5110
|
+
manifest.designDocs = await this.extractDesignDocs(sourceDB);
|
|
5111
|
+
const docsByType = await this.extractDocumentsByType(sourceDB);
|
|
5112
|
+
const attachments = /* @__PURE__ */ new Map();
|
|
5113
|
+
if (this.config.includeAttachments) {
|
|
5114
|
+
await this.extractAllAttachments(docsByType, attachments);
|
|
5115
|
+
}
|
|
5116
|
+
const chunks = /* @__PURE__ */ new Map();
|
|
5117
|
+
for (const [docType, docs] of Object.entries(docsByType)) {
|
|
5118
|
+
const chunkMetadata = this.createChunks(docs, docType);
|
|
5119
|
+
manifest.chunks.push(...chunkMetadata);
|
|
5120
|
+
manifest.documentCount += docs.length;
|
|
5121
|
+
this.prepareChunkData(chunkMetadata, docs, chunks);
|
|
5122
|
+
}
|
|
5123
|
+
const indices = /* @__PURE__ */ new Map();
|
|
5124
|
+
manifest.indices = await this.buildIndices(docsByType, manifest.designDocs, indices);
|
|
5125
|
+
return {
|
|
5126
|
+
manifest,
|
|
5127
|
+
chunks,
|
|
5128
|
+
indices,
|
|
5129
|
+
attachments
|
|
5130
|
+
};
|
|
4291
5131
|
}
|
|
4292
5132
|
/**
|
|
4293
5133
|
* Pack a CouchDB course database and write the static files to disk
|
|
@@ -5658,58 +6498,24 @@ init_tuiLogger();
|
|
|
5658
6498
|
function randomInt(min, max) {
|
|
5659
6499
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
5660
6500
|
}
|
|
5661
|
-
var ItemQueue = class {
|
|
5662
|
-
q = [];
|
|
5663
|
-
seenCardIds = [];
|
|
5664
|
-
_dequeueCount = 0;
|
|
5665
|
-
get dequeueCount() {
|
|
5666
|
-
return this._dequeueCount;
|
|
5667
|
-
}
|
|
5668
|
-
add(item) {
|
|
5669
|
-
if (this.seenCardIds.find((d) => d === item.cardID)) {
|
|
5670
|
-
return;
|
|
5671
|
-
}
|
|
5672
|
-
this.seenCardIds.push(item.cardID);
|
|
5673
|
-
this.q.push(item);
|
|
5674
|
-
}
|
|
5675
|
-
addAll(items) {
|
|
5676
|
-
items.forEach((i) => this.add(i));
|
|
5677
|
-
}
|
|
5678
|
-
get length() {
|
|
5679
|
-
return this.q.length;
|
|
5680
|
-
}
|
|
5681
|
-
peek(index) {
|
|
5682
|
-
return this.q[index];
|
|
5683
|
-
}
|
|
5684
|
-
dequeue() {
|
|
5685
|
-
if (this.q.length !== 0) {
|
|
5686
|
-
this._dequeueCount++;
|
|
5687
|
-
return this.q.splice(0, 1)[0];
|
|
5688
|
-
} else {
|
|
5689
|
-
return null;
|
|
5690
|
-
}
|
|
5691
|
-
}
|
|
5692
|
-
get toString() {
|
|
5693
|
-
return `${typeof this.q[0]}:
|
|
5694
|
-
` + this.q.map((i) => ` ${i.qualifiedID}: ${i.status}`).join("\n");
|
|
5695
|
-
}
|
|
5696
|
-
};
|
|
5697
6501
|
var SessionController = class extends Loggable {
|
|
5698
6502
|
_className = "SessionController";
|
|
6503
|
+
services;
|
|
6504
|
+
srsService;
|
|
6505
|
+
eloService;
|
|
6506
|
+
hydrationService;
|
|
5699
6507
|
sources;
|
|
6508
|
+
// dataLayer and getViewComponent now injected into CardHydrationService
|
|
5700
6509
|
_sessionRecord = [];
|
|
5701
6510
|
set sessionRecord(r) {
|
|
5702
6511
|
this._sessionRecord = r;
|
|
5703
6512
|
}
|
|
6513
|
+
// Session card stores
|
|
6514
|
+
_currentCard = null;
|
|
5704
6515
|
reviewQ = new ItemQueue();
|
|
5705
6516
|
newQ = new ItemQueue();
|
|
5706
6517
|
failedQ = new ItemQueue();
|
|
5707
|
-
|
|
5708
|
-
/**
|
|
5709
|
-
* Indicates whether the session has been initialized - eg, the
|
|
5710
|
-
* queues have been populated.
|
|
5711
|
-
*/
|
|
5712
|
-
_isInitialized = false;
|
|
6518
|
+
// END Session card stores
|
|
5713
6519
|
startTime;
|
|
5714
6520
|
endTime;
|
|
5715
6521
|
_secondsRemaining;
|
|
@@ -5727,8 +6533,20 @@ var SessionController = class extends Loggable {
|
|
|
5727
6533
|
/**
|
|
5728
6534
|
*
|
|
5729
6535
|
*/
|
|
5730
|
-
constructor(sources, time) {
|
|
6536
|
+
constructor(sources, time, dataLayer, getViewComponent) {
|
|
5731
6537
|
super();
|
|
6538
|
+
this.srsService = new SrsService(dataLayer.getUserDB());
|
|
6539
|
+
this.eloService = new EloService(dataLayer, dataLayer.getUserDB());
|
|
6540
|
+
this.hydrationService = new CardHydrationService(
|
|
6541
|
+
getViewComponent,
|
|
6542
|
+
(courseId) => dataLayer.getCourseDB(courseId),
|
|
6543
|
+
() => this._selectNextItemToHydrate(),
|
|
6544
|
+
(item) => this.removeItemFromQueue(item),
|
|
6545
|
+
() => this.hasAvailableCards()
|
|
6546
|
+
);
|
|
6547
|
+
this.services = {
|
|
6548
|
+
response: new ResponseProcessor(this.srsService, this.eloService)
|
|
6549
|
+
};
|
|
5732
6550
|
this.sources = sources;
|
|
5733
6551
|
this.startTime = /* @__PURE__ */ new Date();
|
|
5734
6552
|
this._secondsRemaining = time;
|
|
@@ -5739,7 +6557,7 @@ var SessionController = class extends Loggable {
|
|
|
5739
6557
|
}
|
|
5740
6558
|
tick() {
|
|
5741
6559
|
this._secondsRemaining = Math.floor((this.endTime.valueOf() - Date.now()) / 1e3);
|
|
5742
|
-
if (this._secondsRemaining
|
|
6560
|
+
if (this._secondsRemaining <= 0) {
|
|
5743
6561
|
clearInterval(this._intervalHandle);
|
|
5744
6562
|
}
|
|
5745
6563
|
}
|
|
@@ -5783,7 +6601,7 @@ var SessionController = class extends Loggable {
|
|
|
5783
6601
|
} catch (e) {
|
|
5784
6602
|
this.error("Error preparing study session:", e);
|
|
5785
6603
|
}
|
|
5786
|
-
this.
|
|
6604
|
+
await this.hydrationService.ensureHydratedCards();
|
|
5787
6605
|
this._intervalHandle = setInterval(() => {
|
|
5788
6606
|
this.tick();
|
|
5789
6607
|
}, 1e3);
|
|
@@ -5821,12 +6639,8 @@ var SessionController = class extends Loggable {
|
|
|
5821
6639
|
}
|
|
5822
6640
|
}
|
|
5823
6641
|
let report = "Review session created with:\n";
|
|
5824
|
-
|
|
5825
|
-
|
|
5826
|
-
this.reviewQ.add(card);
|
|
5827
|
-
report += ` ${card.qualifiedID}}
|
|
5828
|
-
`;
|
|
5829
|
-
}
|
|
6642
|
+
this.reviewQ.addAll(dueCards, (c) => c.cardID);
|
|
6643
|
+
report += dueCards.map((card) => `Card ${card.courseID}::${card.cardID} `).join("\n");
|
|
5830
6644
|
this.log(report);
|
|
5831
6645
|
}
|
|
5832
6646
|
async getNewCards(n = 10) {
|
|
@@ -5841,36 +6655,32 @@ var SessionController = class extends Loggable {
|
|
|
5841
6655
|
for (let i = 0; i < newContent.length; i++) {
|
|
5842
6656
|
if (newContent[i].length > 0) {
|
|
5843
6657
|
const item = newContent[i].splice(0, 1)[0];
|
|
5844
|
-
this.log(`Adding new card: ${item.
|
|
5845
|
-
this.newQ.add(item);
|
|
6658
|
+
this.log(`Adding new card: ${item.courseID}::${item.cardID}`);
|
|
6659
|
+
this.newQ.add(item, item.cardID);
|
|
5846
6660
|
n--;
|
|
5847
6661
|
}
|
|
5848
6662
|
}
|
|
5849
6663
|
}
|
|
5850
6664
|
}
|
|
5851
|
-
|
|
5852
|
-
const item = this.newQ.dequeue();
|
|
5853
|
-
if (this._isInitialized && this.newQ.length < 5) {
|
|
5854
|
-
void this.getNewCards();
|
|
5855
|
-
}
|
|
5856
|
-
return item;
|
|
5857
|
-
}
|
|
5858
|
-
nextCard(action = "dismiss-success") {
|
|
5859
|
-
this.dismissCurrentCard(action);
|
|
6665
|
+
_selectNextItemToHydrate() {
|
|
5860
6666
|
const choice = Math.random();
|
|
5861
6667
|
let newBound = 0.1;
|
|
5862
6668
|
let reviewBound = 0.75;
|
|
5863
6669
|
if (this.reviewQ.length === 0 && this.failedQ.length === 0 && this.newQ.length === 0) {
|
|
5864
|
-
|
|
5865
|
-
return this._currentCard;
|
|
6670
|
+
return null;
|
|
5866
6671
|
}
|
|
5867
6672
|
if (this._secondsRemaining < 2 && this.failedQ.length === 0) {
|
|
5868
|
-
|
|
5869
|
-
|
|
6673
|
+
return null;
|
|
6674
|
+
}
|
|
6675
|
+
if (this._secondsRemaining <= 0) {
|
|
6676
|
+
if (this.failedQ.length > 0) {
|
|
6677
|
+
return this.failedQ.peek(0);
|
|
6678
|
+
} else {
|
|
6679
|
+
return null;
|
|
6680
|
+
}
|
|
5870
6681
|
}
|
|
5871
6682
|
if (this.newQ.dequeueCount < this.sources.length && this.newQ.length) {
|
|
5872
|
-
|
|
5873
|
-
return this._currentCard;
|
|
6683
|
+
return this.newQ.peek(0);
|
|
5874
6684
|
}
|
|
5875
6685
|
const cleanupTime = this.estimateCleanupTime();
|
|
5876
6686
|
const reviewTime = this.estimateReviewTime();
|
|
@@ -5885,9 +6695,6 @@ var SessionController = class extends Loggable {
|
|
|
5885
6695
|
newBound = 0.01;
|
|
5886
6696
|
reviewBound = 0.1;
|
|
5887
6697
|
}
|
|
5888
|
-
if (this.failedQ.length === 1 && action === "marked-failed") {
|
|
5889
|
-
reviewBound = 1;
|
|
5890
|
-
}
|
|
5891
6698
|
if (this.failedQ.length === 0) {
|
|
5892
6699
|
reviewBound = 1;
|
|
5893
6700
|
}
|
|
@@ -5895,123 +6702,110 @@ var SessionController = class extends Loggable {
|
|
|
5895
6702
|
newBound = reviewBound;
|
|
5896
6703
|
}
|
|
5897
6704
|
if (choice < newBound && this.newQ.length) {
|
|
5898
|
-
|
|
6705
|
+
return this.newQ.peek(0);
|
|
5899
6706
|
} else if (choice < reviewBound && this.reviewQ.length) {
|
|
5900
|
-
|
|
6707
|
+
return this.reviewQ.peek(0);
|
|
5901
6708
|
} else if (this.failedQ.length) {
|
|
5902
|
-
|
|
6709
|
+
return this.failedQ.peek(0);
|
|
5903
6710
|
} else {
|
|
5904
6711
|
this.log(`No more cards available for the session!`);
|
|
6712
|
+
return null;
|
|
6713
|
+
}
|
|
6714
|
+
}
|
|
6715
|
+
async nextCard(action = "dismiss-success") {
|
|
6716
|
+
this.dismissCurrentCard(action);
|
|
6717
|
+
if (this._secondsRemaining <= 0 && this.failedQ.length === 0) {
|
|
5905
6718
|
this._currentCard = null;
|
|
6719
|
+
return null;
|
|
6720
|
+
}
|
|
6721
|
+
let card = this.hydrationService.dequeueHydratedCard();
|
|
6722
|
+
if (!card && this.hasAvailableCards()) {
|
|
6723
|
+
card = await this.hydrationService.waitForHydratedCard();
|
|
5906
6724
|
}
|
|
5907
|
-
|
|
6725
|
+
await this.hydrationService.ensureHydratedCards();
|
|
6726
|
+
if (card) {
|
|
6727
|
+
this._currentCard = card;
|
|
6728
|
+
} else {
|
|
6729
|
+
this._currentCard = null;
|
|
6730
|
+
}
|
|
6731
|
+
return card;
|
|
6732
|
+
}
|
|
6733
|
+
/**
|
|
6734
|
+
* Public API for processing user responses to cards.
|
|
6735
|
+
* @param cardRecord User's response record
|
|
6736
|
+
* @param cardHistory Promise resolving to the card's history
|
|
6737
|
+
* @param courseRegistrationDoc User's course registration document
|
|
6738
|
+
* @param currentCard Current study session record
|
|
6739
|
+
* @param courseId Course identifier
|
|
6740
|
+
* @param cardId Card identifier
|
|
6741
|
+
* @param maxAttemptsPerView Maximum attempts allowed per view
|
|
6742
|
+
* @param maxSessionViews Maximum session views for this card
|
|
6743
|
+
* @param sessionViews Current number of session views
|
|
6744
|
+
* @returns ResponseResult with navigation and UI instructions
|
|
6745
|
+
*/
|
|
6746
|
+
async submitResponse(cardRecord, cardHistory, courseRegistrationDoc, currentCard, courseId, cardId, maxAttemptsPerView, maxSessionViews, sessionViews) {
|
|
6747
|
+
const studySessionItem = {
|
|
6748
|
+
...currentCard.item
|
|
6749
|
+
};
|
|
6750
|
+
return await this.services.response.processResponse(
|
|
6751
|
+
cardRecord,
|
|
6752
|
+
cardHistory,
|
|
6753
|
+
studySessionItem,
|
|
6754
|
+
courseRegistrationDoc,
|
|
6755
|
+
currentCard,
|
|
6756
|
+
courseId,
|
|
6757
|
+
cardId,
|
|
6758
|
+
maxAttemptsPerView,
|
|
6759
|
+
maxSessionViews,
|
|
6760
|
+
sessionViews
|
|
6761
|
+
);
|
|
5908
6762
|
}
|
|
5909
6763
|
dismissCurrentCard(action = "dismiss-success") {
|
|
5910
6764
|
if (this._currentCard) {
|
|
5911
6765
|
if (action === "dismiss-success") {
|
|
5912
6766
|
} else if (action === "marked-failed") {
|
|
6767
|
+
this.hydrationService.cacheFailedCard(this._currentCard);
|
|
5913
6768
|
let failedItem;
|
|
5914
|
-
if (isReview(this._currentCard)) {
|
|
6769
|
+
if (isReview(this._currentCard.item)) {
|
|
5915
6770
|
failedItem = {
|
|
5916
|
-
cardID: this._currentCard.cardID,
|
|
5917
|
-
courseID: this._currentCard.courseID,
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
contentSourceType: this._currentCard.contentSourceType,
|
|
6771
|
+
cardID: this._currentCard.item.cardID,
|
|
6772
|
+
courseID: this._currentCard.item.courseID,
|
|
6773
|
+
contentSourceID: this._currentCard.item.contentSourceID,
|
|
6774
|
+
contentSourceType: this._currentCard.item.contentSourceType,
|
|
5921
6775
|
status: "failed-review",
|
|
5922
|
-
reviewID: this._currentCard.reviewID
|
|
6776
|
+
reviewID: this._currentCard.item.reviewID
|
|
5923
6777
|
};
|
|
5924
6778
|
} else {
|
|
5925
6779
|
failedItem = {
|
|
5926
|
-
cardID: this._currentCard.cardID,
|
|
5927
|
-
courseID: this._currentCard.courseID,
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
contentSourceType: this._currentCard.contentSourceType,
|
|
6780
|
+
cardID: this._currentCard.item.cardID,
|
|
6781
|
+
courseID: this._currentCard.item.courseID,
|
|
6782
|
+
contentSourceID: this._currentCard.item.contentSourceID,
|
|
6783
|
+
contentSourceType: this._currentCard.item.contentSourceType,
|
|
5931
6784
|
status: "failed-new"
|
|
5932
6785
|
};
|
|
5933
6786
|
}
|
|
5934
|
-
this.failedQ.add(failedItem);
|
|
6787
|
+
this.failedQ.add(failedItem, failedItem.cardID);
|
|
5935
6788
|
} else if (action === "dismiss-error") {
|
|
5936
6789
|
} else if (action === "dismiss-failed") {
|
|
5937
6790
|
}
|
|
5938
6791
|
}
|
|
5939
6792
|
}
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
// src/study/SpacedRepetition.ts
|
|
5943
|
-
init_util();
|
|
5944
|
-
init_logger();
|
|
5945
|
-
import moment6 from "moment";
|
|
5946
|
-
var duration = moment6.duration;
|
|
5947
|
-
function newInterval(user, cardHistory) {
|
|
5948
|
-
if (areQuestionRecords(cardHistory)) {
|
|
5949
|
-
return newQuestionInterval(user, cardHistory);
|
|
5950
|
-
} else {
|
|
5951
|
-
return 1e5;
|
|
5952
|
-
}
|
|
5953
|
-
}
|
|
5954
|
-
function newQuestionInterval(user, cardHistory) {
|
|
5955
|
-
const records = cardHistory.records;
|
|
5956
|
-
const currentAttempt = records[records.length - 1];
|
|
5957
|
-
const lastInterval = lastSuccessfulInterval(records);
|
|
5958
|
-
if (lastInterval > cardHistory.bestInterval) {
|
|
5959
|
-
cardHistory.bestInterval = lastInterval;
|
|
5960
|
-
void user.update(cardHistory._id, {
|
|
5961
|
-
bestInterval: lastInterval
|
|
5962
|
-
});
|
|
6793
|
+
hasAvailableCards() {
|
|
6794
|
+
return this.reviewQ.length > 0 || this.newQ.length > 0 || this.failedQ.length > 0;
|
|
5963
6795
|
}
|
|
5964
|
-
|
|
5965
|
-
|
|
5966
|
-
|
|
5967
|
-
|
|
5968
|
-
|
|
5969
|
-
|
|
5970
|
-
if (
|
|
5971
|
-
|
|
5972
|
-
logger.debug(`Weighted average interval calculation:
|
|
5973
|
-
(${cardHistory.lapses} * ${interval} + ${cardHistory.streak} * ${cardHistory.bestInterval}) / (${cardHistory.lapses} + ${cardHistory.streak}) = ${ret}`);
|
|
5974
|
-
return ret;
|
|
6796
|
+
/**
|
|
6797
|
+
* Helper method for CardHydrationService to remove items from appropriate queue.
|
|
6798
|
+
*/
|
|
6799
|
+
removeItemFromQueue(item) {
|
|
6800
|
+
if (this.reviewQ.peek(0) === item) {
|
|
6801
|
+
this.reviewQ.dequeue((queueItem) => queueItem.cardID);
|
|
6802
|
+
} else if (this.newQ.peek(0) === item) {
|
|
6803
|
+
this.newQ.dequeue((queueItem) => queueItem.cardID);
|
|
5975
6804
|
} else {
|
|
5976
|
-
|
|
5977
|
-
}
|
|
5978
|
-
} else {
|
|
5979
|
-
return 0;
|
|
5980
|
-
}
|
|
5981
|
-
}
|
|
5982
|
-
function lastSuccessfulInterval(cardHistory) {
|
|
5983
|
-
for (let i = cardHistory.length - 1; i >= 1; i--) {
|
|
5984
|
-
if (cardHistory[i].priorAttemps === 0 && cardHistory[i].isCorrect) {
|
|
5985
|
-
const lastInterval = secondsBetween(cardHistory[i - 1].timeStamp, cardHistory[i].timeStamp);
|
|
5986
|
-
const ret = Math.max(lastInterval, 20 * 60 * 60);
|
|
5987
|
-
logger.debug(`Last interval w/ this card was: ${lastInterval}s, returning ${ret}s`);
|
|
5988
|
-
return ret;
|
|
6805
|
+
this.failedQ.dequeue((queueItem) => queueItem.cardID);
|
|
5989
6806
|
}
|
|
5990
6807
|
}
|
|
5991
|
-
|
|
5992
|
-
}
|
|
5993
|
-
function getStreak(records) {
|
|
5994
|
-
let streak = 0;
|
|
5995
|
-
let index = records.length - 1;
|
|
5996
|
-
while (index >= 0 && records[index].isCorrect) {
|
|
5997
|
-
index--;
|
|
5998
|
-
streak++;
|
|
5999
|
-
}
|
|
6000
|
-
return streak;
|
|
6001
|
-
}
|
|
6002
|
-
function getLapses(records) {
|
|
6003
|
-
return records.filter((r) => r.isCorrect === false).length;
|
|
6004
|
-
}
|
|
6005
|
-
function getInitialInterval(cardHistory) {
|
|
6006
|
-
logger.warn(`history of length: ${cardHistory.length} ignored!`);
|
|
6007
|
-
return 60 * 60 * 24 * 3;
|
|
6008
|
-
}
|
|
6009
|
-
function secondsBetween(start, end) {
|
|
6010
|
-
start = moment6(start);
|
|
6011
|
-
end = moment6(end);
|
|
6012
|
-
const ret = duration(end.diff(start)).asSeconds();
|
|
6013
|
-
return ret;
|
|
6014
|
-
}
|
|
6808
|
+
};
|
|
6015
6809
|
|
|
6016
6810
|
// src/index.ts
|
|
6017
6811
|
init_factory();
|