@vue-skuilder/db 0.1.7 → 0.1.8-0

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 (61) hide show
  1. package/dist/{SyncStrategy-DnJRj-Xp.d.mts → SyncStrategy-CyATpyLQ.d.mts} +6 -0
  2. package/dist/{SyncStrategy-DnJRj-Xp.d.ts → SyncStrategy-CyATpyLQ.d.ts} +6 -0
  3. package/dist/core/index.d.mts +5 -5
  4. package/dist/core/index.d.ts +5 -5
  5. package/dist/core/index.js +131 -118
  6. package/dist/core/index.js.map +1 -1
  7. package/dist/core/index.mjs +128 -115
  8. package/dist/core/index.mjs.map +1 -1
  9. package/dist/{dataLayerProvider-BbW9EnZK.d.mts → dataLayerProvider-BInqI_RF.d.mts} +1 -1
  10. package/dist/{dataLayerProvider-6stCgDME.d.ts → dataLayerProvider-DqtNroSh.d.ts} +1 -1
  11. package/dist/impl/couch/index.d.mts +6 -6
  12. package/dist/impl/couch/index.d.ts +6 -6
  13. package/dist/impl/couch/index.js +1365 -1252
  14. package/dist/impl/couch/index.js.map +1 -1
  15. package/dist/impl/couch/index.mjs +1359 -1246
  16. package/dist/impl/couch/index.mjs.map +1 -1
  17. package/dist/impl/static/index.d.mts +8 -6
  18. package/dist/impl/static/index.d.ts +8 -6
  19. package/dist/impl/static/index.js +253 -843
  20. package/dist/impl/static/index.js.map +1 -1
  21. package/dist/impl/static/index.mjs +250 -842
  22. package/dist/impl/static/index.mjs.map +1 -1
  23. package/dist/index-CLL31bEy.d.ts +137 -0
  24. package/dist/index-CUNnL38E.d.mts +137 -0
  25. package/dist/index.d.mts +10 -55
  26. package/dist/index.d.ts +10 -55
  27. package/dist/index.js +343 -170
  28. package/dist/index.js.map +1 -1
  29. package/dist/index.mjs +340 -167
  30. package/dist/index.mjs.map +1 -1
  31. package/dist/{types-BvzcRAys.d.ts → types-BefDGkKa.d.ts} +1 -1
  32. package/dist/{types-CQQ80R5N.d.mts → types-DC-ckZug.d.mts} +1 -1
  33. package/dist/{types-legacy-CtrmkOLu.d.mts → types-legacy-Birv-Jx6.d.mts} +2 -2
  34. package/dist/{types-legacy-CtrmkOLu.d.ts → types-legacy-Birv-Jx6.d.ts} +2 -2
  35. package/dist/{userDB-DUY63VMN.d.ts → userDB-C33Hzjgn.d.mts} +10 -3
  36. package/dist/{userDB-7fM4tpgr.d.mts → userDB-DusL7OXe.d.ts} +10 -3
  37. package/dist/util/packer/index.d.mts +3 -63
  38. package/dist/util/packer/index.d.ts +3 -63
  39. package/dist/util/packer/index.js +53 -1
  40. package/dist/util/packer/index.js.map +1 -1
  41. package/dist/util/packer/index.mjs +53 -1
  42. package/dist/util/packer/index.mjs.map +1 -1
  43. package/package.json +7 -4
  44. package/src/core/types/types-legacy.ts +13 -1
  45. package/src/core/types/user.ts +9 -2
  46. package/src/core/util/index.ts +5 -4
  47. package/src/impl/common/BaseUserDB.ts +33 -22
  48. package/src/impl/common/SyncStrategy.ts +7 -0
  49. package/src/impl/common/index.ts +0 -1
  50. package/src/impl/common/userDBHelpers.ts +4 -4
  51. package/src/impl/couch/CouchDBSyncStrategy.ts +10 -0
  52. package/src/impl/couch/courseAPI.ts +7 -6
  53. package/src/impl/couch/index.ts +10 -5
  54. package/src/impl/couch/updateQueue.ts +12 -8
  55. package/src/impl/couch/user-course-relDB.ts +17 -27
  56. package/src/impl/static/NoOpSyncStrategy.ts +5 -0
  57. package/src/impl/static/StaticDataUnpacker.ts +18 -36
  58. package/src/impl/static/courseDB.ts +135 -17
  59. package/src/util/migrator/FileSystemAdapter.ts +20 -0
  60. package/src/util/migrator/StaticToCouchDBMigrator.ts +6 -0
  61. package/src/util/packer/CouchDBToStaticPacker.ts +92 -2
@@ -102,32 +102,31 @@ var init_SyncStrategy = __esm({
102
102
  });
103
103
 
104
104
  // src/core/types/types-legacy.ts
105
- var GuestUsername, DocType, cardHistoryPrefix;
105
+ var GuestUsername, DocTypePrefixes;
106
106
  var init_types_legacy = __esm({
107
107
  "src/core/types/types-legacy.ts"() {
108
108
  "use strict";
109
109
  init_logger();
110
110
  GuestUsername = "Guest";
111
- DocType = /* @__PURE__ */ ((DocType2) => {
112
- DocType2["DISPLAYABLE_DATA"] = "DISPLAYABLE_DATA";
113
- DocType2["CARD"] = "CARD";
114
- DocType2["DATASHAPE"] = "DATASHAPE";
115
- DocType2["QUESTIONTYPE"] = "QUESTION";
116
- DocType2["VIEW"] = "VIEW";
117
- DocType2["PEDAGOGY"] = "PEDAGOGY";
118
- DocType2["CARDRECORD"] = "CARDRECORD";
119
- DocType2["SCHEDULED_CARD"] = "SCHEDULED_CARD";
120
- DocType2["TAG"] = "TAG";
121
- DocType2["NAVIGATION_STRATEGY"] = "NAVIGATION_STRATEGY";
122
- return DocType2;
123
- })(DocType || {});
124
- cardHistoryPrefix = "cardH";
111
+ DocTypePrefixes = {
112
+ ["CARD" /* CARD */]: "c",
113
+ ["DISPLAYABLE_DATA" /* DISPLAYABLE_DATA */]: "dd",
114
+ ["TAG" /* TAG */]: "TAG",
115
+ ["CARDRECORD" /* CARDRECORD */]: "cardH",
116
+ ["SCHEDULED_CARD" /* SCHEDULED_CARD */]: "card_review_",
117
+ // Add other doctypes here as they get prefixed IDs
118
+ ["DATASHAPE" /* DATASHAPE */]: "DATASHAPE",
119
+ ["QUESTION" /* QUESTIONTYPE */]: "QUESTION",
120
+ ["VIEW" /* VIEW */]: "VIEW",
121
+ ["PEDAGOGY" /* PEDAGOGY */]: "PEDAGOGY",
122
+ ["NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */]: "NAVIGATION_STRATEGY"
123
+ };
125
124
  }
126
125
  });
127
126
 
128
127
  // src/core/util/index.ts
129
128
  function getCardHistoryID(courseID, cardID) {
130
- return `${cardHistoryPrefix}-${courseID}-${cardID}`;
129
+ return `${DocTypePrefixes["CARDRECORD" /* CARDRECORD */]}-${courseID}-${cardID}`;
131
130
  }
132
131
  var init_util = __esm({
133
132
  "src/core/util/index.ts"() {
@@ -210,11 +209,11 @@ function scheduleCardReviewLocal(userDB, review) {
210
209
  const now = import_moment.default.utc();
211
210
  logger.info(`Scheduling for review in: ${review.time.diff(now, "h") / 24} days`);
212
211
  void userDB.put({
213
- _id: REVIEW_PREFIX + review.time.format(REVIEW_TIME_FORMAT),
212
+ _id: DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */] + review.time.format(REVIEW_TIME_FORMAT),
214
213
  cardId: review.card_id,
215
- reviewTime: review.time,
214
+ reviewTime: review.time.toISOString(),
216
215
  courseId: review.course_id,
217
- scheduledAt: now,
216
+ scheduledAt: now.toISOString(),
218
217
  scheduledFor: review.scheduledFor,
219
218
  schedulingAgentId: review.schedulingAgentId
220
219
  });
@@ -230,15 +229,15 @@ async function removeScheduledCardReviewLocal(userDB, reviewDocID) {
230
229
  ${JSON.stringify(err)}`);
231
230
  });
232
231
  }
233
- var import_moment, REVIEW_PREFIX, REVIEW_TIME_FORMAT, log;
232
+ var import_moment, REVIEW_TIME_FORMAT, log;
234
233
  var init_userDBHelpers = __esm({
235
234
  "src/impl/common/userDBHelpers.ts"() {
236
235
  "use strict";
237
236
  import_moment = __toESM(require("moment"));
237
+ init_core();
238
238
  init_logger();
239
239
  init_pouchdb_setup();
240
240
  init_dataDirectory();
241
- REVIEW_PREFIX = "card_review_";
242
241
  REVIEW_TIME_FORMAT = "YYYY-MM-DD--kk:mm:ss-SSS";
243
242
  log = (s) => {
244
243
  logger.info(s);
@@ -273,7 +272,10 @@ var init_updateQueue = __esm({
273
272
  _className = "UpdateQueue";
274
273
  pendingUpdates = {};
275
274
  inprogressUpdates = {};
276
- db;
275
+ readDB;
276
+ // Database for read operations
277
+ writeDB;
278
+ // Database for write operations (local-first)
277
279
  update(id, update) {
278
280
  logger.debug(`Update requested on doc: ${id}`);
279
281
  if (this.pendingUpdates[id]) {
@@ -283,24 +285,25 @@ var init_updateQueue = __esm({
283
285
  }
284
286
  return this.applyUpdates(id);
285
287
  }
286
- constructor(db) {
288
+ constructor(readDB, writeDB) {
287
289
  super();
288
- this.db = db;
290
+ this.readDB = readDB;
291
+ this.writeDB = writeDB || readDB;
289
292
  logger.debug(`UpdateQ initialized...`);
290
- void this.db.info().then((i) => {
293
+ void this.readDB.info().then((i) => {
291
294
  logger.debug(`db info: ${JSON.stringify(i)}`);
292
295
  });
293
296
  }
294
297
  async applyUpdates(id) {
295
298
  logger.debug(`Applying updates on doc: ${id}`);
296
299
  if (this.inprogressUpdates[id]) {
297
- await this.db.info();
300
+ await this.readDB.info();
298
301
  return this.applyUpdates(id);
299
302
  } else {
300
303
  if (this.pendingUpdates[id] && this.pendingUpdates[id].length > 0) {
301
304
  this.inprogressUpdates[id] = true;
302
305
  try {
303
- let doc = await this.db.get(id);
306
+ let doc = await this.readDB.get(id);
304
307
  logger.debug(`Retrieved doc: ${id}`);
305
308
  while (this.pendingUpdates[id].length !== 0) {
306
309
  const update = this.pendingUpdates[id].splice(0, 1)[0];
@@ -313,7 +316,7 @@ var init_updateQueue = __esm({
313
316
  };
314
317
  }
315
318
  }
316
- await this.db.put(doc);
319
+ await this.writeDB.put(doc);
317
320
  logger.debug(`Put doc: ${id}`);
318
321
  if (this.pendingUpdates[id].length === 0) {
319
322
  this.inprogressUpdates[id] = false;
@@ -339,114 +342,68 @@ var init_updateQueue = __esm({
339
342
  }
340
343
  });
341
344
 
342
- // src/impl/couch/clientCache.ts
343
- async function GET_CACHED(k, f) {
344
- if (CLIENT_CACHE[k]) {
345
- return CLIENT_CACHE[k];
345
+ // src/impl/couch/user-course-relDB.ts
346
+ var import_moment2, UsrCrsData;
347
+ var init_user_course_relDB = __esm({
348
+ "src/impl/couch/user-course-relDB.ts"() {
349
+ "use strict";
350
+ import_moment2 = __toESM(require("moment"));
351
+ init_logger();
352
+ UsrCrsData = class {
353
+ user;
354
+ _courseId;
355
+ constructor(user, courseId) {
356
+ this.user = user;
357
+ this._courseId = courseId;
358
+ }
359
+ async getReviewsForcast(daysCount) {
360
+ const time = import_moment2.default.utc().add(daysCount, "days");
361
+ return this.getReviewstoDate(time);
362
+ }
363
+ async getPendingReviews() {
364
+ const now = import_moment2.default.utc();
365
+ return this.getReviewstoDate(now);
366
+ }
367
+ async getScheduledReviewCount() {
368
+ return (await this.getPendingReviews()).length;
369
+ }
370
+ async getCourseSettings() {
371
+ const regDoc = await this.user.getCourseRegistrationsDoc();
372
+ const crsDoc = regDoc.courses.find((c) => c.courseID === this._courseId);
373
+ if (crsDoc && crsDoc.settings) {
374
+ return crsDoc.settings;
375
+ } else {
376
+ logger.warn(`no settings found during lookup on course ${this._courseId}`);
377
+ return {};
378
+ }
379
+ }
380
+ updateCourseSettings(updates) {
381
+ if ("updateCourseSettings" in this.user) {
382
+ void this.user.updateCourseSettings(this._courseId, updates);
383
+ }
384
+ }
385
+ async getReviewstoDate(targetDate) {
386
+ const allReviews = await this.user.getPendingReviews(this._courseId);
387
+ logger.debug(
388
+ `Fetching ${this.user.getUsername()}'s scheduled reviews for course ${this._courseId}.`
389
+ );
390
+ return allReviews.filter((review) => {
391
+ const reviewTime = import_moment2.default.utc(review.reviewTime);
392
+ return targetDate.isAfter(reviewTime);
393
+ });
394
+ }
395
+ };
346
396
  }
347
- CLIENT_CACHE[k] = f ? await f(k) : await GET_ITEM(k);
348
- return GET_CACHED(k);
349
- }
350
- async function GET_ITEM(k) {
351
- throw new Error(`No implementation found for GET_CACHED(${k})`);
352
- }
353
- var CLIENT_CACHE;
397
+ });
398
+
399
+ // src/impl/couch/clientCache.ts
354
400
  var init_clientCache = __esm({
355
401
  "src/impl/couch/clientCache.ts"() {
356
402
  "use strict";
357
- CLIENT_CACHE = {};
358
403
  }
359
404
  });
360
405
 
361
406
  // src/impl/couch/courseAPI.ts
362
- async function addNote55(courseID, codeCourse, shape, data, author, tags, uploads, elo = (0, import_common2.blankCourseElo)()) {
363
- const db = getCourseDB(courseID);
364
- const payload = (0, import_common3.prepareNote55)(courseID, codeCourse, shape, data, author, tags, uploads);
365
- const result = await db.post(payload);
366
- const dataShapeId = import_common.NameSpacer.getDataShapeString({
367
- course: codeCourse,
368
- dataShape: shape.name
369
- });
370
- if (result.ok) {
371
- try {
372
- await createCards(courseID, dataShapeId, result.id, tags, elo, author);
373
- } catch (error) {
374
- let errorMessage = "Unknown error";
375
- if (error instanceof Error) {
376
- errorMessage = error.message;
377
- } else if (error && typeof error === "object" && "reason" in error) {
378
- errorMessage = error.reason;
379
- } else if (error && typeof error === "object" && "message" in error) {
380
- errorMessage = error.message;
381
- } else {
382
- errorMessage = String(error);
383
- }
384
- logger.error(`[addNote55] Failed to create cards for note ${result.id}: ${errorMessage}`);
385
- result.cardCreationFailed = true;
386
- result.cardCreationError = errorMessage;
387
- }
388
- } else {
389
- logger.error(`[addNote55] Error adding note. Result: ${JSON.stringify(result)}`);
390
- }
391
- return result;
392
- }
393
- async function createCards(courseID, datashapeID, noteID, tags, elo = (0, import_common2.blankCourseElo)(), author) {
394
- const cfg = await getCredentialledCourseConfig(courseID);
395
- const dsDescriptor = import_common.NameSpacer.getDataShapeDescriptor(datashapeID);
396
- let questionViewTypes = [];
397
- for (const ds of cfg.dataShapes) {
398
- if (ds.name === datashapeID) {
399
- questionViewTypes = ds.questionTypes;
400
- }
401
- }
402
- if (questionViewTypes.length === 0) {
403
- const errorMsg = `No questionViewTypes found for datashapeID: ${datashapeID} in course config. Cards cannot be created.`;
404
- logger.error(errorMsg);
405
- throw new Error(errorMsg);
406
- }
407
- for (const questionView of questionViewTypes) {
408
- await createCard(questionView, courseID, dsDescriptor, noteID, tags, elo, author);
409
- }
410
- }
411
- async function createCard(questionViewName, courseID, dsDescriptor, noteID, tags, elo = (0, import_common2.blankCourseElo)(), author) {
412
- const qDescriptor = import_common.NameSpacer.getQuestionDescriptor(questionViewName);
413
- const cfg = await getCredentialledCourseConfig(courseID);
414
- for (const rQ of cfg.questionTypes) {
415
- if (rQ.name === questionViewName) {
416
- for (const view of rQ.viewList) {
417
- await addCard(
418
- courseID,
419
- dsDescriptor.course,
420
- [noteID],
421
- import_common.NameSpacer.getViewString({
422
- course: qDescriptor.course,
423
- questionType: qDescriptor.questionType,
424
- view
425
- }),
426
- elo,
427
- tags,
428
- author
429
- );
430
- }
431
- }
432
- }
433
- }
434
- async function addCard(courseID, course, id_displayable_data, id_view, elo, tags, author) {
435
- const db = getCourseDB(courseID);
436
- const card = await db.post({
437
- course,
438
- id_displayable_data,
439
- id_view,
440
- docType: "CARD" /* CARD */,
441
- elo: elo || (0, import_common2.toCourseElo)(990 + Math.round(20 * Math.random())),
442
- author
443
- });
444
- for (const tag of tags) {
445
- logger.info(`adding tag: ${tag} to card ${card.id}`);
446
- await addTagToCard(courseID, card.id, tag, author, false);
447
- }
448
- return card;
449
- }
450
407
  async function getCredentialledCourseConfig(courseID) {
451
408
  try {
452
409
  const db = getCourseDB(courseID);
@@ -459,66 +416,6 @@ async function getCredentialledCourseConfig(courseID) {
459
416
  throw e;
460
417
  }
461
418
  }
462
- async function addTagToCard(courseID, cardID, tagID, author, updateELO = true) {
463
- const prefixedTagID = getTagID(tagID);
464
- const courseDB = getCourseDB(courseID);
465
- const courseApi = new CourseDB(courseID, async () => {
466
- const dummySyncStrategy = {
467
- setupRemoteDB: () => null,
468
- startSync: () => {
469
- },
470
- canCreateAccount: () => false,
471
- canAuthenticate: () => false,
472
- getCurrentUsername: async () => "DummyUser"
473
- };
474
- return BaseUser.Dummy(dummySyncStrategy);
475
- });
476
- try {
477
- logger.info(`Applying tag ${tagID} to card ${courseID + "-" + cardID}...`);
478
- const tag = await courseDB.get(prefixedTagID);
479
- if (!tag.taggedCards.includes(cardID)) {
480
- tag.taggedCards.push(cardID);
481
- if (updateELO) {
482
- try {
483
- const eloData = await courseApi.getCardEloData([cardID]);
484
- const elo = eloData[0];
485
- elo.tags[tagID] = {
486
- count: 0,
487
- score: elo.global.score
488
- // todo: or 1000?
489
- };
490
- await updateCardElo(courseID, cardID, elo);
491
- } catch (error) {
492
- logger.error("Failed to update ELO data for card:", cardID, error);
493
- }
494
- }
495
- return courseDB.put(tag);
496
- } else throw new AlreadyTaggedErr(`Card ${cardID} is already tagged with ${tagID}`);
497
- } catch (e) {
498
- if (e instanceof AlreadyTaggedErr) {
499
- throw e;
500
- }
501
- await createTag(courseID, tagID, author);
502
- return addTagToCard(courseID, cardID, tagID, author, updateELO);
503
- }
504
- }
505
- async function updateCardElo(courseID, cardID, elo) {
506
- if (elo) {
507
- const cDB = getCourseDB(courseID);
508
- const card = await cDB.get(cardID);
509
- logger.debug(`Replacing ${JSON.stringify(card.elo)} with ${JSON.stringify(elo)}`);
510
- card.elo = elo;
511
- return cDB.put(card);
512
- }
513
- }
514
- function getTagID(tagName) {
515
- const tagPrefix = "TAG" /* TAG */.valueOf() + "-";
516
- if (tagName.indexOf(tagPrefix) === 0) {
517
- return tagName;
518
- } else {
519
- return tagPrefix + tagName;
520
- }
521
- }
522
419
  function getCourseDB(courseID) {
523
420
  const dbName = `coursedb-${courseID}`;
524
421
  return new pouchdb_setup_default(
@@ -526,7 +423,7 @@ function getCourseDB(courseID) {
526
423
  pouchDBincludeCredentialsConfig
527
424
  );
528
425
  }
529
- var import_common, import_common2, import_common3, AlreadyTaggedErr;
426
+ var import_common, import_common2, import_common3, import_uuid;
530
427
  var init_courseAPI = __esm({
531
428
  "src/impl/couch/courseAPI.ts"() {
532
429
  "use strict";
@@ -540,12 +437,7 @@ var init_courseAPI = __esm({
540
437
  import_common3 = require("@vue-skuilder/common");
541
438
  init_common();
542
439
  init_logger();
543
- AlreadyTaggedErr = class extends Error {
544
- constructor(message) {
545
- super(message);
546
- this.name = "AlreadyTaggedErr";
547
- }
548
- };
440
+ import_uuid = require("uuid");
549
441
  }
550
442
  });
551
443
 
@@ -680,82 +572,7 @@ var init_navigators = __esm({
680
572
  });
681
573
 
682
574
  // src/impl/couch/courseDB.ts
683
- function randIntWeightedTowardZero(n) {
684
- return Math.floor(Math.random() * Math.random() * Math.random() * n);
685
- }
686
- async function getCourseTagStubs(courseID) {
687
- logger.debug(`Getting tag stubs for course: ${courseID}`);
688
- const stubs = await filterAllDocsByPrefix2(
689
- getCourseDB2(courseID),
690
- "TAG" /* TAG */.valueOf() + "-"
691
- );
692
- stubs.rows.forEach((row) => {
693
- logger.debug(` Tag stub for doc: ${row.id}`);
694
- });
695
- return stubs;
696
- }
697
- async function createTag(courseID, tagName, author) {
698
- logger.debug(`Creating tag: ${tagName}...`);
699
- const tagID = getTagID(tagName);
700
- const courseDB = getCourseDB2(courseID);
701
- const resp = await courseDB.put({
702
- course: courseID,
703
- docType: "TAG" /* TAG */,
704
- name: tagName,
705
- snippet: "",
706
- taggedCards: [],
707
- wiki: "",
708
- author,
709
- _id: tagID
710
- });
711
- return resp;
712
- }
713
- async function updateTag(tag) {
714
- const prior = await getTag(tag.course, tag.name);
715
- return await getCourseDB2(tag.course).put({
716
- ...tag,
717
- _rev: prior._rev
718
- });
719
- }
720
- async function getTag(courseID, tagName) {
721
- const tagID = getTagID(tagName);
722
- const courseDB = getCourseDB2(courseID);
723
- return courseDB.get(tagID);
724
- }
725
- async function removeTagFromCard(courseID, cardID, tagID) {
726
- tagID = getTagID(tagID);
727
- const courseDB = getCourseDB2(courseID);
728
- const tag = await courseDB.get(tagID);
729
- tag.taggedCards = tag.taggedCards.filter((taggedID) => {
730
- return cardID !== taggedID;
731
- });
732
- return courseDB.put(tag);
733
- }
734
- async function getAppliedTags(id_course, id_card) {
735
- const db = getCourseDB2(id_course);
736
- const result = await db.query("getTags", {
737
- startkey: id_card,
738
- endkey: id_card
739
- // include_docs: true
740
- });
741
- return result;
742
- }
743
- async function updateCredentialledCourseConfig(courseID, config) {
744
- logger.debug(`Updating course config:
745
-
746
- ${JSON.stringify(config)}
747
- `);
748
- const db = getCourseDB2(courseID);
749
- const old = await getCredentialledCourseConfig(courseID);
750
- return await db.put({
751
- ...config,
752
- _rev: old._rev
753
- });
754
- }
755
- function isSuccessRow(row) {
756
- return "doc" in row && row.doc !== null && row.doc !== void 0;
757
- }
758
- var import_common5, CourseDB;
575
+ var import_common5;
759
576
  var init_courseDB = __esm({
760
577
  "src/impl/couch/courseDB.ts"() {
761
578
  "use strict";
@@ -768,428 +585,17 @@ var init_courseDB = __esm({
768
585
  init_courseAPI();
769
586
  init_courseLookupDB();
770
587
  init_navigators();
771
- CourseDB = class {
772
- // private log(msg: string): void {
773
- // log(`CourseLog: ${this.id}\n ${msg}`);
774
- // }
775
- db;
776
- id;
777
- _getCurrentUser;
778
- updateQueue;
779
- constructor(id, userLookup) {
780
- this.id = id;
781
- this.db = getCourseDB2(this.id);
782
- this._getCurrentUser = userLookup;
783
- this.updateQueue = new UpdateQueue(this.db);
784
- }
785
- getCourseID() {
786
- return this.id;
787
- }
788
- async getCourseInfo() {
789
- const cardCount = (await this.db.find({
790
- selector: {
791
- docType: "CARD" /* CARD */
792
- },
793
- limit: 1e3
794
- })).docs.length;
795
- return {
796
- cardCount,
797
- registeredUsers: 0
798
- };
799
- }
800
- async getInexperiencedCards(limit = 2) {
801
- return (await this.db.query("cardsByInexperience", {
802
- limit
803
- })).rows.map((r) => {
804
- const ret = {
805
- courseId: this.id,
806
- cardId: r.id,
807
- count: r.key,
808
- elo: r.value
809
- };
810
- return ret;
811
- });
812
- }
813
- async getCardsByEloLimits(options = {
814
- low: 0,
815
- high: Number.MIN_SAFE_INTEGER,
816
- limit: 25,
817
- page: 0
818
- }) {
819
- return (await this.db.query("elo", {
820
- startkey: options.low,
821
- endkey: options.high,
822
- limit: options.limit,
823
- skip: options.limit * options.page
824
- })).rows.map((r) => {
825
- return `${this.id}-${r.id}-${r.key}`;
826
- });
827
- }
828
- async getCardEloData(id) {
829
- const docs = await this.db.allDocs({
830
- keys: id,
831
- include_docs: true
832
- });
833
- const ret = [];
834
- docs.rows.forEach((r) => {
835
- if (isSuccessRow(r)) {
836
- if (r.doc && r.doc.elo) {
837
- ret.push((0, import_common5.toCourseElo)(r.doc.elo));
838
- } else {
839
- logger.warn("no elo data for card: " + r.id);
840
- ret.push((0, import_common5.blankCourseElo)());
841
- }
842
- } else {
843
- logger.warn("no elo data for card: " + JSON.stringify(r));
844
- ret.push((0, import_common5.blankCourseElo)());
845
- }
846
- });
847
- return ret;
848
- }
849
- /**
850
- * Returns the lowest and highest `global` ELO ratings in the course
851
- */
852
- async getELOBounds() {
853
- const [low, high] = await Promise.all([
854
- (await this.db.query("elo", {
855
- startkey: 0,
856
- limit: 1,
857
- include_docs: false
858
- })).rows[0].key,
859
- (await this.db.query("elo", {
860
- limit: 1,
861
- descending: true,
862
- startkey: 1e5
863
- })).rows[0].key
864
- ]);
865
- return {
866
- low,
867
- high
868
- };
869
- }
870
- async removeCard(id) {
871
- const doc = await this.db.get(id);
872
- if (!doc.docType || !(doc.docType === "CARD" /* CARD */)) {
873
- throw new Error(`failed to remove ${id} from course ${this.id}. id does not point to a card`);
874
- }
875
- return this.db.remove(doc);
876
- }
877
- async getCardDisplayableDataIDs(id) {
878
- logger.debug(id.join(", "));
879
- const cards = await this.db.allDocs({
880
- keys: id,
881
- include_docs: true
882
- });
883
- const ret = {};
884
- cards.rows.forEach((r) => {
885
- if (isSuccessRow(r)) {
886
- ret[r.id] = r.doc.id_displayable_data;
887
- }
888
- });
889
- await Promise.all(
890
- cards.rows.map((r) => {
891
- return async () => {
892
- if (isSuccessRow(r)) {
893
- ret[r.id] = r.doc.id_displayable_data;
894
- }
895
- };
896
- })
897
- );
898
- return ret;
899
- }
900
- async getCardsByELO(elo, cardLimit) {
901
- elo = parseInt(elo);
902
- const limit = cardLimit ? cardLimit : 25;
903
- const below = await this.db.query("elo", {
904
- limit: Math.ceil(limit / 2),
905
- startkey: elo,
906
- descending: true
907
- });
908
- const aboveLimit = limit - below.rows.length;
909
- const above = await this.db.query("elo", {
910
- limit: aboveLimit,
911
- startkey: elo + 1
912
- });
913
- let cards = below.rows;
914
- cards = cards.concat(above.rows);
915
- const ret = cards.sort((a, b) => {
916
- const s = Math.abs(a.key - elo) - Math.abs(b.key - elo);
917
- if (s === 0) {
918
- return Math.random() - 0.5;
919
- } else {
920
- return s;
921
- }
922
- }).map((c) => `${this.id}-${c.id}-${c.key}`);
923
- const str = `below:
924
- ${below.rows.map((r) => ` ${r.id}-${r.key}
925
- `)}
926
-
927
- above:
928
- ${above.rows.map((r) => ` ${r.id}-${r.key}
929
- `)}`;
930
- logger.debug(`Getting ${limit} cards centered around elo: ${elo}:
931
-
932
- ` + str);
933
- return ret;
934
- }
935
- async getCourseConfig() {
936
- const ret = await getCredentialledCourseConfig(this.id);
937
- if (ret) {
938
- return ret;
939
- } else {
940
- throw new Error(`Course config not found for course ID: ${this.id}`);
941
- }
942
- }
943
- async updateCourseConfig(cfg) {
944
- logger.debug(`Updating: ${JSON.stringify(cfg)}`);
945
- try {
946
- return await updateCredentialledCourseConfig(this.id, cfg);
947
- } catch (error) {
948
- logger.error(`Error updating course config in course DB: ${error}`);
949
- throw error;
950
- }
951
- }
952
- async updateCardElo(cardId, elo) {
953
- if (!elo) {
954
- throw new Error(`Cannot update card elo with null or undefined value for card ID: ${cardId}`);
955
- }
956
- try {
957
- const result = await this.updateQueue.update(cardId, (card) => {
958
- logger.debug(`Replacing ${JSON.stringify(card.elo)} with ${JSON.stringify(elo)}`);
959
- card.elo = elo;
960
- return card;
961
- });
962
- return { ok: true, id: cardId, rev: result._rev };
963
- } catch (error) {
964
- logger.error(`Failed to update card elo for card ID: ${cardId}`, error);
965
- throw new Error(`Failed to update card elo for card ID: ${cardId}`);
966
- }
967
- }
968
- async getAppliedTags(cardId) {
969
- const ret = await getAppliedTags(this.id, cardId);
970
- if (ret) {
971
- return ret;
972
- } else {
973
- throw new Error(`Failed to find tags for card ${this.id}-${cardId}`);
974
- }
975
- }
976
- async addTagToCard(cardId, tagId, updateELO) {
977
- return await addTagToCard(this.id, cardId, tagId, (await this._getCurrentUser()).getUsername(), updateELO);
978
- }
979
- async removeTagFromCard(cardId, tagId) {
980
- return await removeTagFromCard(this.id, cardId, tagId);
981
- }
982
- async createTag(name, author) {
983
- return await createTag(this.id, name, author);
984
- }
985
- async getTag(tagId) {
986
- return await getTag(this.id, tagId);
987
- }
988
- async updateTag(tag) {
989
- if (tag.course !== this.id) {
990
- throw new Error(`Tag ${JSON.stringify(tag)} does not belong to course ${this.id}`);
991
- }
992
- return await updateTag(tag);
993
- }
994
- async getCourseTagStubs() {
995
- return getCourseTagStubs(this.id);
996
- }
997
- async addNote(codeCourse, shape, data, author, tags, uploads, elo = (0, import_common5.blankCourseElo)()) {
998
- try {
999
- const resp = await addNote55(this.id, codeCourse, shape, data, author, tags, uploads, elo);
1000
- if (resp.ok) {
1001
- if (resp.cardCreationFailed) {
1002
- logger.warn(
1003
- `[courseDB.addNote] Note added but card creation failed: ${resp.cardCreationError}`
1004
- );
1005
- return {
1006
- status: import_common5.Status.error,
1007
- message: `Note was added but no cards were created: ${resp.cardCreationError}`,
1008
- id: resp.id
1009
- };
1010
- }
1011
- return {
1012
- status: import_common5.Status.ok,
1013
- message: "",
1014
- id: resp.id
1015
- };
1016
- } else {
1017
- return {
1018
- status: import_common5.Status.error,
1019
- message: "Unexpected error adding note"
1020
- };
1021
- }
1022
- } catch (e) {
1023
- const err = e;
1024
- logger.error(
1025
- `[addNote] error ${err.name}
1026
- reason: ${err.reason}
1027
- message: ${err.message}`
1028
- );
1029
- return {
1030
- status: import_common5.Status.error,
1031
- message: `Error adding note to course. ${e.reason || err.message}`
1032
- };
1033
- }
1034
- }
1035
- async getCourseDoc(id, options) {
1036
- return await getCourseDoc(this.id, id, options);
1037
- }
1038
- async getCourseDocs(ids, options = {}) {
1039
- return await getCourseDocs(this.id, ids, options);
1040
- }
1041
- ////////////////////////////////////
1042
- // NavigationStrategyManager implementation
1043
- ////////////////////////////////////
1044
- getNavigationStrategy(id) {
1045
- logger.debug(`[courseDB] Getting navigation strategy: ${id}`);
1046
- const strategy = {
1047
- id: "ELO",
1048
- docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
1049
- name: "ELO",
1050
- description: "ELO-based navigation strategy for ordering content by difficulty",
1051
- implementingClass: "elo" /* ELO */,
1052
- course: this.id,
1053
- serializedData: ""
1054
- // serde is a noop for ELO navigator.
1055
- };
1056
- return Promise.resolve(strategy);
1057
- }
1058
- getAllNavigationStrategies() {
1059
- logger.debug("[courseDB] Returning hard-coded navigation strategies");
1060
- const strategies = [
1061
- {
1062
- id: "ELO",
1063
- docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
1064
- name: "ELO",
1065
- description: "ELO-based navigation strategy for ordering content by difficulty",
1066
- implementingClass: "elo" /* ELO */,
1067
- course: this.id,
1068
- serializedData: ""
1069
- // serde is a noop for ELO navigator.
1070
- }
1071
- ];
1072
- return Promise.resolve(strategies);
1073
- }
1074
- addNavigationStrategy(data) {
1075
- logger.debug(`[courseDB] Adding navigation strategy: ${data.id}`);
1076
- logger.debug(JSON.stringify(data));
1077
- return Promise.resolve();
1078
- }
1079
- updateNavigationStrategy(id, data) {
1080
- logger.debug(`[courseDB] Updating navigation strategy: ${id}`);
1081
- logger.debug(JSON.stringify(data));
1082
- return Promise.resolve();
1083
- }
1084
- async surfaceNavigationStrategy() {
1085
- logger.warn(`Returning hard-coded default ELO navigator`);
1086
- const ret = {
1087
- id: "ELO",
1088
- docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
1089
- name: "ELO",
1090
- description: "ELO-based navigation strategy",
1091
- implementingClass: "elo" /* ELO */,
1092
- course: this.id,
1093
- serializedData: ""
1094
- // serde is a noop for ELO navigator.
1095
- };
1096
- return Promise.resolve(ret);
1097
- }
1098
- ////////////////////////////////////
1099
- // END NavigationStrategyManager implementation
1100
- ////////////////////////////////////
1101
- ////////////////////////////////////
1102
- // StudyContentSource implementation
1103
- ////////////////////////////////////
1104
- async getNewCards(limit = 99) {
1105
- const u = await this._getCurrentUser();
1106
- try {
1107
- const strategy = await this.surfaceNavigationStrategy();
1108
- const navigator = await ContentNavigator.create(u, this, strategy);
1109
- return navigator.getNewCards(limit);
1110
- } catch (e) {
1111
- logger.error(`[courseDB] Error surfacing a NavigationStrategy: ${e}`);
1112
- throw e;
1113
- }
1114
- }
1115
- async getPendingReviews() {
1116
- const u = await this._getCurrentUser();
1117
- try {
1118
- const strategy = await this.surfaceNavigationStrategy();
1119
- const navigator = await ContentNavigator.create(u, this, strategy);
1120
- return navigator.getPendingReviews();
1121
- } catch (e) {
1122
- logger.error(`[courseDB] Error surfacing a NavigationStrategy: ${e}`);
1123
- throw e;
1124
- }
1125
- }
1126
- async getCardsCenteredAtELO(options = {
1127
- limit: 99,
1128
- elo: "user"
1129
- }, filter) {
1130
- let targetElo;
1131
- if (options.elo === "user") {
1132
- const u = await this._getCurrentUser();
1133
- targetElo = -1;
1134
- try {
1135
- const courseDoc = (await u.getCourseRegistrationsDoc()).courses.find((c) => {
1136
- return c.courseID === this.id;
1137
- });
1138
- targetElo = (0, import_common5.EloToNumber)(courseDoc.elo);
1139
- } catch {
1140
- targetElo = 1e3;
1141
- }
1142
- } else if (options.elo === "random") {
1143
- const bounds = await GET_CACHED(`elo-bounds-${this.id}`, () => this.getELOBounds());
1144
- targetElo = Math.round(bounds.low + Math.random() * (bounds.high - bounds.low));
1145
- } else {
1146
- targetElo = options.elo;
1147
- }
1148
- let cards = [];
1149
- let mult = 4;
1150
- let previousCount = -1;
1151
- let newCount = 0;
1152
- while (cards.length < options.limit && newCount !== previousCount) {
1153
- cards = await this.getCardsByELO(targetElo, mult * options.limit);
1154
- previousCount = newCount;
1155
- newCount = cards.length;
1156
- logger.debug(`Found ${cards.length} elo neighbor cards...`);
1157
- if (filter) {
1158
- cards = cards.filter(filter);
1159
- logger.debug(`Filtered to ${cards.length} cards...`);
1160
- }
1161
- mult *= 2;
1162
- }
1163
- const selectedCards = [];
1164
- while (selectedCards.length < options.limit && cards.length > 0) {
1165
- const index = randIntWeightedTowardZero(cards.length);
1166
- const card = cards.splice(index, 1)[0];
1167
- selectedCards.push(card);
1168
- }
1169
- return selectedCards.map((c) => {
1170
- const split = c.split("-");
1171
- return {
1172
- courseID: this.id,
1173
- cardID: split[1],
1174
- qualifiedID: `${split[0]}-${split[1]}`,
1175
- contentSourceType: "course",
1176
- contentSourceID: this.id,
1177
- status: "new"
1178
- };
1179
- });
1180
- }
1181
- };
1182
588
  }
1183
589
  });
1184
590
 
1185
591
  // src/impl/couch/classroomDB.ts
1186
- var import_moment2;
592
+ var import_moment3;
1187
593
  var init_classroomDB2 = __esm({
1188
594
  "src/impl/couch/classroomDB.ts"() {
1189
595
  "use strict";
1190
596
  init_factory();
1191
597
  init_logger();
1192
- import_moment2 = __toESM(require("moment"));
598
+ import_moment3 = __toESM(require("moment"));
1193
599
  init_pouchdb_setup();
1194
600
  init_couch();
1195
601
  init_courseDB();
@@ -1236,45 +642,13 @@ var init_CouchDBSyncStrategy = __esm({
1236
642
  });
1237
643
 
1238
644
  // src/impl/couch/index.ts
1239
- function getCourseDB2(courseID) {
1240
- return new pouchdb_setup_default(
1241
- ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + "coursedb-" + courseID,
1242
- pouchDBincludeCredentialsConfig
1243
- );
1244
- }
1245
- function getCourseDocs(courseID, docIDs, options = {}) {
1246
- return getCourseDB2(courseID).allDocs({
1247
- ...options,
1248
- keys: docIDs
1249
- });
1250
- }
1251
- function getCourseDoc(courseID, docID, options = {}) {
1252
- return getCourseDB2(courseID).get(docID, options);
1253
- }
1254
- function filterAllDocsByPrefix2(db, prefix, opts) {
1255
- const options = {
1256
- startkey: prefix,
1257
- endkey: prefix + "\uFFF0",
1258
- include_docs: true
1259
- };
1260
- if (opts) {
1261
- Object.assign(options, opts);
1262
- }
1263
- return db.allDocs(options);
1264
- }
1265
- function getStartAndEndKeys2(key) {
1266
- return {
1267
- startkey: key,
1268
- endkey: key + "\uFFF0"
1269
- };
1270
- }
1271
- var import_moment3, import_process, isBrowser, GUEST_LOCAL_DB, localUserDB, pouchDBincludeCredentialsConfig, REVIEW_PREFIX2, REVIEW_TIME_FORMAT2;
645
+ var import_moment4, import_process, isBrowser, GUEST_LOCAL_DB, localUserDB, pouchDBincludeCredentialsConfig;
1272
646
  var init_couch = __esm({
1273
647
  "src/impl/couch/index.ts"() {
1274
648
  "use strict";
1275
649
  init_factory();
1276
650
  init_types_legacy();
1277
- import_moment3 = __toESM(require("moment"));
651
+ import_moment4 = __toESM(require("moment"));
1278
652
  init_logger();
1279
653
  init_pouchdb_setup();
1280
654
  import_process = __toESM(require("process"));
@@ -1296,75 +670,6 @@ var init_couch = __esm({
1296
670
  return pouchdb_setup_default.fetch(url, opts);
1297
671
  }
1298
672
  };
1299
- REVIEW_PREFIX2 = "card_review_";
1300
- REVIEW_TIME_FORMAT2 = "YYYY-MM-DD--kk:mm:ss-SSS";
1301
- }
1302
- });
1303
-
1304
- // src/impl/couch/user-course-relDB.ts
1305
- var import_moment4, UsrCrsData;
1306
- var init_user_course_relDB = __esm({
1307
- "src/impl/couch/user-course-relDB.ts"() {
1308
- "use strict";
1309
- import_moment4 = __toESM(require("moment"));
1310
- init_couch();
1311
- init_courseDB();
1312
- init_logger();
1313
- UsrCrsData = class {
1314
- user;
1315
- course;
1316
- _courseId;
1317
- constructor(user, courseId) {
1318
- this.user = user;
1319
- this.course = new CourseDB(courseId, async () => this.user);
1320
- this._courseId = courseId;
1321
- }
1322
- async getReviewsForcast(daysCount) {
1323
- const time = import_moment4.default.utc().add(daysCount, "days");
1324
- return this.getReviewstoDate(time);
1325
- }
1326
- async getPendingReviews() {
1327
- const now = import_moment4.default.utc();
1328
- return this.getReviewstoDate(now);
1329
- }
1330
- async getScheduledReviewCount() {
1331
- return (await this.getPendingReviews()).length;
1332
- }
1333
- async getCourseSettings() {
1334
- const regDoc = await this.user.getCourseRegistrationsDoc();
1335
- const crsDoc = regDoc.courses.find((c) => c.courseID === this._courseId);
1336
- if (crsDoc && crsDoc.settings) {
1337
- return crsDoc.settings;
1338
- } else {
1339
- logger.warn(`no settings found during lookup on course ${this._courseId}`);
1340
- return {};
1341
- }
1342
- }
1343
- updateCourseSettings(updates) {
1344
- void this.user.updateCourseSettings(this._courseId, updates);
1345
- }
1346
- async getReviewstoDate(targetDate) {
1347
- const keys = getStartAndEndKeys2(REVIEW_PREFIX2);
1348
- const reviews = await this.user.remote().allDocs({
1349
- startkey: keys.startkey,
1350
- endkey: keys.endkey,
1351
- include_docs: true
1352
- });
1353
- logger.debug(
1354
- `Fetching ${this.user.getUsername()}'s scheduled reviews for course ${this._courseId}.`
1355
- );
1356
- return reviews.rows.filter((r) => {
1357
- if (r.id.startsWith(REVIEW_PREFIX2)) {
1358
- const date = import_moment4.default.utc(r.id.substr(REVIEW_PREFIX2.length), REVIEW_TIME_FORMAT2);
1359
- if (targetDate.isAfter(date)) {
1360
- if (this._courseId === void 0 || r.doc.courseId === this._courseId) {
1361
- return true;
1362
- }
1363
- }
1364
- }
1365
- }).map((r) => r.doc);
1366
- }
1367
- };
1368
673
  }
1369
674
  });
1370
675
 
@@ -1461,10 +766,11 @@ async function dropUserFromClassroom(user, classID) {
1461
766
  async function getUserClassrooms(user) {
1462
767
  return getOrCreateClassroomRegistrationsDoc(user);
1463
768
  }
1464
- var import_common8, import_moment5, log3, cardHistoryPrefix2, BaseUser, userCoursesDoc, userClassroomsDoc;
769
+ var import_common8, import_moment5, log3, BaseUser, userCoursesDoc, userClassroomsDoc;
1465
770
  var init_BaseUserDB = __esm({
1466
771
  "src/impl/common/BaseUserDB.ts"() {
1467
772
  "use strict";
773
+ init_core();
1468
774
  init_util();
1469
775
  import_common8 = require("@vue-skuilder/common");
1470
776
  import_moment5 = __toESM(require("moment"));
@@ -1477,7 +783,6 @@ var init_BaseUserDB = __esm({
1477
783
  log3 = (s) => {
1478
784
  logger.info(s);
1479
785
  };
1480
- cardHistoryPrefix2 = "cardH-";
1481
786
  BaseUser = class _BaseUser {
1482
787
  static _instance;
1483
788
  static _initialized = false;
@@ -1498,11 +803,13 @@ var init_BaseUserDB = __esm({
1498
803
  isLoggedIn() {
1499
804
  return !this._username.startsWith(GuestUsername);
1500
805
  }
1501
- remoteDB;
1502
806
  remote() {
1503
807
  return this.remoteDB;
1504
808
  }
1505
809
  localDB;
810
+ remoteDB;
811
+ writeDB;
812
+ // Database to use for write operations (local-first approach)
1506
813
  updateQueue;
1507
814
  async createAccount(username, password) {
1508
815
  if (!this.syncStrategy.canCreateAccount()) {
@@ -1566,8 +873,8 @@ Currently logged-in as ${this._username}.`
1566
873
  const allDocs = await localDB.allDocs({ include_docs: false });
1567
874
  const docsToDelete = allDocs.rows.filter((row) => {
1568
875
  const id = row.id;
1569
- return id.startsWith(cardHistoryPrefix2) || // Card interaction history
1570
- id.startsWith(REVIEW_PREFIX) || // Scheduled reviews
876
+ return id.startsWith(DocTypePrefixes["CARDRECORD" /* CARDRECORD */]) || // Card interaction history
877
+ id.startsWith(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */]) || // Scheduled reviews
1571
878
  id === _BaseUser.DOC_IDS.COURSE_REGISTRATIONS || // Course registrations
1572
879
  id === _BaseUser.DOC_IDS.CLASSROOM_REGISTRATIONS || // Classroom registrations
1573
880
  id === _BaseUser.DOC_IDS.CONFIG;
@@ -1636,7 +943,7 @@ Currently logged-in as ${this._username}.`
1636
943
  *
1637
944
  */
1638
945
  async getActiveCards() {
1639
- const keys = getStartAndEndKeys(REVIEW_PREFIX);
946
+ const keys = getStartAndEndKeys(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */]);
1640
947
  const reviews = await this.remoteDB.allDocs({
1641
948
  startkey: keys.startkey,
1642
949
  endkey: keys.endkey,
@@ -1708,7 +1015,7 @@ Currently logged-in as ${this._username}.`
1708
1015
  }
1709
1016
  }
1710
1017
  async getReviewstoDate(targetDate, course_id) {
1711
- const keys = getStartAndEndKeys(REVIEW_PREFIX);
1018
+ const keys = getStartAndEndKeys(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */]);
1712
1019
  const reviews = await this.remoteDB.allDocs({
1713
1020
  startkey: keys.startkey,
1714
1021
  endkey: keys.endkey,
@@ -1718,8 +1025,11 @@ Currently logged-in as ${this._username}.`
1718
1025
  `Fetching ${this._username}'s scheduled reviews${course_id ? ` for course ${course_id}` : ""}.`
1719
1026
  );
1720
1027
  return reviews.rows.filter((r) => {
1721
- if (r.id.startsWith(REVIEW_PREFIX)) {
1722
- const date = import_moment5.default.utc(r.id.substr(REVIEW_PREFIX.length), REVIEW_TIME_FORMAT);
1028
+ if (r.id.startsWith(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */])) {
1029
+ const date = import_moment5.default.utc(
1030
+ r.id.substr(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */].length),
1031
+ REVIEW_TIME_FORMAT
1032
+ );
1723
1033
  if (targetDate.isAfter(date)) {
1724
1034
  if (course_id === void 0 || r.doc.courseId === course_id) {
1725
1035
  return true;
@@ -1834,7 +1144,8 @@ Currently logged-in as ${this._username}.`
1834
1144
  const defaultConfig = {
1835
1145
  _id: _BaseUser.DOC_IDS.CONFIG,
1836
1146
  darkMode: false,
1837
- likesConfetti: false
1147
+ likesConfetti: false,
1148
+ sessionTimeLimit: 5
1838
1149
  };
1839
1150
  try {
1840
1151
  const cfg = await this.localDB.get(_BaseUser.DOC_IDS.CONFIG);
@@ -1907,7 +1218,8 @@ Currently logged-in as ${this._username}.`
1907
1218
  setDBandQ() {
1908
1219
  this.localDB = getLocalUserDB(this._username);
1909
1220
  this.remoteDB = this.syncStrategy.setupRemoteDB(this._username);
1910
- this.updateQueue = new UpdateQueue(this.localDB);
1221
+ this.writeDB = this.syncStrategy.getWriteDB ? this.syncStrategy.getWriteDB(this._username) : this.localDB;
1222
+ this.updateQueue = new UpdateQueue(this.localDB, this.writeDB);
1911
1223
  }
1912
1224
  async init() {
1913
1225
  _BaseUser._initialized = false;
@@ -2025,8 +1337,8 @@ Currently logged-in as ${this._username}.`
2025
1337
  streak: 0,
2026
1338
  bestInterval: 0
2027
1339
  };
2028
- void this.remoteDB.put(initCardHistory);
2029
- return initCardHistory;
1340
+ const putResult = await this.writeDB.put(initCardHistory);
1341
+ return { ...initCardHistory, _rev: putResult.rev };
2030
1342
  } else {
2031
1343
  throw new Error(`putCardRecord failed because of:
2032
1344
  name:${reason.name}
@@ -2061,7 +1373,7 @@ Currently logged-in as ${this._username}.`
2061
1373
  const deletePromises = duplicateDocIds.map(async (docId) => {
2062
1374
  try {
2063
1375
  const doc = await this.remoteDB.get(docId);
2064
- await this.remoteDB.remove(doc);
1376
+ await this.writeDB.remove(doc);
2065
1377
  log3(`Successfully removed duplicate review: ${docId}`);
2066
1378
  } catch (error) {
2067
1379
  log3(`Failed to remove duplicate review ${docId}: ${error}`);
@@ -2083,7 +1395,7 @@ Currently logged-in as ${this._username}.`
2083
1395
  * @param course_id optional specification of individual course
2084
1396
  */
2085
1397
  async getSeenCards(course_id) {
2086
- let prefix = cardHistoryPrefix2;
1398
+ let prefix = DocTypePrefixes["CARDRECORD" /* CARDRECORD */];
2087
1399
  if (course_id) {
2088
1400
  prefix += course_id;
2089
1401
  }
@@ -2092,8 +1404,8 @@ Currently logged-in as ${this._username}.`
2092
1404
  });
2093
1405
  const ret = [];
2094
1406
  docs.rows.forEach((row) => {
2095
- if (row.id.startsWith(cardHistoryPrefix2)) {
2096
- ret.push(row.id.substr(cardHistoryPrefix2.length));
1407
+ if (row.id.startsWith(DocTypePrefixes["CARDRECORD" /* CARDRECORD */])) {
1408
+ ret.push(row.id.substr(DocTypePrefixes["CARDRECORD" /* CARDRECORD */].length));
2097
1409
  }
2098
1410
  });
2099
1411
  return ret;
@@ -2105,7 +1417,7 @@ Currently logged-in as ${this._username}.`
2105
1417
  async getHistory() {
2106
1418
  const cards = await filterAllDocsByPrefix(
2107
1419
  this.remoteDB,
2108
- cardHistoryPrefix2,
1420
+ DocTypePrefixes["CARDRECORD" /* CARDRECORD */],
2109
1421
  {
2110
1422
  include_docs: true,
2111
1423
  attachments: false
@@ -2146,7 +1458,7 @@ Currently logged-in as ${this._username}.`
2146
1458
  } catch (e) {
2147
1459
  const err = e;
2148
1460
  if (err.status === 404) {
2149
- await this.remoteDB.put({
1461
+ await this.writeDB.put({
2150
1462
  _id: _BaseUser.DOC_IDS.CLASSROOM_REGISTRATIONS,
2151
1463
  registrations: []
2152
1464
  });
@@ -2194,10 +1506,10 @@ Currently logged-in as ${this._username}.`
2194
1506
  }
2195
1507
  }
2196
1508
  async scheduleCardReview(review) {
2197
- return scheduleCardReviewLocal(this.remoteDB, review);
1509
+ return scheduleCardReviewLocal(this.writeDB, review);
2198
1510
  }
2199
1511
  async removeScheduledCardReview(reviewId) {
2200
- return removeScheduledCardReviewLocal(this.remoteDB, reviewId);
1512
+ return removeScheduledCardReviewLocal(this.writeDB, reviewId);
2201
1513
  }
2202
1514
  async registerForClassroom(_classId, _registerAs) {
2203
1515
  return registerUserForClassroom(this._username, _classId, _registerAs);
@@ -2428,18 +1740,21 @@ var init_StaticDataUnpacker = __esm({
2428
1740
  async getTagsIndex() {
2429
1741
  return await this.loadIndex("tags");
2430
1742
  }
1743
+ getDocTypeFromId(id) {
1744
+ for (const docTypeKey in DocTypePrefixes) {
1745
+ const prefix = DocTypePrefixes[docTypeKey];
1746
+ if (id.startsWith(`${prefix}-`)) {
1747
+ return docTypeKey;
1748
+ }
1749
+ }
1750
+ return void 0;
1751
+ }
2431
1752
  /**
2432
1753
  * Find which chunk contains a specific document ID
2433
1754
  */
2434
1755
  async findChunkForDocument(docId) {
2435
- let expectedDocType = void 0;
2436
- for (const docType of Object.values(DocType)) {
2437
- if (docId.startsWith(`${docType}-`)) {
2438
- expectedDocType = docType;
2439
- break;
2440
- }
2441
- }
2442
- if (expectedDocType !== void 0) {
1756
+ const expectedDocType = this.getDocTypeFromId(docId);
1757
+ if (expectedDocType) {
2443
1758
  const typeChunks = this.manifest.chunks.filter((c) => c.docType === expectedDocType);
2444
1759
  for (const chunk of typeChunks) {
2445
1760
  if (docId >= chunk.startKey && docId <= chunk.endKey) {
@@ -2449,21 +1764,8 @@ var init_StaticDataUnpacker = __esm({
2449
1764
  }
2450
1765
  }
2451
1766
  }
2452
- return void 0;
2453
1767
  } else {
2454
- const displayableChunks = this.manifest.chunks.filter(
2455
- (c) => c.docType === "DISPLAYABLE_DATA"
2456
- );
2457
- for (const chunk of displayableChunks) {
2458
- if (docId >= chunk.startKey && docId <= chunk.endKey) {
2459
- const exists = await this.verifyDocumentInChunk(docId, chunk);
2460
- if (exists) {
2461
- return chunk;
2462
- }
2463
- }
2464
- }
2465
- const cardChunks = this.manifest.chunks.filter((c) => c.docType === "CARD");
2466
- for (const chunk of cardChunks) {
1768
+ for (const chunk of this.manifest.chunks) {
2467
1769
  if (docId >= chunk.startKey && docId <= chunk.endKey) {
2468
1770
  const exists = await this.verifyDocumentInChunk(docId, chunk);
2469
1771
  if (exists) {
@@ -2484,6 +1786,7 @@ var init_StaticDataUnpacker = __esm({
2484
1786
  }
2485
1787
  return void 0;
2486
1788
  }
1789
+ return void 0;
2487
1790
  }
2488
1791
  /**
2489
1792
  * Verify that a document actually exists in a specific chunk by loading and checking it
@@ -2727,6 +2030,7 @@ var init_courseDB3 = __esm({
2727
2030
  import_common11 = require("@vue-skuilder/common");
2728
2031
  init_types_legacy();
2729
2032
  init_navigators();
2033
+ init_logger();
2730
2034
  StaticCourseDB = class {
2731
2035
  constructor(courseId, unpacker, userDB, manifest) {
2732
2036
  this.courseId = courseId;
@@ -2748,10 +2052,11 @@ var init_courseDB3 = __esm({
2748
2052
  throw new Error("Cannot update course config in static mode");
2749
2053
  }
2750
2054
  async getCourseInfo() {
2055
+ const cardCount = this.manifest.chunks.filter((chunk) => chunk.docType === "CARD" /* CARD */).reduce((total, chunk) => total + chunk.documentCount, 0);
2751
2056
  return {
2752
- cardCount: 0,
2753
- // Would come from manifest
2057
+ cardCount,
2754
2058
  registeredUsers: 0
2059
+ // Always 0 in static mode
2755
2060
  };
2756
2061
  }
2757
2062
  async getCourseDoc(id, _options) {
@@ -2840,12 +2145,56 @@ var init_courseDB3 = __esm({
2840
2145
  courseID: this.courseId
2841
2146
  }));
2842
2147
  }
2843
- async getAppliedTags(_cardId) {
2844
- return {
2845
- total_rows: 0,
2846
- offset: 0,
2847
- rows: []
2848
- };
2148
+ async getAppliedTags(cardId) {
2149
+ try {
2150
+ const tagsIndex = await this.unpacker.getTagsIndex();
2151
+ const cardTags = tagsIndex.byCard[cardId] || [];
2152
+ const rows = await Promise.all(
2153
+ cardTags.map(async (tagName) => {
2154
+ const tagId = `${"TAG" /* TAG */}-${tagName}`;
2155
+ try {
2156
+ const tagDoc = await this.unpacker.getDocument(tagId);
2157
+ return {
2158
+ id: tagId,
2159
+ key: cardId,
2160
+ value: {
2161
+ name: tagDoc.name,
2162
+ snippet: tagDoc.snippet,
2163
+ count: tagDoc.taggedCards?.length || 0
2164
+ }
2165
+ };
2166
+ } catch (error) {
2167
+ if (error && error.status === 404) {
2168
+ logger.warn(`Tag document not found for ${tagName}, creating stub`);
2169
+ } else {
2170
+ logger.error(`Error getting tag document for ${tagName}:`, error);
2171
+ throw error;
2172
+ }
2173
+ return {
2174
+ id: tagId,
2175
+ key: cardId,
2176
+ value: {
2177
+ name: tagName,
2178
+ snippet: `Tag: ${tagName}`,
2179
+ count: tagsIndex.byTag[tagName]?.length || 0
2180
+ }
2181
+ };
2182
+ }
2183
+ })
2184
+ );
2185
+ return {
2186
+ total_rows: rows.length,
2187
+ offset: 0,
2188
+ rows
2189
+ };
2190
+ } catch (error) {
2191
+ logger.error(`Error getting applied tags for card ${cardId}:`, error);
2192
+ return {
2193
+ total_rows: 0,
2194
+ offset: 0,
2195
+ rows: []
2196
+ };
2197
+ }
2849
2198
  }
2850
2199
  async addTagToCard(_cardId, _tagId) {
2851
2200
  throw new Error("Cannot modify tags in static mode");
@@ -2863,11 +2212,69 @@ var init_courseDB3 = __esm({
2863
2212
  throw new Error("Cannot update tags in static mode");
2864
2213
  }
2865
2214
  async getCourseTagStubs() {
2866
- return {
2867
- total_rows: 0,
2868
- offset: 0,
2869
- rows: []
2870
- };
2215
+ try {
2216
+ const tagsIndex = await this.unpacker.getTagsIndex();
2217
+ if (!tagsIndex || !tagsIndex.byTag) {
2218
+ logger.warn("Tags index not found or empty");
2219
+ return {
2220
+ total_rows: 0,
2221
+ offset: 0,
2222
+ rows: []
2223
+ };
2224
+ }
2225
+ const tagNames = Object.keys(tagsIndex.byTag);
2226
+ const rows = await Promise.all(
2227
+ tagNames.map(async (tagName) => {
2228
+ const cardIds = tagsIndex.byTag[tagName] || [];
2229
+ const tagId = `${"TAG" /* TAG */}-${tagName}`;
2230
+ try {
2231
+ const tagDoc = await this.unpacker.getDocument(tagId);
2232
+ return {
2233
+ id: tagId,
2234
+ key: tagId,
2235
+ value: { rev: "1-static" },
2236
+ doc: tagDoc
2237
+ };
2238
+ } catch (error) {
2239
+ if (error && error.status === 404) {
2240
+ logger.warn(`Tag document not found for ${tagName}, creating stub`);
2241
+ const stubDoc = {
2242
+ _id: tagId,
2243
+ _rev: "1-static",
2244
+ course: this.courseId,
2245
+ docType: "TAG" /* TAG */,
2246
+ name: tagName,
2247
+ snippet: `Tag: ${tagName}`,
2248
+ wiki: "",
2249
+ taggedCards: cardIds,
2250
+ author: "system"
2251
+ };
2252
+ return {
2253
+ id: tagId,
2254
+ key: tagId,
2255
+ value: { rev: "1-static" },
2256
+ doc: stubDoc
2257
+ };
2258
+ } else {
2259
+ logger.error(`Error getting tag document for ${tagName}:`, error);
2260
+ throw error;
2261
+ }
2262
+ }
2263
+ })
2264
+ );
2265
+ return {
2266
+ total_rows: rows.length,
2267
+ offset: 0,
2268
+ rows
2269
+ };
2270
+ } catch (error) {
2271
+ logger.error("Failed to get course tag stubs:", error);
2272
+ return {
2273
+ total_rows: 0,
2274
+ offset: 0,
2275
+ rows: []
2276
+ };
2277
+ }
2871
2278
  }
2872
2279
  async addNote(_codeCourse, _shape, _data, _author, _tags, _uploads, _elo) {
2873
2280
  return {
@@ -2973,6 +2380,9 @@ var init_NoOpSyncStrategy = __esm({
2973
2380
  setupRemoteDB(username) {
2974
2381
  return getLocalUserDB(username);
2975
2382
  }
2383
+ getWriteDB(username) {
2384
+ return getLocalUserDB(username);
2385
+ }
2976
2386
  startSync(_localDB, _remoteDB) {
2977
2387
  }
2978
2388
  stopSync() {