@vue-skuilder/db 0.1.11-7 → 0.1.11

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 (68) hide show
  1. package/dist/core/index.d.mts +5 -5
  2. package/dist/core/index.d.ts +5 -5
  3. package/dist/core/index.js +212 -50
  4. package/dist/core/index.js.map +1 -1
  5. package/dist/core/index.mjs +212 -50
  6. package/dist/core/index.mjs.map +1 -1
  7. package/dist/{dataLayerProvider-DqtNroSh.d.ts → dataLayerProvider-VieuAAkV.d.mts} +8 -1
  8. package/dist/{dataLayerProvider-BInqI_RF.d.mts → dataLayerProvider-juuqUHOP.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 +229 -63
  12. package/dist/impl/couch/index.js.map +1 -1
  13. package/dist/impl/couch/index.mjs +228 -62
  14. package/dist/impl/couch/index.mjs.map +1 -1
  15. package/dist/impl/static/index.d.mts +13 -6
  16. package/dist/impl/static/index.d.ts +13 -6
  17. package/dist/impl/static/index.js +142 -46
  18. package/dist/impl/static/index.js.map +1 -1
  19. package/dist/impl/static/index.mjs +142 -46
  20. package/dist/impl/static/index.mjs.map +1 -1
  21. package/dist/{index-CLL31bEy.d.ts → index-CWY6yhkV.d.ts} +1 -1
  22. package/dist/{index-CUNnL38E.d.mts → index-DZyxHCcf.d.mts} +1 -1
  23. package/dist/index.d.mts +28 -20
  24. package/dist/index.d.ts +28 -20
  25. package/dist/index.js +374 -108
  26. package/dist/index.js.map +1 -1
  27. package/dist/index.mjs +378 -108
  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-DC-ckZug.d.mts → types-Che4wTwA.d.mts} +1 -1
  36. package/dist/{types-BefDGkKa.d.ts → types-DtoI27Xh.d.ts} +1 -1
  37. package/dist/{types-legacy-Birv-Jx6.d.mts → types-legacy-B8ahaCbj.d.mts} +5 -1
  38. package/dist/{types-legacy-Birv-Jx6.d.ts → types-legacy-B8ahaCbj.d.ts} +5 -1
  39. package/dist/{userDB-C33Hzjgn.d.mts → userDB-B7zTQ123.d.ts} +88 -55
  40. package/dist/{userDB-DusL7OXe.d.ts → userDB-DJ8HMw83.d.mts} +88 -55
  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/index.ts +1 -1
  50. package/src/core/types/types-legacy.ts +5 -0
  51. package/src/impl/common/BaseUserDB.ts +45 -3
  52. package/src/impl/couch/CouchDBSyncStrategy.ts +2 -2
  53. package/src/impl/couch/PouchDataLayerProvider.ts +21 -0
  54. package/src/impl/couch/adminDB.ts +2 -2
  55. package/src/impl/couch/auth.ts +13 -4
  56. package/src/impl/couch/classroomDB.ts +10 -12
  57. package/src/impl/couch/courseAPI.ts +2 -2
  58. package/src/impl/couch/courseDB.ts +130 -11
  59. package/src/impl/couch/courseLookupDB.ts +4 -3
  60. package/src/impl/couch/index.ts +36 -4
  61. package/src/impl/couch/pouchdb-setup.ts +3 -3
  62. package/src/impl/couch/updateQueue.ts +51 -33
  63. package/src/impl/static/StaticDataLayerProvider.ts +11 -0
  64. package/src/impl/static/courseDB.ts +47 -8
  65. package/src/pouch/index.ts +2 -0
  66. package/src/study/SessionController.ts +168 -51
  67. package/src/study/SpacedRepetition.ts +1 -1
  68. 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
  }
@@ -450,37 +450,48 @@ var init_updateQueue = __esm({
450
450
  } else {
451
451
  if (this.pendingUpdates[id] && this.pendingUpdates[id].length > 0) {
452
452
  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) };
453
+ const MAX_RETRIES = 5;
454
+ for (let i = 0; i < MAX_RETRIES; i++) {
455
+ try {
456
+ const doc = await this.readDB.get(id);
457
+ logger.debug(`Retrieved doc: ${id}`);
458
+ let updatedDoc = { ...doc };
459
+ const updatesToApply = [...this.pendingUpdates[id]];
460
+ for (const update of updatesToApply) {
461
+ if (typeof update === "function") {
462
+ updatedDoc = { ...updatedDoc, ...update(updatedDoc) };
463
+ } else {
464
+ updatedDoc = {
465
+ ...updatedDoc,
466
+ ...update
467
+ };
468
+ }
469
+ }
470
+ await this.writeDB.put(updatedDoc);
471
+ logger.debug(`Put doc: ${id}`);
472
+ this.pendingUpdates[id].splice(0, updatesToApply.length);
473
+ if (this.pendingUpdates[id].length === 0) {
474
+ this.inprogressUpdates[id] = false;
475
+ delete this.inprogressUpdates[id];
460
476
  } else {
461
- doc = {
462
- ...doc,
463
- ...update
464
- };
477
+ return this.applyUpdates(id);
478
+ }
479
+ return updatedDoc;
480
+ } catch (e) {
481
+ if (e.name === "conflict" && i < MAX_RETRIES - 1) {
482
+ logger.warn(`Conflict on update for doc ${id}, retry #${i + 1}`);
483
+ await new Promise((res) => setTimeout(res, 50 * Math.random()));
484
+ } else {
485
+ delete this.inprogressUpdates[id];
486
+ if (this.pendingUpdates[id]) {
487
+ delete this.pendingUpdates[id];
488
+ }
489
+ logger.error(`Error on attemped update (retry ${i}): ${JSON.stringify(e)}`);
490
+ throw e;
465
491
  }
466
492
  }
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
493
  }
494
+ throw new Error(`UpdateQueue failed for doc ${id} after ${MAX_RETRIES} retries.`);
484
495
  } else {
485
496
  throw new Error(`Empty Updates Queue Triggered`);
486
497
  }
@@ -735,7 +746,7 @@ function getCourseDB(courseID) {
735
746
  const dbName = `coursedb-${courseID}`;
736
747
  return new pouchdb_setup_default(
737
748
  ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + dbName,
738
- pouchDBincludeCredentialsConfig
749
+ createPouchDBConfig()
739
750
  );
740
751
  }
741
752
  var AlreadyTaggedErr;
@@ -857,6 +868,7 @@ var init_courseLookupDB = __esm({
857
868
  const doc = await _CourseLookup._db.get(courseID);
858
869
  return await _CourseLookup._db.remove(doc);
859
870
  }
871
+ // [ ] rename to allCourses()
860
872
  static async allCourseWare() {
861
873
  const resp = await _CourseLookup._db.allDocs({
862
874
  include_docs: true
@@ -927,13 +939,16 @@ var init_elo = __esm({
927
939
  }
928
940
  async getNewCards(limit = 99) {
929
941
  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;
942
+ return (await this.course.getCardsCenteredAtELO(
943
+ { limit, elo: "user" },
944
+ (c) => {
945
+ if (activeCards.some((ac) => c.cardID === ac.cardID)) {
946
+ return false;
947
+ } else {
948
+ return true;
949
+ }
935
950
  }
936
- })).map((c) => {
951
+ )).map((c) => {
937
952
  return {
938
953
  ...c,
939
954
  status: "new"
@@ -981,7 +996,7 @@ var init_navigators = __esm({
981
996
  static async create(user, course, strategyData) {
982
997
  const implementingClass = strategyData.implementingClass;
983
998
  let NavigatorImpl;
984
- const variations = ["", ".js", ".ts"];
999
+ const variations = [".ts", ".js", ""];
985
1000
  for (const ext of variations) {
986
1001
  try {
987
1002
  const module = await globImport(`./${implementingClass}${ext}`);
@@ -1290,7 +1305,13 @@ var init_courseDB = __esm({
1290
1305
  } else {
1291
1306
  return s;
1292
1307
  }
1293
- }).map((c) => `${this.id}-${c.id}-${c.key}`);
1308
+ }).map((c) => {
1309
+ return {
1310
+ courseID: this.id,
1311
+ cardID: c.id,
1312
+ elo: c.key
1313
+ };
1314
+ });
1294
1315
  const str = `below:
1295
1316
  ${below.rows.map((r) => ` ${r.id}-${r.key}
1296
1317
  `)}
@@ -1345,7 +1366,13 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
1345
1366
  }
1346
1367
  }
1347
1368
  async addTagToCard(cardId, tagId, updateELO) {
1348
- return await addTagToCard(this.id, cardId, tagId, (await this._getCurrentUser()).getUsername(), updateELO);
1369
+ return await addTagToCard(
1370
+ this.id,
1371
+ cardId,
1372
+ tagId,
1373
+ (await this._getCurrentUser()).getUsername(),
1374
+ updateELO
1375
+ );
1349
1376
  }
1350
1377
  async removeTagFromCard(cardId, tagId) {
1351
1378
  return await removeTagFromCard(this.id, cardId, tagId);
@@ -1538,17 +1565,93 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
1538
1565
  selectedCards.push(card);
1539
1566
  }
1540
1567
  return selectedCards.map((c) => {
1541
- const split = c.split("-");
1542
1568
  return {
1543
1569
  courseID: this.id,
1544
- cardID: split[1],
1545
- qualifiedID: `${split[0]}-${split[1]}`,
1570
+ cardID: c.cardID,
1546
1571
  contentSourceType: "course",
1547
1572
  contentSourceID: this.id,
1573
+ elo: c.elo,
1548
1574
  status: "new"
1549
1575
  };
1550
1576
  });
1551
1577
  }
1578
+ // Admin search methods
1579
+ async searchCards(query) {
1580
+ logger.log(`[CourseDB ${this.id}] Searching for: "${query}"`);
1581
+ let displayableData;
1582
+ try {
1583
+ displayableData = await this.db.find({
1584
+ selector: {
1585
+ docType: "DISPLAYABLE_DATA",
1586
+ "data.0.data": { $regex: `.*${query}.*` }
1587
+ }
1588
+ });
1589
+ logger.log(`[CourseDB ${this.id}] Regex search on data[0].data successful`);
1590
+ } catch (regexError) {
1591
+ logger.log(
1592
+ `[CourseDB ${this.id}] Regex search failed, falling back to manual search:`,
1593
+ regexError
1594
+ );
1595
+ const allDisplayable = await this.db.find({
1596
+ selector: {
1597
+ docType: "DISPLAYABLE_DATA"
1598
+ }
1599
+ });
1600
+ logger.log(
1601
+ `[CourseDB ${this.id}] Retrieved ${allDisplayable.docs.length} documents for manual filtering`
1602
+ );
1603
+ displayableData = {
1604
+ docs: allDisplayable.docs.filter((doc) => {
1605
+ const docString = JSON.stringify(doc).toLowerCase();
1606
+ const match = docString.includes(query.toLowerCase());
1607
+ if (match) {
1608
+ logger.log(`[CourseDB ${this.id}] Manual match found in document: ${doc._id}`);
1609
+ }
1610
+ return match;
1611
+ })
1612
+ };
1613
+ }
1614
+ logger.log(
1615
+ `[CourseDB ${this.id}] Found ${displayableData.docs.length} displayable data documents`
1616
+ );
1617
+ if (displayableData.docs.length === 0) {
1618
+ const allDisplayableData = await this.db.find({
1619
+ selector: {
1620
+ docType: "DISPLAYABLE_DATA"
1621
+ },
1622
+ limit: 5
1623
+ // Just sample a few
1624
+ });
1625
+ logger.log(
1626
+ `[CourseDB ${this.id}] Sample displayable data:`,
1627
+ allDisplayableData.docs.map((d) => ({
1628
+ id: d._id,
1629
+ docType: d.docType,
1630
+ dataStructure: d.data ? Object.keys(d.data) : "no data field",
1631
+ dataContent: d.data,
1632
+ fullDoc: d
1633
+ }))
1634
+ );
1635
+ }
1636
+ const allResults = [];
1637
+ for (const dd of displayableData.docs) {
1638
+ const cards = await this.db.find({
1639
+ selector: {
1640
+ docType: "CARD",
1641
+ id_displayable_data: { $in: [dd._id] }
1642
+ }
1643
+ });
1644
+ logger.log(
1645
+ `[CourseDB ${this.id}] Displayable data ${dd._id} linked to ${cards.docs.length} cards`
1646
+ );
1647
+ allResults.push(...cards.docs);
1648
+ }
1649
+ logger.log(`[CourseDB ${this.id}] Total cards found: ${allResults.length}`);
1650
+ return allResults;
1651
+ }
1652
+ async find(request) {
1653
+ return this.db.find(request);
1654
+ }
1552
1655
  };
1553
1656
  }
1554
1657
  });
@@ -1614,7 +1717,7 @@ var init_classroomDB2 = __esm({
1614
1717
  const dbName = `classdb-student-${this._id}`;
1615
1718
  this._db = new pouchdb_setup_default(
1616
1719
  ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + dbName,
1617
- pouchDBincludeCredentialsConfig
1720
+ createPouchDBConfig()
1618
1721
  );
1619
1722
  try {
1620
1723
  const cfg = await this._db.get(CLASSROOM_CONFIG);
@@ -1683,9 +1786,11 @@ var init_classroomDB2 = __esm({
1683
1786
  ret.push(await getCourseDB2(content.courseID).get(content.cardID));
1684
1787
  }
1685
1788
  }
1686
- logger.info(`New Cards from classroom ${this._cfg.name}: ${ret.map((c) => c.qualifiedID)}`);
1789
+ logger.info(
1790
+ `New Cards from classroom ${this._cfg.name}: ${ret.map((c) => `${c.courseID}-${c.cardID}`)}`
1791
+ );
1687
1792
  return ret.filter((c) => {
1688
- if (activeCards.some((ac) => c.qualifiedID.includes(ac))) {
1793
+ if (activeCards.some((ac) => c.cardID === ac.cardID)) {
1689
1794
  return false;
1690
1795
  } else {
1691
1796
  return true;
@@ -1704,11 +1809,11 @@ var init_classroomDB2 = __esm({
1704
1809
  const stuDbName = `classdb-student-${this._id}`;
1705
1810
  this._db = new pouchdb_setup_default(
1706
1811
  ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + dbName,
1707
- pouchDBincludeCredentialsConfig
1812
+ createPouchDBConfig()
1708
1813
  );
1709
1814
  this._stuDb = new pouchdb_setup_default(
1710
1815
  ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + stuDbName,
1711
- pouchDBincludeCredentialsConfig
1816
+ createPouchDBConfig()
1712
1817
  );
1713
1818
  try {
1714
1819
  return this._db.get(CLASSROOM_CONFIG).then((cfg) => {
@@ -1793,7 +1898,7 @@ var init_adminDB2 = __esm({
1793
1898
  constructor() {
1794
1899
  this.usersDB = new pouchdb_setup_default(
1795
1900
  ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + "_users",
1796
- pouchDBincludeCredentialsConfig
1901
+ createPouchDBConfig()
1797
1902
  );
1798
1903
  }
1799
1904
  async getUsers() {
@@ -1855,9 +1960,10 @@ import fetch2 from "cross-fetch";
1855
1960
  async function getCurrentSession() {
1856
1961
  try {
1857
1962
  if (ENV.COUCHDB_SERVER_URL === NOT_SET || ENV.COUCHDB_SERVER_PROTOCOL === NOT_SET) {
1858
- throw new Error("CouchDB server configuration not properly initialized");
1963
+ throw new Error(`CouchDB server configuration not properly initialized. Protocol: "${ENV.COUCHDB_SERVER_PROTOCOL}", URL: "${ENV.COUCHDB_SERVER_URL}"`);
1859
1964
  }
1860
- const url = `${ENV.COUCHDB_SERVER_PROTOCOL}://${ENV.COUCHDB_SERVER_URL}_session`;
1965
+ const baseUrl = ENV.COUCHDB_SERVER_URL.endsWith("/") ? ENV.COUCHDB_SERVER_URL.slice(0, -1) : ENV.COUCHDB_SERVER_URL;
1966
+ const url = `${ENV.COUCHDB_SERVER_PROTOCOL}://${baseUrl}/_session`;
1861
1967
  logger.debug(`Attempting session check at: ${url}`);
1862
1968
  const response = await fetch2(url, {
1863
1969
  method: "GET",
@@ -1869,8 +1975,10 @@ async function getCurrentSession() {
1869
1975
  const resp = await response.json();
1870
1976
  return resp;
1871
1977
  } catch (error) {
1872
- logger.error(`Session check error: ${error}`);
1873
- throw new Error(`Session check failed: ${error}`);
1978
+ const baseUrl = ENV.COUCHDB_SERVER_URL.endsWith("/") ? ENV.COUCHDB_SERVER_URL.slice(0, -1) : ENV.COUCHDB_SERVER_URL;
1979
+ const url = `${ENV.COUCHDB_SERVER_PROTOCOL}://${baseUrl}/_session`;
1980
+ logger.error(`Session check error attempting to connect to: ${url} - ${error}`);
1981
+ throw new Error(`Session check failed connecting to ${url}: ${error}`);
1874
1982
  }
1875
1983
  }
1876
1984
  async function getLoggedInUsername() {
@@ -2063,7 +2171,7 @@ var init_CouchDBSyncStrategy = __esm({
2063
2171
  log3(`Fetching user database: ${dbName} (${username})`);
2064
2172
  const ret = new pouchdb_setup_default(
2065
2173
  ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + dbName,
2066
- pouchDBincludeCredentialsConfig
2174
+ createPouchDBConfig()
2067
2175
  );
2068
2176
  if (guestAccount) {
2069
2177
  updateGuestAccountExpirationDate(ret);
@@ -2078,10 +2186,29 @@ var init_CouchDBSyncStrategy = __esm({
2078
2186
  import fetch3 from "cross-fetch";
2079
2187
  import moment4 from "moment";
2080
2188
  import process2 from "process";
2189
+ function createPouchDBConfig() {
2190
+ const hasExplicitCredentials = ENV.COUCHDB_USERNAME && ENV.COUCHDB_PASSWORD;
2191
+ const isNodeEnvironment2 = typeof window === "undefined";
2192
+ if (hasExplicitCredentials && isNodeEnvironment2) {
2193
+ return {
2194
+ fetch(url, opts = {}) {
2195
+ const basicAuth = btoa(`${ENV.COUCHDB_USERNAME}:${ENV.COUCHDB_PASSWORD}`);
2196
+ const headers = new Headers(opts.headers || {});
2197
+ headers.set("Authorization", `Basic ${basicAuth}`);
2198
+ const newOpts = {
2199
+ ...opts,
2200
+ headers
2201
+ };
2202
+ return pouchdb_setup_default.fetch(url, newOpts);
2203
+ }
2204
+ };
2205
+ }
2206
+ return pouchDBincludeCredentialsConfig;
2207
+ }
2081
2208
  function getCourseDB2(courseID) {
2082
2209
  return new pouchdb_setup_default(
2083
2210
  ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + "coursedb-" + courseID,
2084
- pouchDBincludeCredentialsConfig
2211
+ createPouchDBConfig()
2085
2212
  );
2086
2213
  }
2087
2214
  function getCourseDocs(courseID, docIDs, options = {}) {
@@ -2370,6 +2497,9 @@ Currently logged-in as ${this._username}.`
2370
2497
  await this.init();
2371
2498
  return ret;
2372
2499
  }
2500
+ async get(id) {
2501
+ return this.localDB.get(id);
2502
+ }
2373
2503
  update(id, update) {
2374
2504
  return this.updateQueue.update(id, update);
2375
2505
  }
@@ -2416,7 +2546,12 @@ Currently logged-in as ${this._username}.`
2416
2546
  endkey: keys.endkey,
2417
2547
  include_docs: true
2418
2548
  });
2419
- return reviews.rows.map((r) => `${r.doc.courseId}-${r.doc.cardId}`);
2549
+ return reviews.rows.map((r) => {
2550
+ return {
2551
+ courseID: r.doc.courseId,
2552
+ cardID: r.doc.cardId
2553
+ };
2554
+ });
2420
2555
  }
2421
2556
  async getActivityRecords() {
2422
2557
  try {
@@ -2696,8 +2831,18 @@ Currently logged-in as ${this._username}.`
2696
2831
  }
2697
2832
  this.setDBandQ();
2698
2833
  this.syncStrategy.startSync(this.localDB, this.remoteDB);
2699
- void this.applyDesignDocs();
2700
- void this.deduplicateReviews();
2834
+ this.applyDesignDocs().catch((error) => {
2835
+ log4(`Error in applyDesignDocs background task: ${error}`);
2836
+ if (error && typeof error === "object") {
2837
+ log4(`Full error details in applyDesignDocs: ${JSON.stringify(error)}`);
2838
+ }
2839
+ });
2840
+ this.deduplicateReviews().catch((error) => {
2841
+ log4(`Error in deduplicateReviews background task: ${error}`);
2842
+ if (error && typeof error === "object") {
2843
+ log4(`Full error details in background task: ${JSON.stringify(error)}`);
2844
+ }
2845
+ });
2701
2846
  _BaseUser._initialized = true;
2702
2847
  }
2703
2848
  static designDocs = [
@@ -2715,10 +2860,15 @@ Currently logged-in as ${this._username}.`
2715
2860
  }
2716
2861
  ];
2717
2862
  async applyDesignDocs() {
2863
+ log4(`Starting applyDesignDocs for user: ${this._username}`);
2864
+ log4(`Remote DB name: ${this.remoteDB.name || "unknown"}`);
2718
2865
  if (this._username === "admin") {
2866
+ log4("Skipping design docs for admin user");
2719
2867
  return;
2720
2868
  }
2869
+ log4(`Applying ${_BaseUser.designDocs.length} design docs`);
2721
2870
  for (const doc of _BaseUser.designDocs) {
2871
+ log4(`Applying design doc: ${doc._id}`);
2722
2872
  try {
2723
2873
  try {
2724
2874
  const existingDoc = await this.remoteDB.get(doc._id);
@@ -2817,8 +2967,13 @@ Currently logged-in as ${this._username}.`
2817
2967
  async deduplicateReviews() {
2818
2968
  try {
2819
2969
  log4("Starting deduplication of scheduled reviews...");
2970
+ log4(`Remote DB name: ${this.remoteDB.name || "unknown"}`);
2971
+ log4(`Write DB name: ${this.writeDB.name || "unknown"}`);
2820
2972
  const reviewsMap = {};
2821
2973
  const duplicateDocIds = [];
2974
+ log4(
2975
+ `Attempting to query remoteDB for reviewCards/reviewCards. Database: ${this.remoteDB.name || "unknown"}`
2976
+ );
2822
2977
  const scheduledReviews = await this.remoteDB.query("reviewCards/reviewCards");
2823
2978
  log4(`Found ${scheduledReviews.rows.length} scheduled reviews to process`);
2824
2979
  scheduledReviews.rows.forEach((r) => {
@@ -2853,6 +3008,17 @@ Currently logged-in as ${this._username}.`
2853
3008
  }
2854
3009
  } catch (error) {
2855
3010
  log4(`Error during review deduplication: ${error}`);
3011
+ if (error && typeof error === "object" && "status" in error && error.status === 404) {
3012
+ log4(
3013
+ `Database not found (404) during review deduplication. Database: ${this.remoteDB.name || "unknown"}`
3014
+ );
3015
+ log4(
3016
+ `This might indicate the user database doesn't exist or the reviewCards view isn't available`
3017
+ );
3018
+ }
3019
+ if (error && typeof error === "object") {
3020
+ log4(`Full error details: ${JSON.stringify(error)}`);
3021
+ }
2856
3022
  }
2857
3023
  }
2858
3024
  /**
@@ -3086,6 +3252,16 @@ var init_PouchDataLayerProvider = __esm({
3086
3252
  getAdminDB() {
3087
3253
  return new AdminDB();
3088
3254
  }
3255
+ async createUserReaderForUser(targetUsername) {
3256
+ const requestingUsername = await getLoggedInUsername();
3257
+ if (requestingUsername !== "admin") {
3258
+ throw new Error("Unauthorized: Only admin users can access other users' data");
3259
+ }
3260
+ logger.info(`Admin user '${requestingUsername}' requesting UserDBReader for '${targetUsername}'`);
3261
+ const syncStrategy = new CouchDBSyncStrategy();
3262
+ const targetUserDB = await BaseUser.instance(syncStrategy, targetUsername);
3263
+ return targetUserDB;
3264
+ }
3089
3265
  isReadOnly() {
3090
3266
  return false;
3091
3267
  }
@@ -3538,7 +3714,10 @@ var init_courseDB2 = __esm({
3538
3714
  };
3539
3715
  }
3540
3716
  async getCardsByELO(elo, limit) {
3541
- return this.unpacker.queryByElo(elo, limit || 25);
3717
+ return (await this.unpacker.queryByElo(elo, limit || 25)).map((card) => {
3718
+ const [courseID, cardID, elo2] = card.split("-");
3719
+ return { courseID, cardID, elo: elo2 ? parseInt(elo2) : void 0 };
3720
+ });
3542
3721
  }
3543
3722
  async getCardEloData(cardIds) {
3544
3723
  const results = await Promise.all(
@@ -3582,14 +3761,19 @@ var init_courseDB2 = __esm({
3582
3761
  } else if (options.elo === "random") {
3583
3762
  targetElo = 800 + Math.random() * 400;
3584
3763
  }
3585
- let cardIds = await this.unpacker.queryByElo(targetElo, options.limit * 2);
3764
+ let cardIds = (await this.unpacker.queryByElo(targetElo, options.limit * 2)).map((c) => {
3765
+ return {
3766
+ cardID: c,
3767
+ courseID: this.courseId
3768
+ };
3769
+ });
3586
3770
  if (filter) {
3587
3771
  cardIds = cardIds.filter(filter);
3588
3772
  }
3589
- return cardIds.slice(0, options.limit).map((cardId) => ({
3773
+ return cardIds.slice(0, options.limit).map((card) => ({
3590
3774
  status: "new",
3591
- qualifiedID: `${this.courseId}-${cardId}`,
3592
- cardID: cardId,
3775
+ // qualifiedID: `${this.courseId}-${cardId}`,
3776
+ cardID: card.cardID,
3593
3777
  contentSourceType: "course",
3594
3778
  contentSourceID: this.courseId,
3595
3779
  courseID: this.courseId
@@ -3781,6 +3965,16 @@ var init_courseDB2 = __esm({
3781
3965
  async getAttachmentBlob(docId, attachmentName) {
3782
3966
  return this.unpacker.getAttachmentBlob(docId, attachmentName);
3783
3967
  }
3968
+ // Admin search methods
3969
+ async searchCards(_query) {
3970
+ return [];
3971
+ }
3972
+ async find(_request) {
3973
+ return {
3974
+ docs: [],
3975
+ warning: "Find operations not supported in static mode"
3976
+ };
3977
+ }
3784
3978
  };
3785
3979
  }
3786
3980
  });
@@ -3936,6 +4130,12 @@ var init_StaticDataLayerProvider = __esm({
3936
4130
  getAdminDB() {
3937
4131
  throw new Error("Admin functions not supported in static mode");
3938
4132
  }
4133
+ async createUserReaderForUser(targetUsername) {
4134
+ logger.warn(`StaticDataLayerProvider: Multi-user access not supported in static mode`);
4135
+ logger.warn(`Request: trying to access data for ${targetUsername}`);
4136
+ logger.warn(`Returning current user's data instead`);
4137
+ return this.getUserDB();
4138
+ }
3939
4139
  isReadOnly() {
3940
4140
  return true;
3941
4141
  }
@@ -5655,6 +5855,11 @@ init_dataDirectory();
5655
5855
  init_tuiLogger();
5656
5856
 
5657
5857
  // src/study/SessionController.ts
5858
+ import {
5859
+ displayableDataToViewData,
5860
+ isCourseElo,
5861
+ toCourseElo as toCourseElo3
5862
+ } from "@vue-skuilder/common";
5658
5863
  function randomInt(min, max) {
5659
5864
  return Math.floor(Math.random() * (max - min + 1)) + min;
5660
5865
  }
@@ -5665,15 +5870,15 @@ var ItemQueue = class {
5665
5870
  get dequeueCount() {
5666
5871
  return this._dequeueCount;
5667
5872
  }
5668
- add(item) {
5669
- if (this.seenCardIds.find((d) => d === item.cardID)) {
5873
+ add(item, cardId) {
5874
+ if (this.seenCardIds.find((d) => d === cardId)) {
5670
5875
  return;
5671
5876
  }
5672
- this.seenCardIds.push(item.cardID);
5877
+ this.seenCardIds.push(cardId);
5673
5878
  this.q.push(item);
5674
5879
  }
5675
- addAll(items) {
5676
- items.forEach((i) => this.add(i));
5880
+ addAll(items, cardIdExtractor) {
5881
+ items.forEach((i) => this.add(i, cardIdExtractor(i)));
5677
5882
  }
5678
5883
  get length() {
5679
5884
  return this.q.length;
@@ -5691,12 +5896,14 @@ var ItemQueue = class {
5691
5896
  }
5692
5897
  get toString() {
5693
5898
  return `${typeof this.q[0]}:
5694
- ` + this.q.map((i) => ` ${i.qualifiedID}: ${i.status}`).join("\n");
5899
+ ` + this.q.map((i) => ` ${i.courseID}+${i.cardID}: ${i.status}`).join("\n");
5695
5900
  }
5696
5901
  };
5697
5902
  var SessionController = class extends Loggable {
5698
5903
  _className = "SessionController";
5699
5904
  sources;
5905
+ dataLayer;
5906
+ getViewComponent;
5700
5907
  _sessionRecord = [];
5701
5908
  set sessionRecord(r) {
5702
5909
  this._sessionRecord = r;
@@ -5704,12 +5911,9 @@ var SessionController = class extends Loggable {
5704
5911
  reviewQ = new ItemQueue();
5705
5912
  newQ = new ItemQueue();
5706
5913
  failedQ = new ItemQueue();
5914
+ hydratedQ = new ItemQueue();
5707
5915
  _currentCard = null;
5708
- /**
5709
- * Indicates whether the session has been initialized - eg, the
5710
- * queues have been populated.
5711
- */
5712
- _isInitialized = false;
5916
+ hydration_in_progress = false;
5713
5917
  startTime;
5714
5918
  endTime;
5715
5919
  _secondsRemaining;
@@ -5727,12 +5931,14 @@ var SessionController = class extends Loggable {
5727
5931
  /**
5728
5932
  *
5729
5933
  */
5730
- constructor(sources, time) {
5934
+ constructor(sources, time, dataLayer, getViewComponent) {
5731
5935
  super();
5732
5936
  this.sources = sources;
5733
5937
  this.startTime = /* @__PURE__ */ new Date();
5734
5938
  this._secondsRemaining = time;
5735
5939
  this.endTime = new Date(this.startTime.valueOf() + 1e3 * this._secondsRemaining);
5940
+ this.dataLayer = dataLayer;
5941
+ this.getViewComponent = getViewComponent;
5736
5942
  this.log(`Session constructed:
5737
5943
  startTime: ${this.startTime}
5738
5944
  endTime: ${this.endTime}`);
@@ -5783,7 +5989,7 @@ var SessionController = class extends Loggable {
5783
5989
  } catch (e) {
5784
5990
  this.error("Error preparing study session:", e);
5785
5991
  }
5786
- this._isInitialized = true;
5992
+ await this._fillHydratedQueue();
5787
5993
  this._intervalHandle = setInterval(() => {
5788
5994
  this.tick();
5789
5995
  }, 1e3);
@@ -5821,12 +6027,8 @@ var SessionController = class extends Loggable {
5821
6027
  }
5822
6028
  }
5823
6029
  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
- }
6030
+ this.reviewQ.addAll(dueCards, (c) => c.cardID);
6031
+ report += dueCards.map((card) => `Card ${card.courseID}::${card.cardID} `).join("\n");
5830
6032
  this.log(report);
5831
6033
  }
5832
6034
  async getNewCards(n = 10) {
@@ -5841,36 +6043,32 @@ var SessionController = class extends Loggable {
5841
6043
  for (let i = 0; i < newContent.length; i++) {
5842
6044
  if (newContent[i].length > 0) {
5843
6045
  const item = newContent[i].splice(0, 1)[0];
5844
- this.log(`Adding new card: ${item.qualifiedID}`);
5845
- this.newQ.add(item);
6046
+ this.log(`Adding new card: ${item.courseID}::${item.cardID}`);
6047
+ this.newQ.add(item, item.cardID);
5846
6048
  n--;
5847
6049
  }
5848
6050
  }
5849
6051
  }
5850
6052
  }
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);
6053
+ _selectNextItemToHydrate(action = "dismiss-success") {
5860
6054
  const choice = Math.random();
5861
6055
  let newBound = 0.1;
5862
6056
  let reviewBound = 0.75;
5863
6057
  if (this.reviewQ.length === 0 && this.failedQ.length === 0 && this.newQ.length === 0) {
5864
- this._currentCard = null;
5865
- return this._currentCard;
6058
+ return null;
5866
6059
  }
5867
6060
  if (this._secondsRemaining < 2 && this.failedQ.length === 0) {
5868
- this._currentCard = null;
5869
- return this._currentCard;
6061
+ return null;
6062
+ }
6063
+ if (this._secondsRemaining <= 0) {
6064
+ if (this.failedQ.length > 0) {
6065
+ return this.failedQ.peek(0);
6066
+ } else {
6067
+ return null;
6068
+ }
5870
6069
  }
5871
6070
  if (this.newQ.dequeueCount < this.sources.length && this.newQ.length) {
5872
- this._currentCard = this.nextNewCard();
5873
- return this._currentCard;
6071
+ return this.newQ.peek(0);
5874
6072
  }
5875
6073
  const cleanupTime = this.estimateCleanupTime();
5876
6074
  const reviewTime = this.estimateReviewTime();
@@ -5895,16 +6093,27 @@ var SessionController = class extends Loggable {
5895
6093
  newBound = reviewBound;
5896
6094
  }
5897
6095
  if (choice < newBound && this.newQ.length) {
5898
- this._currentCard = this.nextNewCard();
6096
+ return this.newQ.peek(0);
5899
6097
  } else if (choice < reviewBound && this.reviewQ.length) {
5900
- this._currentCard = this.reviewQ.dequeue();
6098
+ return this.reviewQ.peek(0);
5901
6099
  } else if (this.failedQ.length) {
5902
- this._currentCard = this.failedQ.dequeue();
6100
+ return this.failedQ.peek(0);
5903
6101
  } else {
5904
6102
  this.log(`No more cards available for the session!`);
5905
- this._currentCard = null;
6103
+ return null;
5906
6104
  }
5907
- return this._currentCard;
6105
+ }
6106
+ async nextCard(action = "dismiss-success") {
6107
+ this.dismissCurrentCard(action);
6108
+ let card = this.hydratedQ.dequeue();
6109
+ if (!card && this.hasAvailableCards()) {
6110
+ void this._fillHydratedQueue();
6111
+ card = await this.nextHydratedCard();
6112
+ }
6113
+ if (this.hydratedQ.length < 3) {
6114
+ void this._fillHydratedQueue();
6115
+ }
6116
+ return card;
5908
6117
  }
5909
6118
  dismissCurrentCard(action = "dismiss-success") {
5910
6119
  if (this._currentCard) {
@@ -5915,7 +6124,6 @@ var SessionController = class extends Loggable {
5915
6124
  failedItem = {
5916
6125
  cardID: this._currentCard.cardID,
5917
6126
  courseID: this._currentCard.courseID,
5918
- qualifiedID: this._currentCard.qualifiedID,
5919
6127
  contentSourceID: this._currentCard.contentSourceID,
5920
6128
  contentSourceType: this._currentCard.contentSourceType,
5921
6129
  status: "failed-review",
@@ -5925,18 +6133,80 @@ var SessionController = class extends Loggable {
5925
6133
  failedItem = {
5926
6134
  cardID: this._currentCard.cardID,
5927
6135
  courseID: this._currentCard.courseID,
5928
- qualifiedID: this._currentCard.qualifiedID,
5929
6136
  contentSourceID: this._currentCard.contentSourceID,
5930
6137
  contentSourceType: this._currentCard.contentSourceType,
5931
6138
  status: "failed-new"
5932
6139
  };
5933
6140
  }
5934
- this.failedQ.add(failedItem);
6141
+ this.failedQ.add(failedItem, failedItem.cardID);
5935
6142
  } else if (action === "dismiss-error") {
5936
6143
  } else if (action === "dismiss-failed") {
5937
6144
  }
5938
6145
  }
5939
6146
  }
6147
+ hasAvailableCards() {
6148
+ return this.reviewQ.length > 0 || this.newQ.length > 0 || this.failedQ.length > 0;
6149
+ }
6150
+ async nextHydratedCard() {
6151
+ while (this.hydratedQ.length === 0 && this.hasAvailableCards()) {
6152
+ await new Promise((resolve) => setTimeout(resolve, 25));
6153
+ }
6154
+ return this.hydratedQ.dequeue();
6155
+ }
6156
+ async _fillHydratedQueue() {
6157
+ if (this.hydration_in_progress) {
6158
+ return;
6159
+ }
6160
+ const BUFFER_SIZE = 5;
6161
+ this.hydration_in_progress = true;
6162
+ while (this.hydratedQ.length < BUFFER_SIZE) {
6163
+ const nextItem = this._selectNextItemToHydrate();
6164
+ if (!nextItem) {
6165
+ return;
6166
+ }
6167
+ try {
6168
+ const cardData = await this.dataLayer.getCourseDB(nextItem.courseID).getCourseDoc(nextItem.cardID);
6169
+ if (!isCourseElo(cardData.elo)) {
6170
+ cardData.elo = toCourseElo3(cardData.elo);
6171
+ }
6172
+ const view = this.getViewComponent(cardData.id_view);
6173
+ const dataDocs = await Promise.all(
6174
+ cardData.id_displayable_data.map(
6175
+ (id) => this.dataLayer.getCourseDB(nextItem.courseID).getCourseDoc(id, {
6176
+ attachments: true,
6177
+ binary: true
6178
+ })
6179
+ )
6180
+ );
6181
+ const data = dataDocs.map(displayableDataToViewData).reverse();
6182
+ this.hydratedQ.add(
6183
+ {
6184
+ item: nextItem,
6185
+ view,
6186
+ data
6187
+ },
6188
+ nextItem.cardID
6189
+ );
6190
+ if (this.reviewQ.peek(0) === nextItem) {
6191
+ this.reviewQ.dequeue();
6192
+ } else if (this.newQ.peek(0) === nextItem) {
6193
+ this.newQ.dequeue();
6194
+ } else {
6195
+ this.failedQ.dequeue();
6196
+ }
6197
+ } catch (e) {
6198
+ this.error(`Error hydrating card ${nextItem.cardID}:`, e);
6199
+ if (this.reviewQ.peek(0) === nextItem) {
6200
+ this.reviewQ.dequeue();
6201
+ } else if (this.newQ.peek(0) === nextItem) {
6202
+ this.newQ.dequeue();
6203
+ } else {
6204
+ this.failedQ.dequeue();
6205
+ }
6206
+ }
6207
+ }
6208
+ this.hydration_in_progress = false;
6209
+ }
5940
6210
  };
5941
6211
 
5942
6212
  // src/study/SpacedRepetition.ts
@@ -5962,7 +6232,7 @@ function newQuestionInterval(user, cardHistory) {
5962
6232
  });
5963
6233
  }
5964
6234
  if (currentAttempt.isCorrect) {
5965
- const skill = currentAttempt.performance;
6235
+ const skill = Math.min(1, Math.max(0, currentAttempt.performance));
5966
6236
  logger.debug(`Demontrated skill: ${skill}`);
5967
6237
  const interval = lastInterval * (0.75 + skill);
5968
6238
  cardHistory.lapses = getLapses(cardHistory.records);