@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
@@ -80,32 +80,31 @@ var init_SyncStrategy = __esm({
80
80
  });
81
81
 
82
82
  // src/core/types/types-legacy.ts
83
- var GuestUsername, DocType, cardHistoryPrefix;
83
+ var GuestUsername, DocTypePrefixes;
84
84
  var init_types_legacy = __esm({
85
85
  "src/core/types/types-legacy.ts"() {
86
86
  "use strict";
87
87
  init_logger();
88
88
  GuestUsername = "Guest";
89
- DocType = /* @__PURE__ */ ((DocType2) => {
90
- DocType2["DISPLAYABLE_DATA"] = "DISPLAYABLE_DATA";
91
- DocType2["CARD"] = "CARD";
92
- DocType2["DATASHAPE"] = "DATASHAPE";
93
- DocType2["QUESTIONTYPE"] = "QUESTION";
94
- DocType2["VIEW"] = "VIEW";
95
- DocType2["PEDAGOGY"] = "PEDAGOGY";
96
- DocType2["CARDRECORD"] = "CARDRECORD";
97
- DocType2["SCHEDULED_CARD"] = "SCHEDULED_CARD";
98
- DocType2["TAG"] = "TAG";
99
- DocType2["NAVIGATION_STRATEGY"] = "NAVIGATION_STRATEGY";
100
- return DocType2;
101
- })(DocType || {});
102
- cardHistoryPrefix = "cardH";
89
+ DocTypePrefixes = {
90
+ ["CARD" /* CARD */]: "c",
91
+ ["DISPLAYABLE_DATA" /* DISPLAYABLE_DATA */]: "dd",
92
+ ["TAG" /* TAG */]: "TAG",
93
+ ["CARDRECORD" /* CARDRECORD */]: "cardH",
94
+ ["SCHEDULED_CARD" /* SCHEDULED_CARD */]: "card_review_",
95
+ // Add other doctypes here as they get prefixed IDs
96
+ ["DATASHAPE" /* DATASHAPE */]: "DATASHAPE",
97
+ ["QUESTION" /* QUESTIONTYPE */]: "QUESTION",
98
+ ["VIEW" /* VIEW */]: "VIEW",
99
+ ["PEDAGOGY" /* PEDAGOGY */]: "PEDAGOGY",
100
+ ["NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */]: "NAVIGATION_STRATEGY"
101
+ };
103
102
  }
104
103
  });
105
104
 
106
105
  // src/core/util/index.ts
107
106
  function getCardHistoryID(courseID, cardID) {
108
- return `${cardHistoryPrefix}-${courseID}-${cardID}`;
107
+ return `${DocTypePrefixes["CARDRECORD" /* CARDRECORD */]}-${courseID}-${cardID}`;
109
108
  }
110
109
  var init_util = __esm({
111
110
  "src/core/util/index.ts"() {
@@ -188,11 +187,11 @@ function scheduleCardReviewLocal(userDB, review) {
188
187
  const now = moment.utc();
189
188
  logger.info(`Scheduling for review in: ${review.time.diff(now, "h") / 24} days`);
190
189
  void userDB.put({
191
- _id: REVIEW_PREFIX + review.time.format(REVIEW_TIME_FORMAT),
190
+ _id: DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */] + review.time.format(REVIEW_TIME_FORMAT),
192
191
  cardId: review.card_id,
193
- reviewTime: review.time,
192
+ reviewTime: review.time.toISOString(),
194
193
  courseId: review.course_id,
195
- scheduledAt: now,
194
+ scheduledAt: now.toISOString(),
196
195
  scheduledFor: review.scheduledFor,
197
196
  schedulingAgentId: review.schedulingAgentId
198
197
  });
@@ -208,14 +207,14 @@ async function removeScheduledCardReviewLocal(userDB, reviewDocID) {
208
207
  ${JSON.stringify(err)}`);
209
208
  });
210
209
  }
211
- var REVIEW_PREFIX, REVIEW_TIME_FORMAT, log;
210
+ var REVIEW_TIME_FORMAT, log;
212
211
  var init_userDBHelpers = __esm({
213
212
  "src/impl/common/userDBHelpers.ts"() {
214
213
  "use strict";
214
+ init_core();
215
215
  init_logger();
216
216
  init_pouchdb_setup();
217
217
  init_dataDirectory();
218
- REVIEW_PREFIX = "card_review_";
219
218
  REVIEW_TIME_FORMAT = "YYYY-MM-DD--kk:mm:ss-SSS";
220
219
  log = (s) => {
221
220
  logger.info(s);
@@ -250,7 +249,10 @@ var init_updateQueue = __esm({
250
249
  _className = "UpdateQueue";
251
250
  pendingUpdates = {};
252
251
  inprogressUpdates = {};
253
- db;
252
+ readDB;
253
+ // Database for read operations
254
+ writeDB;
255
+ // Database for write operations (local-first)
254
256
  update(id, update) {
255
257
  logger.debug(`Update requested on doc: ${id}`);
256
258
  if (this.pendingUpdates[id]) {
@@ -260,24 +262,25 @@ var init_updateQueue = __esm({
260
262
  }
261
263
  return this.applyUpdates(id);
262
264
  }
263
- constructor(db) {
265
+ constructor(readDB, writeDB) {
264
266
  super();
265
- this.db = db;
267
+ this.readDB = readDB;
268
+ this.writeDB = writeDB || readDB;
266
269
  logger.debug(`UpdateQ initialized...`);
267
- void this.db.info().then((i) => {
270
+ void this.readDB.info().then((i) => {
268
271
  logger.debug(`db info: ${JSON.stringify(i)}`);
269
272
  });
270
273
  }
271
274
  async applyUpdates(id) {
272
275
  logger.debug(`Applying updates on doc: ${id}`);
273
276
  if (this.inprogressUpdates[id]) {
274
- await this.db.info();
277
+ await this.readDB.info();
275
278
  return this.applyUpdates(id);
276
279
  } else {
277
280
  if (this.pendingUpdates[id] && this.pendingUpdates[id].length > 0) {
278
281
  this.inprogressUpdates[id] = true;
279
282
  try {
280
- let doc = await this.db.get(id);
283
+ let doc = await this.readDB.get(id);
281
284
  logger.debug(`Retrieved doc: ${id}`);
282
285
  while (this.pendingUpdates[id].length !== 0) {
283
286
  const update = this.pendingUpdates[id].splice(0, 1)[0];
@@ -290,7 +293,7 @@ var init_updateQueue = __esm({
290
293
  };
291
294
  }
292
295
  }
293
- await this.db.put(doc);
296
+ await this.writeDB.put(doc);
294
297
  logger.debug(`Put doc: ${id}`);
295
298
  if (this.pendingUpdates[id].length === 0) {
296
299
  this.inprogressUpdates[id] = false;
@@ -316,22 +319,64 @@ var init_updateQueue = __esm({
316
319
  }
317
320
  });
318
321
 
319
- // src/impl/couch/clientCache.ts
320
- async function GET_CACHED(k, f) {
321
- if (CLIENT_CACHE[k]) {
322
- return CLIENT_CACHE[k];
322
+ // src/impl/couch/user-course-relDB.ts
323
+ import moment2 from "moment";
324
+ var UsrCrsData;
325
+ var init_user_course_relDB = __esm({
326
+ "src/impl/couch/user-course-relDB.ts"() {
327
+ "use strict";
328
+ init_logger();
329
+ UsrCrsData = class {
330
+ user;
331
+ _courseId;
332
+ constructor(user, courseId) {
333
+ this.user = user;
334
+ this._courseId = courseId;
335
+ }
336
+ async getReviewsForcast(daysCount) {
337
+ const time = moment2.utc().add(daysCount, "days");
338
+ return this.getReviewstoDate(time);
339
+ }
340
+ async getPendingReviews() {
341
+ const now = moment2.utc();
342
+ return this.getReviewstoDate(now);
343
+ }
344
+ async getScheduledReviewCount() {
345
+ return (await this.getPendingReviews()).length;
346
+ }
347
+ async getCourseSettings() {
348
+ const regDoc = await this.user.getCourseRegistrationsDoc();
349
+ const crsDoc = regDoc.courses.find((c) => c.courseID === this._courseId);
350
+ if (crsDoc && crsDoc.settings) {
351
+ return crsDoc.settings;
352
+ } else {
353
+ logger.warn(`no settings found during lookup on course ${this._courseId}`);
354
+ return {};
355
+ }
356
+ }
357
+ updateCourseSettings(updates) {
358
+ if ("updateCourseSettings" in this.user) {
359
+ void this.user.updateCourseSettings(this._courseId, updates);
360
+ }
361
+ }
362
+ async getReviewstoDate(targetDate) {
363
+ const allReviews = await this.user.getPendingReviews(this._courseId);
364
+ logger.debug(
365
+ `Fetching ${this.user.getUsername()}'s scheduled reviews for course ${this._courseId}.`
366
+ );
367
+ return allReviews.filter((review) => {
368
+ const reviewTime = moment2.utc(review.reviewTime);
369
+ return targetDate.isAfter(reviewTime);
370
+ });
371
+ }
372
+ };
323
373
  }
324
- CLIENT_CACHE[k] = f ? await f(k) : await GET_ITEM(k);
325
- return GET_CACHED(k);
326
- }
327
- async function GET_ITEM(k) {
328
- throw new Error(`No implementation found for GET_CACHED(${k})`);
329
- }
330
- var CLIENT_CACHE;
374
+ });
375
+
376
+ // src/impl/couch/clientCache.ts
331
377
  var init_clientCache = __esm({
332
378
  "src/impl/couch/clientCache.ts"() {
333
379
  "use strict";
334
- CLIENT_CACHE = {};
335
380
  }
336
381
  });
337
382
 
@@ -339,94 +384,7 @@ var init_clientCache = __esm({
339
384
  import { NameSpacer } from "@vue-skuilder/common";
340
385
  import { blankCourseElo, toCourseElo } from "@vue-skuilder/common";
341
386
  import { prepareNote55 } from "@vue-skuilder/common";
342
- async function addNote55(courseID, codeCourse, shape, data, author, tags, uploads, elo = blankCourseElo()) {
343
- const db = getCourseDB(courseID);
344
- const payload = prepareNote55(courseID, codeCourse, shape, data, author, tags, uploads);
345
- const result = await db.post(payload);
346
- const dataShapeId = NameSpacer.getDataShapeString({
347
- course: codeCourse,
348
- dataShape: shape.name
349
- });
350
- if (result.ok) {
351
- try {
352
- await createCards(courseID, dataShapeId, result.id, tags, elo, author);
353
- } catch (error) {
354
- let errorMessage = "Unknown error";
355
- if (error instanceof Error) {
356
- errorMessage = error.message;
357
- } else if (error && typeof error === "object" && "reason" in error) {
358
- errorMessage = error.reason;
359
- } else if (error && typeof error === "object" && "message" in error) {
360
- errorMessage = error.message;
361
- } else {
362
- errorMessage = String(error);
363
- }
364
- logger.error(`[addNote55] Failed to create cards for note ${result.id}: ${errorMessage}`);
365
- result.cardCreationFailed = true;
366
- result.cardCreationError = errorMessage;
367
- }
368
- } else {
369
- logger.error(`[addNote55] Error adding note. Result: ${JSON.stringify(result)}`);
370
- }
371
- return result;
372
- }
373
- async function createCards(courseID, datashapeID, noteID, tags, elo = blankCourseElo(), author) {
374
- const cfg = await getCredentialledCourseConfig(courseID);
375
- const dsDescriptor = NameSpacer.getDataShapeDescriptor(datashapeID);
376
- let questionViewTypes = [];
377
- for (const ds of cfg.dataShapes) {
378
- if (ds.name === datashapeID) {
379
- questionViewTypes = ds.questionTypes;
380
- }
381
- }
382
- if (questionViewTypes.length === 0) {
383
- const errorMsg = `No questionViewTypes found for datashapeID: ${datashapeID} in course config. Cards cannot be created.`;
384
- logger.error(errorMsg);
385
- throw new Error(errorMsg);
386
- }
387
- for (const questionView of questionViewTypes) {
388
- await createCard(questionView, courseID, dsDescriptor, noteID, tags, elo, author);
389
- }
390
- }
391
- async function createCard(questionViewName, courseID, dsDescriptor, noteID, tags, elo = blankCourseElo(), author) {
392
- const qDescriptor = NameSpacer.getQuestionDescriptor(questionViewName);
393
- const cfg = await getCredentialledCourseConfig(courseID);
394
- for (const rQ of cfg.questionTypes) {
395
- if (rQ.name === questionViewName) {
396
- for (const view of rQ.viewList) {
397
- await addCard(
398
- courseID,
399
- dsDescriptor.course,
400
- [noteID],
401
- NameSpacer.getViewString({
402
- course: qDescriptor.course,
403
- questionType: qDescriptor.questionType,
404
- view
405
- }),
406
- elo,
407
- tags,
408
- author
409
- );
410
- }
411
- }
412
- }
413
- }
414
- async function addCard(courseID, course, id_displayable_data, id_view, elo, tags, author) {
415
- const db = getCourseDB(courseID);
416
- const card = await db.post({
417
- course,
418
- id_displayable_data,
419
- id_view,
420
- docType: "CARD" /* CARD */,
421
- elo: elo || toCourseElo(990 + Math.round(20 * Math.random())),
422
- author
423
- });
424
- for (const tag of tags) {
425
- logger.info(`adding tag: ${tag} to card ${card.id}`);
426
- await addTagToCard(courseID, card.id, tag, author, false);
427
- }
428
- return card;
429
- }
387
+ import { v4 as uuidv4 } from "uuid";
430
388
  async function getCredentialledCourseConfig(courseID) {
431
389
  try {
432
390
  const db = getCourseDB(courseID);
@@ -439,66 +397,6 @@ async function getCredentialledCourseConfig(courseID) {
439
397
  throw e;
440
398
  }
441
399
  }
442
- async function addTagToCard(courseID, cardID, tagID, author, updateELO = true) {
443
- const prefixedTagID = getTagID(tagID);
444
- const courseDB = getCourseDB(courseID);
445
- const courseApi = new CourseDB(courseID, async () => {
446
- const dummySyncStrategy = {
447
- setupRemoteDB: () => null,
448
- startSync: () => {
449
- },
450
- canCreateAccount: () => false,
451
- canAuthenticate: () => false,
452
- getCurrentUsername: async () => "DummyUser"
453
- };
454
- return BaseUser.Dummy(dummySyncStrategy);
455
- });
456
- try {
457
- logger.info(`Applying tag ${tagID} to card ${courseID + "-" + cardID}...`);
458
- const tag = await courseDB.get(prefixedTagID);
459
- if (!tag.taggedCards.includes(cardID)) {
460
- tag.taggedCards.push(cardID);
461
- if (updateELO) {
462
- try {
463
- const eloData = await courseApi.getCardEloData([cardID]);
464
- const elo = eloData[0];
465
- elo.tags[tagID] = {
466
- count: 0,
467
- score: elo.global.score
468
- // todo: or 1000?
469
- };
470
- await updateCardElo(courseID, cardID, elo);
471
- } catch (error) {
472
- logger.error("Failed to update ELO data for card:", cardID, error);
473
- }
474
- }
475
- return courseDB.put(tag);
476
- } else throw new AlreadyTaggedErr(`Card ${cardID} is already tagged with ${tagID}`);
477
- } catch (e) {
478
- if (e instanceof AlreadyTaggedErr) {
479
- throw e;
480
- }
481
- await createTag(courseID, tagID, author);
482
- return addTagToCard(courseID, cardID, tagID, author, updateELO);
483
- }
484
- }
485
- async function updateCardElo(courseID, cardID, elo) {
486
- if (elo) {
487
- const cDB = getCourseDB(courseID);
488
- const card = await cDB.get(cardID);
489
- logger.debug(`Replacing ${JSON.stringify(card.elo)} with ${JSON.stringify(elo)}`);
490
- card.elo = elo;
491
- return cDB.put(card);
492
- }
493
- }
494
- function getTagID(tagName) {
495
- const tagPrefix = "TAG" /* TAG */.valueOf() + "-";
496
- if (tagName.indexOf(tagPrefix) === 0) {
497
- return tagName;
498
- } else {
499
- return tagPrefix + tagName;
500
- }
501
- }
502
400
  function getCourseDB(courseID) {
503
401
  const dbName = `coursedb-${courseID}`;
504
402
  return new pouchdb_setup_default(
@@ -506,7 +404,6 @@ function getCourseDB(courseID) {
506
404
  pouchDBincludeCredentialsConfig
507
405
  );
508
406
  }
509
- var AlreadyTaggedErr;
510
407
  var init_courseAPI = __esm({
511
408
  "src/impl/couch/courseAPI.ts"() {
512
409
  "use strict";
@@ -517,12 +414,6 @@ var init_courseAPI = __esm({
517
414
  init_types_legacy();
518
415
  init_common();
519
416
  init_logger();
520
- AlreadyTaggedErr = class extends Error {
521
- constructor(message) {
522
- super(message);
523
- this.name = "AlreadyTaggedErr";
524
- }
525
- };
526
417
  }
527
418
  });
528
419
 
@@ -663,82 +554,6 @@ import {
663
554
  blankCourseElo as blankCourseElo2,
664
555
  toCourseElo as toCourseElo2
665
556
  } from "@vue-skuilder/common";
666
- function randIntWeightedTowardZero(n) {
667
- return Math.floor(Math.random() * Math.random() * Math.random() * n);
668
- }
669
- async function getCourseTagStubs(courseID) {
670
- logger.debug(`Getting tag stubs for course: ${courseID}`);
671
- const stubs = await filterAllDocsByPrefix2(
672
- getCourseDB2(courseID),
673
- "TAG" /* TAG */.valueOf() + "-"
674
- );
675
- stubs.rows.forEach((row) => {
676
- logger.debug(` Tag stub for doc: ${row.id}`);
677
- });
678
- return stubs;
679
- }
680
- async function createTag(courseID, tagName, author) {
681
- logger.debug(`Creating tag: ${tagName}...`);
682
- const tagID = getTagID(tagName);
683
- const courseDB = getCourseDB2(courseID);
684
- const resp = await courseDB.put({
685
- course: courseID,
686
- docType: "TAG" /* TAG */,
687
- name: tagName,
688
- snippet: "",
689
- taggedCards: [],
690
- wiki: "",
691
- author,
692
- _id: tagID
693
- });
694
- return resp;
695
- }
696
- async function updateTag(tag) {
697
- const prior = await getTag(tag.course, tag.name);
698
- return await getCourseDB2(tag.course).put({
699
- ...tag,
700
- _rev: prior._rev
701
- });
702
- }
703
- async function getTag(courseID, tagName) {
704
- const tagID = getTagID(tagName);
705
- const courseDB = getCourseDB2(courseID);
706
- return courseDB.get(tagID);
707
- }
708
- async function removeTagFromCard(courseID, cardID, tagID) {
709
- tagID = getTagID(tagID);
710
- const courseDB = getCourseDB2(courseID);
711
- const tag = await courseDB.get(tagID);
712
- tag.taggedCards = tag.taggedCards.filter((taggedID) => {
713
- return cardID !== taggedID;
714
- });
715
- return courseDB.put(tag);
716
- }
717
- async function getAppliedTags(id_course, id_card) {
718
- const db = getCourseDB2(id_course);
719
- const result = await db.query("getTags", {
720
- startkey: id_card,
721
- endkey: id_card
722
- // include_docs: true
723
- });
724
- return result;
725
- }
726
- async function updateCredentialledCourseConfig(courseID, config) {
727
- logger.debug(`Updating course config:
728
-
729
- ${JSON.stringify(config)}
730
- `);
731
- const db = getCourseDB2(courseID);
732
- const old = await getCredentialledCourseConfig(courseID);
733
- return await db.put({
734
- ...config,
735
- _rev: old._rev
736
- });
737
- }
738
- function isSuccessRow(row) {
739
- return "doc" in row && row.doc !== null && row.doc !== void 0;
740
- }
741
- var CourseDB;
742
557
  var init_courseDB = __esm({
743
558
  "src/impl/couch/courseDB.ts"() {
744
559
  "use strict";
@@ -750,422 +565,11 @@ var init_courseDB = __esm({
750
565
  init_courseAPI();
751
566
  init_courseLookupDB();
752
567
  init_navigators();
753
- CourseDB = class {
754
- // private log(msg: string): void {
755
- // log(`CourseLog: ${this.id}\n ${msg}`);
756
- // }
757
- db;
758
- id;
759
- _getCurrentUser;
760
- updateQueue;
761
- constructor(id, userLookup) {
762
- this.id = id;
763
- this.db = getCourseDB2(this.id);
764
- this._getCurrentUser = userLookup;
765
- this.updateQueue = new UpdateQueue(this.db);
766
- }
767
- getCourseID() {
768
- return this.id;
769
- }
770
- async getCourseInfo() {
771
- const cardCount = (await this.db.find({
772
- selector: {
773
- docType: "CARD" /* CARD */
774
- },
775
- limit: 1e3
776
- })).docs.length;
777
- return {
778
- cardCount,
779
- registeredUsers: 0
780
- };
781
- }
782
- async getInexperiencedCards(limit = 2) {
783
- return (await this.db.query("cardsByInexperience", {
784
- limit
785
- })).rows.map((r) => {
786
- const ret = {
787
- courseId: this.id,
788
- cardId: r.id,
789
- count: r.key,
790
- elo: r.value
791
- };
792
- return ret;
793
- });
794
- }
795
- async getCardsByEloLimits(options = {
796
- low: 0,
797
- high: Number.MIN_SAFE_INTEGER,
798
- limit: 25,
799
- page: 0
800
- }) {
801
- return (await this.db.query("elo", {
802
- startkey: options.low,
803
- endkey: options.high,
804
- limit: options.limit,
805
- skip: options.limit * options.page
806
- })).rows.map((r) => {
807
- return `${this.id}-${r.id}-${r.key}`;
808
- });
809
- }
810
- async getCardEloData(id) {
811
- const docs = await this.db.allDocs({
812
- keys: id,
813
- include_docs: true
814
- });
815
- const ret = [];
816
- docs.rows.forEach((r) => {
817
- if (isSuccessRow(r)) {
818
- if (r.doc && r.doc.elo) {
819
- ret.push(toCourseElo2(r.doc.elo));
820
- } else {
821
- logger.warn("no elo data for card: " + r.id);
822
- ret.push(blankCourseElo2());
823
- }
824
- } else {
825
- logger.warn("no elo data for card: " + JSON.stringify(r));
826
- ret.push(blankCourseElo2());
827
- }
828
- });
829
- return ret;
830
- }
831
- /**
832
- * Returns the lowest and highest `global` ELO ratings in the course
833
- */
834
- async getELOBounds() {
835
- const [low, high] = await Promise.all([
836
- (await this.db.query("elo", {
837
- startkey: 0,
838
- limit: 1,
839
- include_docs: false
840
- })).rows[0].key,
841
- (await this.db.query("elo", {
842
- limit: 1,
843
- descending: true,
844
- startkey: 1e5
845
- })).rows[0].key
846
- ]);
847
- return {
848
- low,
849
- high
850
- };
851
- }
852
- async removeCard(id) {
853
- const doc = await this.db.get(id);
854
- if (!doc.docType || !(doc.docType === "CARD" /* CARD */)) {
855
- throw new Error(`failed to remove ${id} from course ${this.id}. id does not point to a card`);
856
- }
857
- return this.db.remove(doc);
858
- }
859
- async getCardDisplayableDataIDs(id) {
860
- logger.debug(id.join(", "));
861
- const cards = await this.db.allDocs({
862
- keys: id,
863
- include_docs: true
864
- });
865
- const ret = {};
866
- cards.rows.forEach((r) => {
867
- if (isSuccessRow(r)) {
868
- ret[r.id] = r.doc.id_displayable_data;
869
- }
870
- });
871
- await Promise.all(
872
- cards.rows.map((r) => {
873
- return async () => {
874
- if (isSuccessRow(r)) {
875
- ret[r.id] = r.doc.id_displayable_data;
876
- }
877
- };
878
- })
879
- );
880
- return ret;
881
- }
882
- async getCardsByELO(elo, cardLimit) {
883
- elo = parseInt(elo);
884
- const limit = cardLimit ? cardLimit : 25;
885
- const below = await this.db.query("elo", {
886
- limit: Math.ceil(limit / 2),
887
- startkey: elo,
888
- descending: true
889
- });
890
- const aboveLimit = limit - below.rows.length;
891
- const above = await this.db.query("elo", {
892
- limit: aboveLimit,
893
- startkey: elo + 1
894
- });
895
- let cards = below.rows;
896
- cards = cards.concat(above.rows);
897
- const ret = cards.sort((a, b) => {
898
- const s = Math.abs(a.key - elo) - Math.abs(b.key - elo);
899
- if (s === 0) {
900
- return Math.random() - 0.5;
901
- } else {
902
- return s;
903
- }
904
- }).map((c) => `${this.id}-${c.id}-${c.key}`);
905
- const str = `below:
906
- ${below.rows.map((r) => ` ${r.id}-${r.key}
907
- `)}
908
-
909
- above:
910
- ${above.rows.map((r) => ` ${r.id}-${r.key}
911
- `)}`;
912
- logger.debug(`Getting ${limit} cards centered around elo: ${elo}:
913
-
914
- ` + str);
915
- return ret;
916
- }
917
- async getCourseConfig() {
918
- const ret = await getCredentialledCourseConfig(this.id);
919
- if (ret) {
920
- return ret;
921
- } else {
922
- throw new Error(`Course config not found for course ID: ${this.id}`);
923
- }
924
- }
925
- async updateCourseConfig(cfg) {
926
- logger.debug(`Updating: ${JSON.stringify(cfg)}`);
927
- try {
928
- return await updateCredentialledCourseConfig(this.id, cfg);
929
- } catch (error) {
930
- logger.error(`Error updating course config in course DB: ${error}`);
931
- throw error;
932
- }
933
- }
934
- async updateCardElo(cardId, elo) {
935
- if (!elo) {
936
- throw new Error(`Cannot update card elo with null or undefined value for card ID: ${cardId}`);
937
- }
938
- try {
939
- const result = await this.updateQueue.update(cardId, (card) => {
940
- logger.debug(`Replacing ${JSON.stringify(card.elo)} with ${JSON.stringify(elo)}`);
941
- card.elo = elo;
942
- return card;
943
- });
944
- return { ok: true, id: cardId, rev: result._rev };
945
- } catch (error) {
946
- logger.error(`Failed to update card elo for card ID: ${cardId}`, error);
947
- throw new Error(`Failed to update card elo for card ID: ${cardId}`);
948
- }
949
- }
950
- async getAppliedTags(cardId) {
951
- const ret = await getAppliedTags(this.id, cardId);
952
- if (ret) {
953
- return ret;
954
- } else {
955
- throw new Error(`Failed to find tags for card ${this.id}-${cardId}`);
956
- }
957
- }
958
- async addTagToCard(cardId, tagId, updateELO) {
959
- return await addTagToCard(this.id, cardId, tagId, (await this._getCurrentUser()).getUsername(), updateELO);
960
- }
961
- async removeTagFromCard(cardId, tagId) {
962
- return await removeTagFromCard(this.id, cardId, tagId);
963
- }
964
- async createTag(name, author) {
965
- return await createTag(this.id, name, author);
966
- }
967
- async getTag(tagId) {
968
- return await getTag(this.id, tagId);
969
- }
970
- async updateTag(tag) {
971
- if (tag.course !== this.id) {
972
- throw new Error(`Tag ${JSON.stringify(tag)} does not belong to course ${this.id}`);
973
- }
974
- return await updateTag(tag);
975
- }
976
- async getCourseTagStubs() {
977
- return getCourseTagStubs(this.id);
978
- }
979
- async addNote(codeCourse, shape, data, author, tags, uploads, elo = blankCourseElo2()) {
980
- try {
981
- const resp = await addNote55(this.id, codeCourse, shape, data, author, tags, uploads, elo);
982
- if (resp.ok) {
983
- if (resp.cardCreationFailed) {
984
- logger.warn(
985
- `[courseDB.addNote] Note added but card creation failed: ${resp.cardCreationError}`
986
- );
987
- return {
988
- status: Status.error,
989
- message: `Note was added but no cards were created: ${resp.cardCreationError}`,
990
- id: resp.id
991
- };
992
- }
993
- return {
994
- status: Status.ok,
995
- message: "",
996
- id: resp.id
997
- };
998
- } else {
999
- return {
1000
- status: Status.error,
1001
- message: "Unexpected error adding note"
1002
- };
1003
- }
1004
- } catch (e) {
1005
- const err = e;
1006
- logger.error(
1007
- `[addNote] error ${err.name}
1008
- reason: ${err.reason}
1009
- message: ${err.message}`
1010
- );
1011
- return {
1012
- status: Status.error,
1013
- message: `Error adding note to course. ${e.reason || err.message}`
1014
- };
1015
- }
1016
- }
1017
- async getCourseDoc(id, options) {
1018
- return await getCourseDoc(this.id, id, options);
1019
- }
1020
- async getCourseDocs(ids, options = {}) {
1021
- return await getCourseDocs(this.id, ids, options);
1022
- }
1023
- ////////////////////////////////////
1024
- // NavigationStrategyManager implementation
1025
- ////////////////////////////////////
1026
- getNavigationStrategy(id) {
1027
- logger.debug(`[courseDB] Getting navigation strategy: ${id}`);
1028
- const strategy = {
1029
- id: "ELO",
1030
- docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
1031
- name: "ELO",
1032
- description: "ELO-based navigation strategy for ordering content by difficulty",
1033
- implementingClass: "elo" /* ELO */,
1034
- course: this.id,
1035
- serializedData: ""
1036
- // serde is a noop for ELO navigator.
1037
- };
1038
- return Promise.resolve(strategy);
1039
- }
1040
- getAllNavigationStrategies() {
1041
- logger.debug("[courseDB] Returning hard-coded navigation strategies");
1042
- const strategies = [
1043
- {
1044
- id: "ELO",
1045
- docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
1046
- name: "ELO",
1047
- description: "ELO-based navigation strategy for ordering content by difficulty",
1048
- implementingClass: "elo" /* ELO */,
1049
- course: this.id,
1050
- serializedData: ""
1051
- // serde is a noop for ELO navigator.
1052
- }
1053
- ];
1054
- return Promise.resolve(strategies);
1055
- }
1056
- addNavigationStrategy(data) {
1057
- logger.debug(`[courseDB] Adding navigation strategy: ${data.id}`);
1058
- logger.debug(JSON.stringify(data));
1059
- return Promise.resolve();
1060
- }
1061
- updateNavigationStrategy(id, data) {
1062
- logger.debug(`[courseDB] Updating navigation strategy: ${id}`);
1063
- logger.debug(JSON.stringify(data));
1064
- return Promise.resolve();
1065
- }
1066
- async surfaceNavigationStrategy() {
1067
- logger.warn(`Returning hard-coded default ELO navigator`);
1068
- const ret = {
1069
- id: "ELO",
1070
- docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
1071
- name: "ELO",
1072
- description: "ELO-based navigation strategy",
1073
- implementingClass: "elo" /* ELO */,
1074
- course: this.id,
1075
- serializedData: ""
1076
- // serde is a noop for ELO navigator.
1077
- };
1078
- return Promise.resolve(ret);
1079
- }
1080
- ////////////////////////////////////
1081
- // END NavigationStrategyManager implementation
1082
- ////////////////////////////////////
1083
- ////////////////////////////////////
1084
- // StudyContentSource implementation
1085
- ////////////////////////////////////
1086
- async getNewCards(limit = 99) {
1087
- const u = await this._getCurrentUser();
1088
- try {
1089
- const strategy = await this.surfaceNavigationStrategy();
1090
- const navigator = await ContentNavigator.create(u, this, strategy);
1091
- return navigator.getNewCards(limit);
1092
- } catch (e) {
1093
- logger.error(`[courseDB] Error surfacing a NavigationStrategy: ${e}`);
1094
- throw e;
1095
- }
1096
- }
1097
- async getPendingReviews() {
1098
- const u = await this._getCurrentUser();
1099
- try {
1100
- const strategy = await this.surfaceNavigationStrategy();
1101
- const navigator = await ContentNavigator.create(u, this, strategy);
1102
- return navigator.getPendingReviews();
1103
- } catch (e) {
1104
- logger.error(`[courseDB] Error surfacing a NavigationStrategy: ${e}`);
1105
- throw e;
1106
- }
1107
- }
1108
- async getCardsCenteredAtELO(options = {
1109
- limit: 99,
1110
- elo: "user"
1111
- }, filter) {
1112
- let targetElo;
1113
- if (options.elo === "user") {
1114
- const u = await this._getCurrentUser();
1115
- targetElo = -1;
1116
- try {
1117
- const courseDoc = (await u.getCourseRegistrationsDoc()).courses.find((c) => {
1118
- return c.courseID === this.id;
1119
- });
1120
- targetElo = EloToNumber(courseDoc.elo);
1121
- } catch {
1122
- targetElo = 1e3;
1123
- }
1124
- } else if (options.elo === "random") {
1125
- const bounds = await GET_CACHED(`elo-bounds-${this.id}`, () => this.getELOBounds());
1126
- targetElo = Math.round(bounds.low + Math.random() * (bounds.high - bounds.low));
1127
- } else {
1128
- targetElo = options.elo;
1129
- }
1130
- let cards = [];
1131
- let mult = 4;
1132
- let previousCount = -1;
1133
- let newCount = 0;
1134
- while (cards.length < options.limit && newCount !== previousCount) {
1135
- cards = await this.getCardsByELO(targetElo, mult * options.limit);
1136
- previousCount = newCount;
1137
- newCount = cards.length;
1138
- logger.debug(`Found ${cards.length} elo neighbor cards...`);
1139
- if (filter) {
1140
- cards = cards.filter(filter);
1141
- logger.debug(`Filtered to ${cards.length} cards...`);
1142
- }
1143
- mult *= 2;
1144
- }
1145
- const selectedCards = [];
1146
- while (selectedCards.length < options.limit && cards.length > 0) {
1147
- const index = randIntWeightedTowardZero(cards.length);
1148
- const card = cards.splice(index, 1)[0];
1149
- selectedCards.push(card);
1150
- }
1151
- return selectedCards.map((c) => {
1152
- const split = c.split("-");
1153
- return {
1154
- courseID: this.id,
1155
- cardID: split[1],
1156
- qualifiedID: `${split[0]}-${split[1]}`,
1157
- contentSourceType: "course",
1158
- contentSourceID: this.id,
1159
- status: "new"
1160
- };
1161
- });
1162
- }
1163
- };
1164
568
  }
1165
569
  });
1166
570
 
1167
571
  // src/impl/couch/classroomDB.ts
1168
- import moment2 from "moment";
572
+ import moment3 from "moment";
1169
573
  var init_classroomDB2 = __esm({
1170
574
  "src/impl/couch/classroomDB.ts"() {
1171
575
  "use strict";
@@ -1216,41 +620,9 @@ var init_CouchDBSyncStrategy = __esm({
1216
620
  });
1217
621
 
1218
622
  // src/impl/couch/index.ts
1219
- import moment3 from "moment";
623
+ import moment4 from "moment";
1220
624
  import process2 from "process";
1221
- function getCourseDB2(courseID) {
1222
- return new pouchdb_setup_default(
1223
- ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + "coursedb-" + courseID,
1224
- pouchDBincludeCredentialsConfig
1225
- );
1226
- }
1227
- function getCourseDocs(courseID, docIDs, options = {}) {
1228
- return getCourseDB2(courseID).allDocs({
1229
- ...options,
1230
- keys: docIDs
1231
- });
1232
- }
1233
- function getCourseDoc(courseID, docID, options = {}) {
1234
- return getCourseDB2(courseID).get(docID, options);
1235
- }
1236
- function filterAllDocsByPrefix2(db, prefix, opts) {
1237
- const options = {
1238
- startkey: prefix,
1239
- endkey: prefix + "\uFFF0",
1240
- include_docs: true
1241
- };
1242
- if (opts) {
1243
- Object.assign(options, opts);
1244
- }
1245
- return db.allDocs(options);
1246
- }
1247
- function getStartAndEndKeys2(key) {
1248
- return {
1249
- startkey: key,
1250
- endkey: key + "\uFFF0"
1251
- };
1252
- }
1253
- var isBrowser, GUEST_LOCAL_DB, localUserDB, pouchDBincludeCredentialsConfig, REVIEW_PREFIX2, REVIEW_TIME_FORMAT2;
625
+ var isBrowser, GUEST_LOCAL_DB, localUserDB, pouchDBincludeCredentialsConfig;
1254
626
  var init_couch = __esm({
1255
627
  "src/impl/couch/index.ts"() {
1256
628
  "use strict";
@@ -1276,75 +648,6 @@ var init_couch = __esm({
1276
648
  return pouchdb_setup_default.fetch(url, opts);
1277
649
  }
1278
650
  };
1279
- REVIEW_PREFIX2 = "card_review_";
1280
- REVIEW_TIME_FORMAT2 = "YYYY-MM-DD--kk:mm:ss-SSS";
1281
- }
1282
- });
1283
-
1284
- // src/impl/couch/user-course-relDB.ts
1285
- import moment4 from "moment";
1286
- var UsrCrsData;
1287
- var init_user_course_relDB = __esm({
1288
- "src/impl/couch/user-course-relDB.ts"() {
1289
- "use strict";
1290
- init_couch();
1291
- init_courseDB();
1292
- init_logger();
1293
- UsrCrsData = class {
1294
- user;
1295
- course;
1296
- _courseId;
1297
- constructor(user, courseId) {
1298
- this.user = user;
1299
- this.course = new CourseDB(courseId, async () => this.user);
1300
- this._courseId = courseId;
1301
- }
1302
- async getReviewsForcast(daysCount) {
1303
- const time = moment4.utc().add(daysCount, "days");
1304
- return this.getReviewstoDate(time);
1305
- }
1306
- async getPendingReviews() {
1307
- const now = moment4.utc();
1308
- return this.getReviewstoDate(now);
1309
- }
1310
- async getScheduledReviewCount() {
1311
- return (await this.getPendingReviews()).length;
1312
- }
1313
- async getCourseSettings() {
1314
- const regDoc = await this.user.getCourseRegistrationsDoc();
1315
- const crsDoc = regDoc.courses.find((c) => c.courseID === this._courseId);
1316
- if (crsDoc && crsDoc.settings) {
1317
- return crsDoc.settings;
1318
- } else {
1319
- logger.warn(`no settings found during lookup on course ${this._courseId}`);
1320
- return {};
1321
- }
1322
- }
1323
- updateCourseSettings(updates) {
1324
- void this.user.updateCourseSettings(this._courseId, updates);
1325
- }
1326
- async getReviewstoDate(targetDate) {
1327
- const keys = getStartAndEndKeys2(REVIEW_PREFIX2);
1328
- const reviews = await this.user.remote().allDocs({
1329
- startkey: keys.startkey,
1330
- endkey: keys.endkey,
1331
- include_docs: true
1332
- });
1333
- logger.debug(
1334
- `Fetching ${this.user.getUsername()}'s scheduled reviews for course ${this._courseId}.`
1335
- );
1336
- return reviews.rows.filter((r) => {
1337
- if (r.id.startsWith(REVIEW_PREFIX2)) {
1338
- const date = moment4.utc(r.id.substr(REVIEW_PREFIX2.length), REVIEW_TIME_FORMAT2);
1339
- if (targetDate.isAfter(date)) {
1340
- if (this._courseId === void 0 || r.doc.courseId === this._courseId) {
1341
- return true;
1342
- }
1343
- }
1344
- }
1345
- }).map((r) => r.doc);
1346
- }
1347
- };
1348
651
  }
1349
652
  });
1350
653
 
@@ -1443,10 +746,11 @@ async function dropUserFromClassroom(user, classID) {
1443
746
  async function getUserClassrooms(user) {
1444
747
  return getOrCreateClassroomRegistrationsDoc(user);
1445
748
  }
1446
- var log3, cardHistoryPrefix2, BaseUser, userCoursesDoc, userClassroomsDoc;
749
+ var log3, BaseUser, userCoursesDoc, userClassroomsDoc;
1447
750
  var init_BaseUserDB = __esm({
1448
751
  "src/impl/common/BaseUserDB.ts"() {
1449
752
  "use strict";
753
+ init_core();
1450
754
  init_util();
1451
755
  init_types_legacy();
1452
756
  init_logger();
@@ -1457,7 +761,6 @@ var init_BaseUserDB = __esm({
1457
761
  log3 = (s) => {
1458
762
  logger.info(s);
1459
763
  };
1460
- cardHistoryPrefix2 = "cardH-";
1461
764
  BaseUser = class _BaseUser {
1462
765
  static _instance;
1463
766
  static _initialized = false;
@@ -1478,11 +781,13 @@ var init_BaseUserDB = __esm({
1478
781
  isLoggedIn() {
1479
782
  return !this._username.startsWith(GuestUsername);
1480
783
  }
1481
- remoteDB;
1482
784
  remote() {
1483
785
  return this.remoteDB;
1484
786
  }
1485
787
  localDB;
788
+ remoteDB;
789
+ writeDB;
790
+ // Database to use for write operations (local-first approach)
1486
791
  updateQueue;
1487
792
  async createAccount(username, password) {
1488
793
  if (!this.syncStrategy.canCreateAccount()) {
@@ -1546,8 +851,8 @@ Currently logged-in as ${this._username}.`
1546
851
  const allDocs = await localDB.allDocs({ include_docs: false });
1547
852
  const docsToDelete = allDocs.rows.filter((row) => {
1548
853
  const id = row.id;
1549
- return id.startsWith(cardHistoryPrefix2) || // Card interaction history
1550
- id.startsWith(REVIEW_PREFIX) || // Scheduled reviews
854
+ return id.startsWith(DocTypePrefixes["CARDRECORD" /* CARDRECORD */]) || // Card interaction history
855
+ id.startsWith(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */]) || // Scheduled reviews
1551
856
  id === _BaseUser.DOC_IDS.COURSE_REGISTRATIONS || // Course registrations
1552
857
  id === _BaseUser.DOC_IDS.CLASSROOM_REGISTRATIONS || // Classroom registrations
1553
858
  id === _BaseUser.DOC_IDS.CONFIG;
@@ -1616,7 +921,7 @@ Currently logged-in as ${this._username}.`
1616
921
  *
1617
922
  */
1618
923
  async getActiveCards() {
1619
- const keys = getStartAndEndKeys(REVIEW_PREFIX);
924
+ const keys = getStartAndEndKeys(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */]);
1620
925
  const reviews = await this.remoteDB.allDocs({
1621
926
  startkey: keys.startkey,
1622
927
  endkey: keys.endkey,
@@ -1688,7 +993,7 @@ Currently logged-in as ${this._username}.`
1688
993
  }
1689
994
  }
1690
995
  async getReviewstoDate(targetDate, course_id) {
1691
- const keys = getStartAndEndKeys(REVIEW_PREFIX);
996
+ const keys = getStartAndEndKeys(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */]);
1692
997
  const reviews = await this.remoteDB.allDocs({
1693
998
  startkey: keys.startkey,
1694
999
  endkey: keys.endkey,
@@ -1698,8 +1003,11 @@ Currently logged-in as ${this._username}.`
1698
1003
  `Fetching ${this._username}'s scheduled reviews${course_id ? ` for course ${course_id}` : ""}.`
1699
1004
  );
1700
1005
  return reviews.rows.filter((r) => {
1701
- if (r.id.startsWith(REVIEW_PREFIX)) {
1702
- const date = moment5.utc(r.id.substr(REVIEW_PREFIX.length), REVIEW_TIME_FORMAT);
1006
+ if (r.id.startsWith(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */])) {
1007
+ const date = moment5.utc(
1008
+ r.id.substr(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */].length),
1009
+ REVIEW_TIME_FORMAT
1010
+ );
1703
1011
  if (targetDate.isAfter(date)) {
1704
1012
  if (course_id === void 0 || r.doc.courseId === course_id) {
1705
1013
  return true;
@@ -1814,7 +1122,8 @@ Currently logged-in as ${this._username}.`
1814
1122
  const defaultConfig = {
1815
1123
  _id: _BaseUser.DOC_IDS.CONFIG,
1816
1124
  darkMode: false,
1817
- likesConfetti: false
1125
+ likesConfetti: false,
1126
+ sessionTimeLimit: 5
1818
1127
  };
1819
1128
  try {
1820
1129
  const cfg = await this.localDB.get(_BaseUser.DOC_IDS.CONFIG);
@@ -1887,7 +1196,8 @@ Currently logged-in as ${this._username}.`
1887
1196
  setDBandQ() {
1888
1197
  this.localDB = getLocalUserDB(this._username);
1889
1198
  this.remoteDB = this.syncStrategy.setupRemoteDB(this._username);
1890
- this.updateQueue = new UpdateQueue(this.localDB);
1199
+ this.writeDB = this.syncStrategy.getWriteDB ? this.syncStrategy.getWriteDB(this._username) : this.localDB;
1200
+ this.updateQueue = new UpdateQueue(this.localDB, this.writeDB);
1891
1201
  }
1892
1202
  async init() {
1893
1203
  _BaseUser._initialized = false;
@@ -2005,8 +1315,8 @@ Currently logged-in as ${this._username}.`
2005
1315
  streak: 0,
2006
1316
  bestInterval: 0
2007
1317
  };
2008
- void this.remoteDB.put(initCardHistory);
2009
- return initCardHistory;
1318
+ const putResult = await this.writeDB.put(initCardHistory);
1319
+ return { ...initCardHistory, _rev: putResult.rev };
2010
1320
  } else {
2011
1321
  throw new Error(`putCardRecord failed because of:
2012
1322
  name:${reason.name}
@@ -2041,7 +1351,7 @@ Currently logged-in as ${this._username}.`
2041
1351
  const deletePromises = duplicateDocIds.map(async (docId) => {
2042
1352
  try {
2043
1353
  const doc = await this.remoteDB.get(docId);
2044
- await this.remoteDB.remove(doc);
1354
+ await this.writeDB.remove(doc);
2045
1355
  log3(`Successfully removed duplicate review: ${docId}`);
2046
1356
  } catch (error) {
2047
1357
  log3(`Failed to remove duplicate review ${docId}: ${error}`);
@@ -2063,7 +1373,7 @@ Currently logged-in as ${this._username}.`
2063
1373
  * @param course_id optional specification of individual course
2064
1374
  */
2065
1375
  async getSeenCards(course_id) {
2066
- let prefix = cardHistoryPrefix2;
1376
+ let prefix = DocTypePrefixes["CARDRECORD" /* CARDRECORD */];
2067
1377
  if (course_id) {
2068
1378
  prefix += course_id;
2069
1379
  }
@@ -2072,8 +1382,8 @@ Currently logged-in as ${this._username}.`
2072
1382
  });
2073
1383
  const ret = [];
2074
1384
  docs.rows.forEach((row) => {
2075
- if (row.id.startsWith(cardHistoryPrefix2)) {
2076
- ret.push(row.id.substr(cardHistoryPrefix2.length));
1385
+ if (row.id.startsWith(DocTypePrefixes["CARDRECORD" /* CARDRECORD */])) {
1386
+ ret.push(row.id.substr(DocTypePrefixes["CARDRECORD" /* CARDRECORD */].length));
2077
1387
  }
2078
1388
  });
2079
1389
  return ret;
@@ -2085,7 +1395,7 @@ Currently logged-in as ${this._username}.`
2085
1395
  async getHistory() {
2086
1396
  const cards = await filterAllDocsByPrefix(
2087
1397
  this.remoteDB,
2088
- cardHistoryPrefix2,
1398
+ DocTypePrefixes["CARDRECORD" /* CARDRECORD */],
2089
1399
  {
2090
1400
  include_docs: true,
2091
1401
  attachments: false
@@ -2126,7 +1436,7 @@ Currently logged-in as ${this._username}.`
2126
1436
  } catch (e) {
2127
1437
  const err = e;
2128
1438
  if (err.status === 404) {
2129
- await this.remoteDB.put({
1439
+ await this.writeDB.put({
2130
1440
  _id: _BaseUser.DOC_IDS.CLASSROOM_REGISTRATIONS,
2131
1441
  registrations: []
2132
1442
  });
@@ -2174,10 +1484,10 @@ Currently logged-in as ${this._username}.`
2174
1484
  }
2175
1485
  }
2176
1486
  async scheduleCardReview(review) {
2177
- return scheduleCardReviewLocal(this.remoteDB, review);
1487
+ return scheduleCardReviewLocal(this.writeDB, review);
2178
1488
  }
2179
1489
  async removeScheduledCardReview(reviewId) {
2180
- return removeScheduledCardReviewLocal(this.remoteDB, reviewId);
1490
+ return removeScheduledCardReviewLocal(this.writeDB, reviewId);
2181
1491
  }
2182
1492
  async registerForClassroom(_classId, _registerAs) {
2183
1493
  return registerUserForClassroom(this._username, _classId, _registerAs);
@@ -2407,18 +1717,21 @@ var init_StaticDataUnpacker = __esm({
2407
1717
  async getTagsIndex() {
2408
1718
  return await this.loadIndex("tags");
2409
1719
  }
1720
+ getDocTypeFromId(id) {
1721
+ for (const docTypeKey in DocTypePrefixes) {
1722
+ const prefix = DocTypePrefixes[docTypeKey];
1723
+ if (id.startsWith(`${prefix}-`)) {
1724
+ return docTypeKey;
1725
+ }
1726
+ }
1727
+ return void 0;
1728
+ }
2410
1729
  /**
2411
1730
  * Find which chunk contains a specific document ID
2412
1731
  */
2413
1732
  async findChunkForDocument(docId) {
2414
- let expectedDocType = void 0;
2415
- for (const docType of Object.values(DocType)) {
2416
- if (docId.startsWith(`${docType}-`)) {
2417
- expectedDocType = docType;
2418
- break;
2419
- }
2420
- }
2421
- if (expectedDocType !== void 0) {
1733
+ const expectedDocType = this.getDocTypeFromId(docId);
1734
+ if (expectedDocType) {
2422
1735
  const typeChunks = this.manifest.chunks.filter((c) => c.docType === expectedDocType);
2423
1736
  for (const chunk of typeChunks) {
2424
1737
  if (docId >= chunk.startKey && docId <= chunk.endKey) {
@@ -2428,21 +1741,8 @@ var init_StaticDataUnpacker = __esm({
2428
1741
  }
2429
1742
  }
2430
1743
  }
2431
- return void 0;
2432
1744
  } else {
2433
- const displayableChunks = this.manifest.chunks.filter(
2434
- (c) => c.docType === "DISPLAYABLE_DATA"
2435
- );
2436
- for (const chunk of displayableChunks) {
2437
- if (docId >= chunk.startKey && docId <= chunk.endKey) {
2438
- const exists = await this.verifyDocumentInChunk(docId, chunk);
2439
- if (exists) {
2440
- return chunk;
2441
- }
2442
- }
2443
- }
2444
- const cardChunks = this.manifest.chunks.filter((c) => c.docType === "CARD");
2445
- for (const chunk of cardChunks) {
1745
+ for (const chunk of this.manifest.chunks) {
2446
1746
  if (docId >= chunk.startKey && docId <= chunk.endKey) {
2447
1747
  const exists = await this.verifyDocumentInChunk(docId, chunk);
2448
1748
  if (exists) {
@@ -2463,6 +1763,7 @@ var init_StaticDataUnpacker = __esm({
2463
1763
  }
2464
1764
  return void 0;
2465
1765
  }
1766
+ return void 0;
2466
1767
  }
2467
1768
  /**
2468
1769
  * Verify that a document actually exists in a specific chunk by loading and checking it
@@ -2706,6 +2007,7 @@ var init_courseDB3 = __esm({
2706
2007
  "use strict";
2707
2008
  init_types_legacy();
2708
2009
  init_navigators();
2010
+ init_logger();
2709
2011
  StaticCourseDB = class {
2710
2012
  constructor(courseId, unpacker, userDB, manifest) {
2711
2013
  this.courseId = courseId;
@@ -2727,10 +2029,11 @@ var init_courseDB3 = __esm({
2727
2029
  throw new Error("Cannot update course config in static mode");
2728
2030
  }
2729
2031
  async getCourseInfo() {
2032
+ const cardCount = this.manifest.chunks.filter((chunk) => chunk.docType === "CARD" /* CARD */).reduce((total, chunk) => total + chunk.documentCount, 0);
2730
2033
  return {
2731
- cardCount: 0,
2732
- // Would come from manifest
2034
+ cardCount,
2733
2035
  registeredUsers: 0
2036
+ // Always 0 in static mode
2734
2037
  };
2735
2038
  }
2736
2039
  async getCourseDoc(id, _options) {
@@ -2819,12 +2122,56 @@ var init_courseDB3 = __esm({
2819
2122
  courseID: this.courseId
2820
2123
  }));
2821
2124
  }
2822
- async getAppliedTags(_cardId) {
2823
- return {
2824
- total_rows: 0,
2825
- offset: 0,
2826
- rows: []
2827
- };
2125
+ async getAppliedTags(cardId) {
2126
+ try {
2127
+ const tagsIndex = await this.unpacker.getTagsIndex();
2128
+ const cardTags = tagsIndex.byCard[cardId] || [];
2129
+ const rows = await Promise.all(
2130
+ cardTags.map(async (tagName) => {
2131
+ const tagId = `${"TAG" /* TAG */}-${tagName}`;
2132
+ try {
2133
+ const tagDoc = await this.unpacker.getDocument(tagId);
2134
+ return {
2135
+ id: tagId,
2136
+ key: cardId,
2137
+ value: {
2138
+ name: tagDoc.name,
2139
+ snippet: tagDoc.snippet,
2140
+ count: tagDoc.taggedCards?.length || 0
2141
+ }
2142
+ };
2143
+ } catch (error) {
2144
+ if (error && error.status === 404) {
2145
+ logger.warn(`Tag document not found for ${tagName}, creating stub`);
2146
+ } else {
2147
+ logger.error(`Error getting tag document for ${tagName}:`, error);
2148
+ throw error;
2149
+ }
2150
+ return {
2151
+ id: tagId,
2152
+ key: cardId,
2153
+ value: {
2154
+ name: tagName,
2155
+ snippet: `Tag: ${tagName}`,
2156
+ count: tagsIndex.byTag[tagName]?.length || 0
2157
+ }
2158
+ };
2159
+ }
2160
+ })
2161
+ );
2162
+ return {
2163
+ total_rows: rows.length,
2164
+ offset: 0,
2165
+ rows
2166
+ };
2167
+ } catch (error) {
2168
+ logger.error(`Error getting applied tags for card ${cardId}:`, error);
2169
+ return {
2170
+ total_rows: 0,
2171
+ offset: 0,
2172
+ rows: []
2173
+ };
2174
+ }
2828
2175
  }
2829
2176
  async addTagToCard(_cardId, _tagId) {
2830
2177
  throw new Error("Cannot modify tags in static mode");
@@ -2842,11 +2189,69 @@ var init_courseDB3 = __esm({
2842
2189
  throw new Error("Cannot update tags in static mode");
2843
2190
  }
2844
2191
  async getCourseTagStubs() {
2845
- return {
2846
- total_rows: 0,
2847
- offset: 0,
2848
- rows: []
2849
- };
2192
+ try {
2193
+ const tagsIndex = await this.unpacker.getTagsIndex();
2194
+ if (!tagsIndex || !tagsIndex.byTag) {
2195
+ logger.warn("Tags index not found or empty");
2196
+ return {
2197
+ total_rows: 0,
2198
+ offset: 0,
2199
+ rows: []
2200
+ };
2201
+ }
2202
+ const tagNames = Object.keys(tagsIndex.byTag);
2203
+ const rows = await Promise.all(
2204
+ tagNames.map(async (tagName) => {
2205
+ const cardIds = tagsIndex.byTag[tagName] || [];
2206
+ const tagId = `${"TAG" /* TAG */}-${tagName}`;
2207
+ try {
2208
+ const tagDoc = await this.unpacker.getDocument(tagId);
2209
+ return {
2210
+ id: tagId,
2211
+ key: tagId,
2212
+ value: { rev: "1-static" },
2213
+ doc: tagDoc
2214
+ };
2215
+ } catch (error) {
2216
+ if (error && error.status === 404) {
2217
+ logger.warn(`Tag document not found for ${tagName}, creating stub`);
2218
+ const stubDoc = {
2219
+ _id: tagId,
2220
+ _rev: "1-static",
2221
+ course: this.courseId,
2222
+ docType: "TAG" /* TAG */,
2223
+ name: tagName,
2224
+ snippet: `Tag: ${tagName}`,
2225
+ wiki: "",
2226
+ taggedCards: cardIds,
2227
+ author: "system"
2228
+ };
2229
+ return {
2230
+ id: tagId,
2231
+ key: tagId,
2232
+ value: { rev: "1-static" },
2233
+ doc: stubDoc
2234
+ };
2235
+ } else {
2236
+ logger.error(`Error getting tag document for ${tagName}:`, error);
2237
+ throw error;
2238
+ }
2239
+ }
2240
+ })
2241
+ );
2242
+ return {
2243
+ total_rows: rows.length,
2244
+ offset: 0,
2245
+ rows
2246
+ };
2247
+ } catch (error) {
2248
+ logger.error("Failed to get course tag stubs:", error);
2249
+ return {
2250
+ total_rows: 0,
2251
+ offset: 0,
2252
+ rows: []
2253
+ };
2254
+ }
2850
2255
  }
2851
2256
  async addNote(_codeCourse, _shape, _data, _author, _tags, _uploads, _elo) {
2852
2257
  return {
@@ -2952,6 +2357,9 @@ var init_NoOpSyncStrategy = __esm({
2952
2357
  setupRemoteDB(username) {
2953
2358
  return getLocalUserDB(username);
2954
2359
  }
2360
+ getWriteDB(username) {
2361
+ return getLocalUserDB(username);
2362
+ }
2955
2363
  startSync(_localDB, _remoteDB) {
2956
2364
  }
2957
2365
  stopSync() {