@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.
Files changed (76) hide show
  1. package/dist/core/index.d.mts +7 -6
  2. package/dist/core/index.d.ts +7 -6
  3. package/dist/core/index.js +358 -87
  4. package/dist/core/index.js.map +1 -1
  5. package/dist/core/index.mjs +358 -87
  6. package/dist/core/index.mjs.map +1 -1
  7. package/dist/{dataLayerProvider-DqtNroSh.d.ts → dataLayerProvider-BiP3kWix.d.mts} +8 -1
  8. package/dist/{dataLayerProvider-BInqI_RF.d.mts → dataLayerProvider-DSdeyRT3.d.ts} +8 -1
  9. package/dist/impl/couch/index.d.mts +19 -7
  10. package/dist/impl/couch/index.d.ts +19 -7
  11. package/dist/impl/couch/index.js +375 -100
  12. package/dist/impl/couch/index.js.map +1 -1
  13. package/dist/impl/couch/index.mjs +374 -99
  14. package/dist/impl/couch/index.mjs.map +1 -1
  15. package/dist/impl/static/index.d.mts +23 -8
  16. package/dist/impl/static/index.d.ts +23 -8
  17. package/dist/impl/static/index.js +289 -85
  18. package/dist/impl/static/index.js.map +1 -1
  19. package/dist/impl/static/index.mjs +289 -85
  20. package/dist/impl/static/index.mjs.map +1 -1
  21. package/dist/{index-CUNnL38E.d.mts → index-Bmll7Xse.d.mts} +1 -1
  22. package/dist/{index-CLL31bEy.d.ts → index-CD8BZz2k.d.ts} +1 -1
  23. package/dist/index.d.mts +123 -20
  24. package/dist/index.d.ts +123 -20
  25. package/dist/index.js +1133 -343
  26. package/dist/index.js.map +1 -1
  27. package/dist/index.mjs +1137 -343
  28. package/dist/index.mjs.map +1 -1
  29. package/dist/pouch/index.d.mts +1 -0
  30. package/dist/pouch/index.d.ts +1 -0
  31. package/dist/pouch/index.js +49 -0
  32. package/dist/pouch/index.js.map +1 -0
  33. package/dist/pouch/index.mjs +16 -0
  34. package/dist/pouch/index.mjs.map +1 -0
  35. package/dist/{types-BefDGkKa.d.ts → types-CewsN87z.d.ts} +1 -1
  36. package/dist/{types-DC-ckZug.d.mts → types-Dbp5DaRR.d.mts} +1 -1
  37. package/dist/{types-legacy-Birv-Jx6.d.mts → types-legacy-6ettoclI.d.mts} +17 -2
  38. package/dist/{types-legacy-Birv-Jx6.d.ts → types-legacy-6ettoclI.d.ts} +17 -2
  39. package/dist/{userDB-DusL7OXe.d.ts → userDB-C4yyAnpp.d.mts} +89 -56
  40. package/dist/{userDB-C33Hzjgn.d.mts → userDB-CD6s6ZCp.d.ts} +89 -56
  41. package/dist/util/packer/index.d.mts +3 -3
  42. package/dist/util/packer/index.d.ts +3 -3
  43. package/package.json +3 -3
  44. package/src/core/interfaces/contentSource.ts +3 -2
  45. package/src/core/interfaces/courseDB.ts +26 -3
  46. package/src/core/interfaces/dataLayerProvider.ts +9 -1
  47. package/src/core/interfaces/userDB.ts +80 -64
  48. package/src/core/navigators/elo.ts +10 -7
  49. package/src/core/navigators/hardcodedOrder.ts +64 -0
  50. package/src/core/navigators/index.ts +2 -1
  51. package/src/core/types/contentNavigationStrategy.ts +2 -1
  52. package/src/core/types/types-legacy.ts +7 -2
  53. package/src/impl/common/BaseUserDB.ts +60 -14
  54. package/src/impl/couch/CouchDBSyncStrategy.ts +2 -2
  55. package/src/impl/couch/PouchDataLayerProvider.ts +21 -0
  56. package/src/impl/couch/adminDB.ts +2 -2
  57. package/src/impl/couch/auth.ts +13 -4
  58. package/src/impl/couch/classroomDB.ts +10 -12
  59. package/src/impl/couch/courseAPI.ts +2 -2
  60. package/src/impl/couch/courseDB.ts +204 -38
  61. package/src/impl/couch/courseLookupDB.ts +4 -3
  62. package/src/impl/couch/index.ts +36 -4
  63. package/src/impl/couch/pouchdb-setup.ts +3 -3
  64. package/src/impl/couch/updateQueue.ts +59 -36
  65. package/src/impl/static/StaticDataLayerProvider.ts +68 -17
  66. package/src/impl/static/courseDB.ts +64 -20
  67. package/src/impl/static/coursesDB.ts +10 -6
  68. package/src/pouch/index.ts +2 -0
  69. package/src/study/ItemQueue.ts +58 -0
  70. package/src/study/SessionController.ts +182 -111
  71. package/src/study/SpacedRepetition.ts +1 -1
  72. package/src/study/services/CardHydrationService.ts +153 -0
  73. package/src/study/services/EloService.ts +85 -0
  74. package/src/study/services/ResponseProcessor.ts +224 -0
  75. package/src/study/services/SrsService.ts +44 -0
  76. 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
- timeout: 6e4
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
- await this.readDB.info();
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
- try {
477
- let doc = await this.readDB.get(id);
478
- logger.debug(`Retrieved doc: ${id}`);
479
- while (this.pendingUpdates[id].length !== 0) {
480
- const update = this.pendingUpdates[id].splice(0, 1)[0];
481
- if (typeof update === "function") {
482
- doc = { ...doc, ...update(doc) };
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
- doc = {
485
- ...doc,
486
- ...update
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
- pouchDBincludeCredentialsConfig
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({ limit, elo: "user" }, (c) => {
954
- if (activeCards.some((ac) => c.includes(ac))) {
955
- return false;
956
- } else {
957
- return true;
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
- })).map((c) => {
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", ".ts"];
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) => `${this.id}-${c.id}-${c.key}`);
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(this.id, cardId, tagId, (await this._getCurrentUser()).getUsername(), updateELO);
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
- const strategy = {
1436
- id: "ELO",
1437
- docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
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
- return Promise.resolve(strategies);
1557
+ };
1558
+ return Promise.resolve(strategy);
1559
+ } else {
1560
+ return this.db.get(id);
1561
+ }
1462
1562
  }
1463
- addNavigationStrategy(data) {
1464
- logger.debug(`[courseDB] Adding navigation strategy: ${data.id}`);
1465
- logger.debug(JSON.stringify(data));
1466
- return Promise.resolve();
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
- id: "ELO",
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: split[1],
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
- pouchDBincludeCredentialsConfig
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(`New Cards from classroom ${this._cfg.name}: ${ret.map((c) => c.qualifiedID)}`);
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.qualifiedID.includes(ac))) {
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
- pouchDBincludeCredentialsConfig
1935
+ createPouchDBConfig()
1726
1936
  );
1727
1937
  this._stuDb = new pouchdb_setup_default(
1728
1938
  ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + stuDbName,
1729
- pouchDBincludeCredentialsConfig
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
- pouchDBincludeCredentialsConfig
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("CouchDB server configuration not properly initialized");
2085
+ throw new Error(`CouchDB server configuration not properly initialized. Protocol: "${ENV.COUCHDB_SERVER_PROTOCOL}", URL: "${ENV.COUCHDB_SERVER_URL}"`);
1876
2086
  }
1877
- const url = `${ENV.COUCHDB_SERVER_PROTOCOL}://${ENV.COUCHDB_SERVER_URL}_session`;
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
- logger.error(`Session check error: ${error}`);
1890
- throw new Error(`Session check failed: ${error}`);
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
- pouchDBincludeCredentialsConfig
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
- pouchDBincludeCredentialsConfig
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) => `${r.doc.courseId}-${r.doc.cardId}`);
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
- void this.applyDesignDocs();
2719
- void this.deduplicateReviews();
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
- const initCardHistory = {
2818
- _id: cardHistoryID,
2819
- cardID: record.cardID,
2820
- courseID: record.courseID,
2821
- records: [record],
2822
- lapses: 0,
2823
- streak: 0,
2824
- bestInterval: 0
2825
- };
2826
- const putResult = await this.writeDB.put(initCardHistory);
2827
- return { ...initCardHistory, _rev: putResult.rev };
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 cardIds = await this.unpacker.queryByElo(1e3, limit || 10);
3580
- return cardIds.map((cardId) => ({
3581
- status: "new",
3582
- qualifiedID: `${this.courseId}-${cardId}`,
3583
- cardID: cardId,
3584
- contentSourceType: "course",
3585
- contentSourceID: this.courseId,
3586
- courseID: this.courseId
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((cardId) => ({
3905
+ return cardIds.slice(0, options.limit).map((card) => ({
3609
3906
  status: "new",
3610
- qualifiedID: `${this.courseId}-${cardId}`,
3611
- cardID: 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
- id: "ELO",
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
- if (!this.manifests[courseId]) {
3819
- logger.warn(`Course ${courseId} not found`);
3820
- return {};
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
- manifests: config.manifests || {}
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
- for (const [courseId, manifest] of Object.entries(this.config.manifests)) {
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.config.manifests[courseId];
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.config.manifests);
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/SessionController.ts
4638
+ // src/study/services/SrsService.ts
4639
+ var import_moment7 = __toESM(require("moment"));
4294
4640
  init_couch();
4295
4641
 
4296
- // src/util/index.ts
4297
- init_Loggable();
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 CouchDBToStaticPacker = class {
4303
- config;
4304
- sourceDB = null;
4305
- constructor(config = {}) {
4306
- this.config = {
4307
- chunkSize: 1e3,
4308
- includeAttachments: true,
4309
- ...config
4310
- };
4311
- }
4312
- /**
4313
- * Pack a CouchDB course database into static data structures
4314
- */
4315
- async packCourse(sourceDB, courseId) {
4316
- logger.info(`Starting static pack for course: ${courseId}`);
4317
- this.sourceDB = sourceDB;
4318
- const manifest = {
4319
- version: "1.0.0",
4320
- courseId,
4321
- courseName: "",
4322
- courseConfig: null,
4323
- lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
4324
- documentCount: 0,
4325
- chunks: [],
4326
- indices: [],
4327
- designDocs: []
4328
- };
4329
- const courseConfig = await this.extractCourseConfig(sourceDB);
4330
- manifest.courseName = courseConfig.name;
4331
- manifest.courseConfig = courseConfig;
4332
- manifest.designDocs = await this.extractDesignDocs(sourceDB);
4333
- const docsByType = await this.extractDocumentsByType(sourceDB);
4334
- const attachments = /* @__PURE__ */ new Map();
4335
- if (this.config.includeAttachments) {
4336
- await this.extractAllAttachments(docsByType, attachments);
4337
- }
4338
- const chunks = /* @__PURE__ */ new Map();
4339
- for (const [docType, docs] of Object.entries(docsByType)) {
4340
- const chunkMetadata = this.createChunks(docs, docType);
4341
- manifest.chunks.push(...chunkMetadata);
4342
- manifest.documentCount += docs.length;
4343
- this.prepareChunkData(chunkMetadata, docs, chunks);
4344
- }
4345
- const indices = /* @__PURE__ */ new Map();
4346
- manifest.indices = await this.buildIndices(docsByType, manifest.designDocs, indices);
4347
- return {
4348
- manifest,
4349
- chunks,
4350
- indices,
4351
- attachments
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
- _currentCard = null;
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 === 0) {
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._isInitialized = true;
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
- for (let i = 0; i < dueCards.length; i++) {
5887
- const card = dueCards[i];
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.qualifiedID}`);
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
- nextNewCard() {
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
- this._currentCard = null;
5927
- return this._currentCard;
6728
+ return null;
5928
6729
  }
5929
6730
  if (this._secondsRemaining < 2 && this.failedQ.length === 0) {
5930
- this._currentCard = null;
5931
- return this._currentCard;
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
- this._currentCard = this.nextNewCard();
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
- this._currentCard = this.nextNewCard();
6763
+ return this.newQ.peek(0);
5961
6764
  } else if (choice < reviewBound && this.reviewQ.length) {
5962
- this._currentCard = this.reviewQ.dequeue();
6765
+ return this.reviewQ.peek(0);
5963
6766
  } else if (this.failedQ.length) {
5964
- this._currentCard = this.failedQ.dequeue();
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
- return this._currentCard;
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
- qualifiedID: this._currentCard.qualifiedID,
5981
- contentSourceID: this._currentCard.contentSourceID,
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
- qualifiedID: this._currentCard.qualifiedID,
5991
- contentSourceID: this._currentCard.contentSourceID,
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
- if (currentAttempt.isCorrect) {
6027
- const skill = currentAttempt.performance;
6028
- logger.debug(`Demontrated skill: ${skill}`);
6029
- const interval = lastInterval * (0.75 + skill);
6030
- cardHistory.lapses = getLapses(cardHistory.records);
6031
- cardHistory.streak = getStreak(cardHistory.records);
6032
- if (cardHistory.lapses && cardHistory.streak && cardHistory.bestInterval && (cardHistory.lapses >= 0 || cardHistory.streak >= 0)) {
6033
- const ret = (cardHistory.lapses * interval + cardHistory.streak * cardHistory.bestInterval) / (cardHistory.lapses + cardHistory.streak);
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
- return interval;
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
- return getInitialInterval(cardHistory);
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();