@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.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
- timeout: 6e4
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
- await this.readDB.info();
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
- try {
454
- let doc = await this.readDB.get(id);
455
- logger.debug(`Retrieved doc: ${id}`);
456
- while (this.pendingUpdates[id].length !== 0) {
457
- const update = this.pendingUpdates[id].splice(0, 1)[0];
458
- if (typeof update === "function") {
459
- doc = { ...doc, ...update(doc) };
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
- doc = {
462
- ...doc,
463
- ...update
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
- pouchDBincludeCredentialsConfig
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({ limit, elo: "user" }, (c) => {
931
- if (activeCards.some((ac) => c.includes(ac))) {
932
- return false;
933
- } else {
934
- return true;
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
- })).map((c) => {
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", ".ts"];
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) => `${this.id}-${c.id}-${c.key}`);
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(this.id, cardId, tagId, (await this._getCurrentUser()).getUsername(), updateELO);
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
- const strategy = {
1418
- id: "ELO",
1419
- docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
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
- return Promise.resolve(strategies);
1539
+ };
1540
+ return Promise.resolve(strategy);
1541
+ } else {
1542
+ return this.db.get(id);
1543
+ }
1444
1544
  }
1445
- addNavigationStrategy(data) {
1446
- logger.debug(`[courseDB] Adding navigation strategy: ${data.id}`);
1447
- logger.debug(JSON.stringify(data));
1448
- return Promise.resolve();
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
- id: "ELO",
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: split[1],
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
- pouchDBincludeCredentialsConfig
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(`New Cards from classroom ${this._cfg.name}: ${ret.map((c) => c.qualifiedID)}`);
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.qualifiedID.includes(ac))) {
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
- pouchDBincludeCredentialsConfig
1917
+ createPouchDBConfig()
1708
1918
  );
1709
1919
  this._stuDb = new pouchdb_setup_default(
1710
1920
  ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + stuDbName,
1711
- pouchDBincludeCredentialsConfig
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
- pouchDBincludeCredentialsConfig
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("CouchDB server configuration not properly initialized");
2068
+ throw new Error(`CouchDB server configuration not properly initialized. Protocol: "${ENV.COUCHDB_SERVER_PROTOCOL}", URL: "${ENV.COUCHDB_SERVER_URL}"`);
1859
2069
  }
1860
- const url = `${ENV.COUCHDB_SERVER_PROTOCOL}://${ENV.COUCHDB_SERVER_URL}_session`;
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
- logger.error(`Session check error: ${error}`);
1873
- throw new Error(`Session check failed: ${error}`);
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
- pouchDBincludeCredentialsConfig
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
- pouchDBincludeCredentialsConfig
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) => `${r.doc.courseId}-${r.doc.cardId}`);
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
- void this.applyDesignDocs();
2700
- void this.deduplicateReviews();
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
- const initCardHistory = {
2799
- _id: cardHistoryID,
2800
- cardID: record.cardID,
2801
- courseID: record.courseID,
2802
- records: [record],
2803
- lapses: 0,
2804
- streak: 0,
2805
- bestInterval: 0
2806
- };
2807
- const putResult = await this.writeDB.put(initCardHistory);
2808
- return { ...initCardHistory, _rev: putResult.rev };
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 cardIds = await this.unpacker.queryByElo(1e3, limit || 10);
3561
- return cardIds.map((cardId) => ({
3562
- status: "new",
3563
- qualifiedID: `${this.courseId}-${cardId}`,
3564
- cardID: cardId,
3565
- contentSourceType: "course",
3566
- contentSourceID: this.courseId,
3567
- courseID: this.courseId
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((cardId) => ({
3886
+ return cardIds.slice(0, options.limit).map((card) => ({
3590
3887
  status: "new",
3591
- qualifiedID: `${this.courseId}-${cardId}`,
3592
- cardID: 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
- id: "ELO",
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
- if (!this.manifests[courseId]) {
3800
- logger.warn(`Course ${courseId} not found`);
3801
- return {};
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
- manifests: config.manifests || {}
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
- for (const [courseId, manifest] of Object.entries(this.config.manifests)) {
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.config.manifests[courseId];
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.config.manifests);
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/SessionController.ts
4576
+ // src/study/services/SrsService.ts
4232
4577
  init_couch();
4578
+ import moment7 from "moment";
4233
4579
 
4234
- // src/util/index.ts
4235
- init_Loggable();
4236
-
4237
- // src/util/packer/CouchDBToStaticPacker.ts
4238
- init_types_legacy();
4580
+ // src/study/SpacedRepetition.ts
4581
+ init_util();
4239
4582
  init_logger();
4240
- var CouchDBToStaticPacker = class {
4241
- config;
4242
- sourceDB = null;
4243
- constructor(config = {}) {
4244
- this.config = {
4245
- chunkSize: 1e3,
4246
- includeAttachments: true,
4247
- ...config
4248
- };
4249
- }
4250
- /**
4251
- * Pack a CouchDB course database into static data structures
4252
- */
4253
- async packCourse(sourceDB, courseId) {
4254
- logger.info(`Starting static pack for course: ${courseId}`);
4255
- this.sourceDB = sourceDB;
4256
- const manifest = {
4257
- version: "1.0.0",
4258
- courseId,
4259
- courseName: "",
4260
- courseConfig: null,
4261
- lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
4262
- documentCount: 0,
4263
- chunks: [],
4264
- indices: [],
4265
- designDocs: []
4266
- };
4267
- const courseConfig = await this.extractCourseConfig(sourceDB);
4268
- manifest.courseName = courseConfig.name;
4269
- manifest.courseConfig = courseConfig;
4270
- manifest.designDocs = await this.extractDesignDocs(sourceDB);
4271
- const docsByType = await this.extractDocumentsByType(sourceDB);
4272
- const attachments = /* @__PURE__ */ new Map();
4273
- if (this.config.includeAttachments) {
4274
- await this.extractAllAttachments(docsByType, attachments);
4275
- }
4276
- const chunks = /* @__PURE__ */ new Map();
4277
- for (const [docType, docs] of Object.entries(docsByType)) {
4278
- const chunkMetadata = this.createChunks(docs, docType);
4279
- manifest.chunks.push(...chunkMetadata);
4280
- manifest.documentCount += docs.length;
4281
- this.prepareChunkData(chunkMetadata, docs, chunks);
4282
- }
4283
- const indices = /* @__PURE__ */ new Map();
4284
- manifest.indices = await this.buildIndices(docsByType, manifest.designDocs, indices);
4285
- return {
4286
- manifest,
4287
- chunks,
4288
- indices,
4289
- attachments
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
- _currentCard = null;
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 === 0) {
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._isInitialized = true;
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
- for (let i = 0; i < dueCards.length; i++) {
5825
- const card = dueCards[i];
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.qualifiedID}`);
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
- nextNewCard() {
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
- this._currentCard = null;
5865
- return this._currentCard;
6670
+ return null;
5866
6671
  }
5867
6672
  if (this._secondsRemaining < 2 && this.failedQ.length === 0) {
5868
- this._currentCard = null;
5869
- return this._currentCard;
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
- this._currentCard = this.nextNewCard();
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
- this._currentCard = this.nextNewCard();
6705
+ return this.newQ.peek(0);
5899
6706
  } else if (choice < reviewBound && this.reviewQ.length) {
5900
- this._currentCard = this.reviewQ.dequeue();
6707
+ return this.reviewQ.peek(0);
5901
6708
  } else if (this.failedQ.length) {
5902
- this._currentCard = this.failedQ.dequeue();
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
- return this._currentCard;
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
- qualifiedID: this._currentCard.qualifiedID,
5919
- contentSourceID: this._currentCard.contentSourceID,
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
- qualifiedID: this._currentCard.qualifiedID,
5929
- contentSourceID: this._currentCard.contentSourceID,
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
- if (currentAttempt.isCorrect) {
5965
- const skill = currentAttempt.performance;
5966
- logger.debug(`Demontrated skill: ${skill}`);
5967
- const interval = lastInterval * (0.75 + skill);
5968
- cardHistory.lapses = getLapses(cardHistory.records);
5969
- cardHistory.streak = getStreak(cardHistory.records);
5970
- if (cardHistory.lapses && cardHistory.streak && cardHistory.bestInterval && (cardHistory.lapses >= 0 || cardHistory.streak >= 0)) {
5971
- const ret = (cardHistory.lapses * interval + cardHistory.streak * cardHistory.bestInterval) / (cardHistory.lapses + cardHistory.streak);
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
- return interval;
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
- return getInitialInterval(cardHistory);
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();