@vue-skuilder/db 0.1.6 → 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 (70) 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 +825 -762
  6. package/dist/core/index.js.map +1 -1
  7. package/dist/core/index.mjs +812 -750
  8. package/dist/core/index.mjs.map +1 -1
  9. package/dist/{dataLayerProvider-BZmLyBVw.d.mts → dataLayerProvider-BInqI_RF.d.mts} +1 -1
  10. package/dist/{dataLayerProvider-BuntXkCs.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 +2261 -2081
  14. package/dist/impl/couch/index.js.map +1 -1
  15. package/dist/impl/couch/index.mjs +2274 -2095
  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 +524 -1064
  20. package/dist/impl/static/index.js.map +1 -1
  21. package/dist/impl/static/index.mjs +515 -1058
  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 +200 -9
  26. package/dist/index.d.ts +200 -9
  27. package/dist/index.js +4123 -2820
  28. package/dist/index.js.map +1 -1
  29. package/dist/index.mjs +4119 -2830
  30. package/dist/index.mjs.map +1 -1
  31. package/dist/{types-D6SnlHPm.d.ts → types-BefDGkKa.d.ts} +1 -1
  32. package/dist/{types-DPRvCrIk.d.mts → types-DC-ckZug.d.mts} +1 -1
  33. package/dist/{types-legacy-WPe8CtO-.d.mts → types-legacy-Birv-Jx6.d.mts} +2 -2
  34. package/dist/{types-legacy-WPe8CtO-.d.ts → types-legacy-Birv-Jx6.d.ts} +2 -2
  35. package/dist/{userDB-D9EuWTp1.d.ts → userDB-C33Hzjgn.d.mts} +11 -4
  36. package/dist/{userDB-31gsvxyd.d.mts → userDB-DusL7OXe.d.ts} +11 -4
  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/factory.ts +25 -0
  48. package/src/impl/common/BaseUserDB.ts +62 -28
  49. package/src/impl/common/SyncStrategy.ts +7 -0
  50. package/src/impl/common/index.ts +0 -1
  51. package/src/impl/common/userDBHelpers.ts +15 -5
  52. package/src/impl/couch/CouchDBSyncStrategy.ts +10 -0
  53. package/src/impl/couch/courseAPI.ts +7 -6
  54. package/src/impl/couch/courseLookupDB.ts +24 -0
  55. package/src/impl/couch/index.ts +10 -5
  56. package/src/impl/couch/updateQueue.ts +12 -8
  57. package/src/impl/couch/user-course-relDB.ts +17 -27
  58. package/src/impl/static/NoOpSyncStrategy.ts +5 -0
  59. package/src/impl/static/StaticDataUnpacker.ts +18 -36
  60. package/src/impl/static/courseDB.ts +135 -17
  61. package/src/util/dataDirectory.test.ts +53 -0
  62. package/src/util/dataDirectory.ts +52 -0
  63. package/src/util/index.ts +3 -0
  64. package/src/util/migrator/FileSystemAdapter.ts +79 -0
  65. package/src/util/migrator/StaticToCouchDBMigrator.ts +713 -0
  66. package/src/util/migrator/index.ts +18 -0
  67. package/src/util/migrator/types.ts +84 -0
  68. package/src/util/migrator/validation.ts +517 -0
  69. package/src/util/packer/CouchDBToStaticPacker.ts +92 -2
  70. package/src/util/tuiLogger.ts +139 -0
@@ -5,10 +5,10 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __glob = (map) => (path) => {
9
- var fn = map[path];
8
+ var __glob = (map) => (path2) => {
9
+ var fn = map[path2];
10
10
  if (fn) return fn();
11
- throw new Error("Module not found in bundle: " + path);
11
+ throw new Error("Module not found in bundle: " + path2);
12
12
  };
13
13
  var __esm = (fn, res) => function __init() {
14
14
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
@@ -35,6 +35,27 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
35
35
  ));
36
36
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
37
37
 
38
+ // src/impl/common/SyncStrategy.ts
39
+ var init_SyncStrategy = __esm({
40
+ "src/impl/common/SyncStrategy.ts"() {
41
+ "use strict";
42
+ }
43
+ });
44
+
45
+ // src/core/interfaces/adminDB.ts
46
+ var init_adminDB = __esm({
47
+ "src/core/interfaces/adminDB.ts"() {
48
+ "use strict";
49
+ }
50
+ });
51
+
52
+ // src/core/interfaces/classroomDB.ts
53
+ var init_classroomDB = __esm({
54
+ "src/core/interfaces/classroomDB.ts"() {
55
+ "use strict";
56
+ }
57
+ });
58
+
38
59
  // src/util/logger.ts
39
60
  var isDevelopment, logger;
40
61
  var init_logger = __esm({
@@ -80,40 +101,6 @@ var init_logger = __esm({
80
101
  }
81
102
  });
82
103
 
83
- // src/factory.ts
84
- function getDataLayer() {
85
- if (!dataLayerInstance) {
86
- throw new Error("Data layer not initialized. Call initializeDataLayer first.");
87
- }
88
- return dataLayerInstance;
89
- }
90
- var ENV, dataLayerInstance;
91
- var init_factory = __esm({
92
- "src/factory.ts"() {
93
- "use strict";
94
- init_logger();
95
- ENV = {
96
- COUCHDB_SERVER_PROTOCOL: "NOT_SET",
97
- COUCHDB_SERVER_URL: "NOT_SET"
98
- };
99
- dataLayerInstance = null;
100
- }
101
- });
102
-
103
- // src/core/types/types-legacy.ts
104
- var GuestUsername, log, cardHistoryPrefix;
105
- var init_types_legacy = __esm({
106
- "src/core/types/types-legacy.ts"() {
107
- "use strict";
108
- init_logger();
109
- GuestUsername = "Guest";
110
- log = (message) => {
111
- logger.log(message);
112
- };
113
- cardHistoryPrefix = "cardH";
114
- }
115
- });
116
-
117
104
  // src/impl/couch/pouchdb-setup.ts
118
105
  var import_pouchdb, import_pouchdb_find, import_pouchdb_authentication, pouchdb_setup_default;
119
106
  var init_pouchdb_setup = __esm({
@@ -160,7 +147,10 @@ var init_updateQueue = __esm({
160
147
  _className = "UpdateQueue";
161
148
  pendingUpdates = {};
162
149
  inprogressUpdates = {};
163
- db;
150
+ readDB;
151
+ // Database for read operations
152
+ writeDB;
153
+ // Database for write operations (local-first)
164
154
  update(id, update) {
165
155
  logger.debug(`Update requested on doc: ${id}`);
166
156
  if (this.pendingUpdates[id]) {
@@ -170,24 +160,25 @@ var init_updateQueue = __esm({
170
160
  }
171
161
  return this.applyUpdates(id);
172
162
  }
173
- constructor(db) {
163
+ constructor(readDB, writeDB) {
174
164
  super();
175
- this.db = db;
165
+ this.readDB = readDB;
166
+ this.writeDB = writeDB || readDB;
176
167
  logger.debug(`UpdateQ initialized...`);
177
- void this.db.info().then((i) => {
168
+ void this.readDB.info().then((i) => {
178
169
  logger.debug(`db info: ${JSON.stringify(i)}`);
179
170
  });
180
171
  }
181
172
  async applyUpdates(id) {
182
173
  logger.debug(`Applying updates on doc: ${id}`);
183
174
  if (this.inprogressUpdates[id]) {
184
- await this.db.info();
175
+ await this.readDB.info();
185
176
  return this.applyUpdates(id);
186
177
  } else {
187
178
  if (this.pendingUpdates[id] && this.pendingUpdates[id].length > 0) {
188
179
  this.inprogressUpdates[id] = true;
189
180
  try {
190
- let doc = await this.db.get(id);
181
+ let doc = await this.readDB.get(id);
191
182
  logger.debug(`Retrieved doc: ${id}`);
192
183
  while (this.pendingUpdates[id].length !== 0) {
193
184
  const update = this.pendingUpdates[id].splice(0, 1)[0];
@@ -200,7 +191,7 @@ var init_updateQueue = __esm({
200
191
  };
201
192
  }
202
193
  }
203
- await this.db.put(doc);
194
+ await this.writeDB.put(doc);
204
195
  logger.debug(`Put doc: ${id}`);
205
196
  if (this.pendingUpdates[id].length === 0) {
206
197
  this.inprogressUpdates[id] = false;
@@ -226,6 +217,32 @@ var init_updateQueue = __esm({
226
217
  }
227
218
  });
228
219
 
220
+ // src/core/types/types-legacy.ts
221
+ var GuestUsername, log, DocTypePrefixes;
222
+ var init_types_legacy = __esm({
223
+ "src/core/types/types-legacy.ts"() {
224
+ "use strict";
225
+ init_logger();
226
+ GuestUsername = "Guest";
227
+ log = (message) => {
228
+ logger.log(message);
229
+ };
230
+ DocTypePrefixes = {
231
+ ["CARD" /* CARD */]: "c",
232
+ ["DISPLAYABLE_DATA" /* DISPLAYABLE_DATA */]: "dd",
233
+ ["TAG" /* TAG */]: "TAG",
234
+ ["CARDRECORD" /* CARDRECORD */]: "cardH",
235
+ ["SCHEDULED_CARD" /* SCHEDULED_CARD */]: "card_review_",
236
+ // Add other doctypes here as they get prefixed IDs
237
+ ["DATASHAPE" /* DATASHAPE */]: "DATASHAPE",
238
+ ["QUESTION" /* QUESTIONTYPE */]: "QUESTION",
239
+ ["VIEW" /* VIEW */]: "VIEW",
240
+ ["PEDAGOGY" /* PEDAGOGY */]: "PEDAGOGY",
241
+ ["NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */]: "NAVIGATION_STRATEGY"
242
+ };
243
+ }
244
+ });
245
+
229
246
  // src/impl/couch/clientCache.ts
230
247
  async function GET_CACHED(k, f) {
231
248
  if (CLIENT_CACHE[k]) {
@@ -245,2288 +262,2447 @@ var init_clientCache = __esm({
245
262
  }
246
263
  });
247
264
 
248
- // src/impl/common/SyncStrategy.ts
249
- var init_SyncStrategy = __esm({
250
- "src/impl/common/SyncStrategy.ts"() {
251
- "use strict";
265
+ // src/impl/couch/courseAPI.ts
266
+ async function addNote55(courseID, codeCourse, shape, data, author, tags, uploads, elo = (0, import_common2.blankCourseElo)()) {
267
+ const db = getCourseDB(courseID);
268
+ const payload = (0, import_common3.prepareNote55)(courseID, codeCourse, shape, data, author, tags, uploads);
269
+ const _id = `${DocTypePrefixes["DISPLAYABLE_DATA" /* DISPLAYABLE_DATA */]}-${(0, import_uuid.v4)()}`;
270
+ const result = await db.put({ ...payload, _id });
271
+ const dataShapeId = import_common.NameSpacer.getDataShapeString({
272
+ course: codeCourse,
273
+ dataShape: shape.name
274
+ });
275
+ if (result.ok) {
276
+ try {
277
+ await createCards(courseID, dataShapeId, result.id, tags, elo, author);
278
+ } catch (error) {
279
+ let errorMessage = "Unknown error";
280
+ if (error instanceof Error) {
281
+ errorMessage = error.message;
282
+ } else if (error && typeof error === "object" && "reason" in error) {
283
+ errorMessage = error.reason;
284
+ } else if (error && typeof error === "object" && "message" in error) {
285
+ errorMessage = error.message;
286
+ } else {
287
+ errorMessage = String(error);
288
+ }
289
+ logger.error(`[addNote55] Failed to create cards for note ${result.id}: ${errorMessage}`);
290
+ result.cardCreationFailed = true;
291
+ result.cardCreationError = errorMessage;
292
+ }
293
+ } else {
294
+ logger.error(`[addNote55] Error adding note. Result: ${JSON.stringify(result)}`);
252
295
  }
253
- });
254
-
255
- // src/core/util/index.ts
256
- function getCardHistoryID(courseID, cardID) {
257
- return `${cardHistoryPrefix}-${courseID}-${cardID}`;
296
+ return result;
258
297
  }
259
- var init_util = __esm({
260
- "src/core/util/index.ts"() {
261
- "use strict";
262
- init_types_legacy();
298
+ async function createCards(courseID, datashapeID, noteID, tags, elo = (0, import_common2.blankCourseElo)(), author) {
299
+ const cfg = await getCredentialledCourseConfig(courseID);
300
+ const dsDescriptor = import_common.NameSpacer.getDataShapeDescriptor(datashapeID);
301
+ let questionViewTypes = [];
302
+ for (const ds of cfg.dataShapes) {
303
+ if (ds.name === datashapeID) {
304
+ questionViewTypes = ds.questionTypes;
305
+ }
263
306
  }
264
- });
265
-
266
- // src/impl/common/userDBHelpers.ts
267
- function hexEncode(str) {
268
- let hex;
269
- let returnStr = "";
270
- for (let i = 0; i < str.length; i++) {
271
- hex = str.charCodeAt(i).toString(16);
272
- returnStr += ("000" + hex).slice(3);
307
+ if (questionViewTypes.length === 0) {
308
+ const errorMsg = `No questionViewTypes found for datashapeID: ${datashapeID} in course config. Cards cannot be created.`;
309
+ logger.error(errorMsg);
310
+ throw new Error(errorMsg);
273
311
  }
274
- return returnStr;
275
- }
276
- function filterAllDocsByPrefix(db, prefix, opts) {
277
- const options = {
278
- startkey: prefix,
279
- endkey: prefix + "\uFFF0",
280
- include_docs: true
281
- };
282
- if (opts) {
283
- Object.assign(options, opts);
312
+ for (const questionView of questionViewTypes) {
313
+ await createCard(questionView, courseID, dsDescriptor, noteID, tags, elo, author);
284
314
  }
285
- return db.allDocs(options);
286
315
  }
287
- function getStartAndEndKeys(key) {
288
- return {
289
- startkey: key,
290
- endkey: key + "\uFFF0"
291
- };
316
+ async function createCard(questionViewName, courseID, dsDescriptor, noteID, tags, elo = (0, import_common2.blankCourseElo)(), author) {
317
+ const qDescriptor = import_common.NameSpacer.getQuestionDescriptor(questionViewName);
318
+ const cfg = await getCredentialledCourseConfig(courseID);
319
+ for (const rQ of cfg.questionTypes) {
320
+ if (rQ.name === questionViewName) {
321
+ for (const view of rQ.viewList) {
322
+ await addCard(
323
+ courseID,
324
+ dsDescriptor.course,
325
+ [noteID],
326
+ import_common.NameSpacer.getViewString({
327
+ course: qDescriptor.course,
328
+ questionType: qDescriptor.questionType,
329
+ view
330
+ }),
331
+ elo,
332
+ tags,
333
+ author
334
+ );
335
+ }
336
+ }
337
+ }
292
338
  }
293
- function updateGuestAccountExpirationDate(guestDB) {
294
- const currentTime = import_moment.default.utc();
295
- const expirationDate = currentTime.add(2, "months").toISOString();
296
- const expiryDocID2 = "GuestAccountExpirationDate";
297
- void guestDB.get(expiryDocID2).then((doc) => {
298
- return guestDB.put({
299
- _id: expiryDocID2,
300
- _rev: doc._rev,
301
- date: expirationDate
302
- });
303
- }).catch(() => {
304
- return guestDB.put({
305
- _id: expiryDocID2,
306
- date: expirationDate
307
- });
339
+ async function addCard(courseID, course, id_displayable_data, id_view, elo, tags, author) {
340
+ const db = getCourseDB(courseID);
341
+ const _id = `${DocTypePrefixes["CARD" /* CARD */]}-${(0, import_uuid.v4)()}`;
342
+ const card = await db.put({
343
+ _id,
344
+ course,
345
+ id_displayable_data,
346
+ id_view,
347
+ docType: "CARD" /* CARD */,
348
+ elo: elo || (0, import_common2.toCourseElo)(990 + Math.round(20 * Math.random())),
349
+ author
308
350
  });
351
+ for (const tag of tags) {
352
+ logger.info(`adding tag: ${tag} to card ${card.id}`);
353
+ await addTagToCard(courseID, card.id, tag, author, false);
354
+ }
355
+ return card;
309
356
  }
310
- function getLocalUserDB(username) {
311
- return new pouchdb_setup_default(`userdb-${username}`, {});
357
+ async function getCredentialledCourseConfig(courseID) {
358
+ try {
359
+ const db = getCourseDB(courseID);
360
+ const ret = await db.get("CourseConfig");
361
+ ret.courseID = courseID;
362
+ logger.info(`Returning course config: ${JSON.stringify(ret)}`);
363
+ return ret;
364
+ } catch (e) {
365
+ logger.error(`Error fetching config for ${courseID}:`, e);
366
+ throw e;
367
+ }
312
368
  }
313
- function scheduleCardReviewLocal(userDB, review) {
314
- const now = import_moment.default.utc();
315
- logger.info(`Scheduling for review in: ${review.time.diff(now, "h") / 24} days`);
316
- void userDB.put({
317
- _id: REVIEW_PREFIX + review.time.format(REVIEW_TIME_FORMAT),
318
- cardId: review.card_id,
319
- reviewTime: review.time,
320
- courseId: review.course_id,
321
- scheduledAt: now,
322
- scheduledFor: review.scheduledFor,
323
- schedulingAgentId: review.schedulingAgentId
369
+ async function addTagToCard(courseID, cardID, tagID, author, updateELO = true) {
370
+ const prefixedTagID = getTagID(tagID);
371
+ const courseDB = getCourseDB(courseID);
372
+ const courseApi = new CourseDB(courseID, async () => {
373
+ const dummySyncStrategy = {
374
+ setupRemoteDB: () => null,
375
+ startSync: () => {
376
+ },
377
+ canCreateAccount: () => false,
378
+ canAuthenticate: () => false,
379
+ getCurrentUsername: async () => "DummyUser"
380
+ };
381
+ return BaseUser.Dummy(dummySyncStrategy);
324
382
  });
325
- }
326
- async function removeScheduledCardReviewLocal(userDB, reviewDocID) {
327
- const reviewDoc = await userDB.get(reviewDocID);
328
- userDB.remove(reviewDoc).then((res) => {
329
- if (res.ok) {
330
- log2(`Removed Review Doc: ${reviewDocID}`);
383
+ try {
384
+ logger.info(`Applying tag ${tagID} to card ${courseID + "-" + cardID}...`);
385
+ const tag = await courseDB.get(prefixedTagID);
386
+ if (!tag.taggedCards.includes(cardID)) {
387
+ tag.taggedCards.push(cardID);
388
+ if (updateELO) {
389
+ try {
390
+ const eloData = await courseApi.getCardEloData([cardID]);
391
+ const elo = eloData[0];
392
+ elo.tags[tagID] = {
393
+ count: 0,
394
+ score: elo.global.score
395
+ // todo: or 1000?
396
+ };
397
+ await updateCardElo(courseID, cardID, elo);
398
+ } catch (error) {
399
+ logger.error("Failed to update ELO data for card:", cardID, error);
400
+ }
401
+ }
402
+ return courseDB.put(tag);
403
+ } else throw new AlreadyTaggedErr(`Card ${cardID} is already tagged with ${tagID}`);
404
+ } catch (e) {
405
+ if (e instanceof AlreadyTaggedErr) {
406
+ throw e;
331
407
  }
332
- }).catch((err) => {
333
- log2(`Failed to remove Review Doc: ${reviewDocID},
334
- ${JSON.stringify(err)}`);
335
- });
408
+ await createTag(courseID, tagID, author);
409
+ return addTagToCard(courseID, cardID, tagID, author, updateELO);
410
+ }
336
411
  }
337
- var import_moment, REVIEW_PREFIX, REVIEW_TIME_FORMAT, log2;
338
- var init_userDBHelpers = __esm({
339
- "src/impl/common/userDBHelpers.ts"() {
340
- "use strict";
341
- import_moment = __toESM(require("moment"));
342
- init_logger();
412
+ async function updateCardElo(courseID, cardID, elo) {
413
+ if (elo) {
414
+ const cDB = getCourseDB(courseID);
415
+ const card = await cDB.get(cardID);
416
+ logger.debug(`Replacing ${JSON.stringify(card.elo)} with ${JSON.stringify(elo)}`);
417
+ card.elo = elo;
418
+ return cDB.put(card);
419
+ }
420
+ }
421
+ function getTagID(tagName) {
422
+ const tagPrefix = "TAG" /* TAG */.valueOf() + "-";
423
+ if (tagName.indexOf(tagPrefix) === 0) {
424
+ return tagName;
425
+ } else {
426
+ return tagPrefix + tagName;
427
+ }
428
+ }
429
+ function getCourseDB(courseID) {
430
+ const dbName = `coursedb-${courseID}`;
431
+ return new pouchdb_setup_default(
432
+ ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + dbName,
433
+ pouchDBincludeCredentialsConfig
434
+ );
435
+ }
436
+ var import_common, import_common2, import_common3, import_uuid, AlreadyTaggedErr;
437
+ var init_courseAPI = __esm({
438
+ "src/impl/couch/courseAPI.ts"() {
439
+ "use strict";
343
440
  init_pouchdb_setup();
344
- REVIEW_PREFIX = "card_review_";
345
- REVIEW_TIME_FORMAT = "YYYY-MM-DD--kk:mm:ss-SSS";
346
- log2 = (s) => {
347
- logger.info(s);
441
+ init_couch();
442
+ init_factory();
443
+ import_common = require("@vue-skuilder/common");
444
+ import_common2 = require("@vue-skuilder/common");
445
+ init_courseDB();
446
+ init_types_legacy();
447
+ import_common3 = require("@vue-skuilder/common");
448
+ init_common();
449
+ init_logger();
450
+ import_uuid = require("uuid");
451
+ AlreadyTaggedErr = class extends Error {
452
+ constructor(message) {
453
+ super(message);
454
+ this.name = "AlreadyTaggedErr";
455
+ }
348
456
  };
349
457
  }
350
458
  });
351
459
 
352
- // src/impl/couch/user-course-relDB.ts
353
- var import_moment2, UsrCrsData;
354
- var init_user_course_relDB = __esm({
355
- "src/impl/couch/user-course-relDB.ts"() {
460
+ // src/impl/couch/courseLookupDB.ts
461
+ var courseLookupDBTitle, CourseLookup;
462
+ var init_courseLookupDB = __esm({
463
+ "src/impl/couch/courseLookupDB.ts"() {
356
464
  "use strict";
357
- import_moment2 = __toESM(require("moment"));
358
- init_couch();
359
- init_courseDB();
465
+ init_pouchdb_setup();
466
+ init_factory();
360
467
  init_logger();
361
- UsrCrsData = class {
362
- user;
363
- course;
364
- _courseId;
365
- constructor(user, courseId) {
366
- this.user = user;
367
- this.course = new CourseDB(courseId, async () => this.user);
368
- this._courseId = courseId;
468
+ courseLookupDBTitle = "coursedb-lookup";
469
+ logger.debug(`COURSELOOKUP FILE RUNNING`);
470
+ CourseLookup = class _CourseLookup {
471
+ // [ ] this db should be read only for public, admin-only for write
472
+ // Cache for the PouchDB instance
473
+ static _dbInstance = null;
474
+ /**
475
+ * Static getter for the PouchDB database instance.
476
+ * Connects using ENV variables and caches the instance.
477
+ * Throws an error if required ENV variables are not set.
478
+ */
479
+ static get _db() {
480
+ if (this._dbInstance) {
481
+ return this._dbInstance;
482
+ }
483
+ if (ENV.COUCHDB_SERVER_URL === "NOT_SET" || !ENV.COUCHDB_SERVER_URL) {
484
+ throw new Error(
485
+ "CourseLookup.db: COUCHDB_SERVER_URL is not set. Ensure initializeDataLayer has been called with valid configuration."
486
+ );
487
+ }
488
+ if (ENV.COUCHDB_SERVER_PROTOCOL === "NOT_SET" || !ENV.COUCHDB_SERVER_PROTOCOL) {
489
+ throw new Error(
490
+ "CourseLookup.db: COUCHDB_SERVER_PROTOCOL is not set. Ensure initializeDataLayer has been called with valid configuration."
491
+ );
492
+ }
493
+ const dbUrl = `${ENV.COUCHDB_SERVER_PROTOCOL}://${ENV.COUCHDB_SERVER_URL}/${courseLookupDBTitle}`;
494
+ const options = {
495
+ skip_setup: true
496
+ // Keep the original option
497
+ // fetch: (url, opts) => { // Optional: Add for debugging network requests
498
+ // console.log('PouchDB fetch:', url, opts);
499
+ // return pouch.fetch(url, opts);
500
+ // }
501
+ };
502
+ if (ENV.COUCHDB_USERNAME && ENV.COUCHDB_PASSWORD) {
503
+ options.auth = {
504
+ username: ENV.COUCHDB_USERNAME,
505
+ password: ENV.COUCHDB_PASSWORD
506
+ };
507
+ logger.info(`CourseLookup: Connecting to ${dbUrl} with authentication.`);
508
+ } else {
509
+ logger.info(`CourseLookup: Connecting to ${dbUrl} without authentication.`);
510
+ }
511
+ try {
512
+ this._dbInstance = new pouchdb_setup_default(dbUrl, options);
513
+ logger.info(`CourseLookup: Database instance created for ${courseLookupDBTitle}.`);
514
+ return this._dbInstance;
515
+ } catch (error) {
516
+ logger.error(`CourseLookup: Failed to create PouchDB instance for ${dbUrl}`, error);
517
+ this._dbInstance = null;
518
+ throw new Error(
519
+ `CourseLookup: Failed to initialize database connection: ${error instanceof Error ? error.message : String(error)}`
520
+ );
521
+ }
369
522
  }
370
- async getReviewsForcast(daysCount) {
371
- const time = import_moment2.default.utc().add(daysCount, "days");
372
- return this.getReviewstoDate(time);
523
+ /**
524
+ * Adds a new course to the lookup database, and returns the courseID
525
+ * @param courseName
526
+ * @returns
527
+ */
528
+ static async add(courseName) {
529
+ const resp = await _CourseLookup._db.post({
530
+ name: courseName
531
+ });
532
+ return resp.id;
373
533
  }
374
- async getPendingReviews() {
375
- const now = import_moment2.default.utc();
376
- return this.getReviewstoDate(now);
534
+ /**
535
+ * Adds a new course to the lookup database with a specific courseID
536
+ * @param courseId The specific course ID to use
537
+ * @param courseName The course name
538
+ * @param disambiguator Optional disambiguator
539
+ * @returns Promise<void>
540
+ */
541
+ static async addWithId(courseId, courseName, disambiguator) {
542
+ const doc = {
543
+ _id: courseId,
544
+ name: courseName
545
+ };
546
+ if (disambiguator) {
547
+ doc.disambiguator = disambiguator;
548
+ }
549
+ await _CourseLookup._db.put(doc);
377
550
  }
378
- async getScheduledReviewCount() {
379
- return (await this.getPendingReviews()).length;
551
+ /**
552
+ * Removes a course from the index
553
+ * @param courseID
554
+ */
555
+ static async delete(courseID) {
556
+ const doc = await _CourseLookup._db.get(courseID);
557
+ return await _CourseLookup._db.remove(doc);
380
558
  }
381
- async getCourseSettings() {
382
- const regDoc = await this.user.getCourseRegistrationsDoc();
383
- const crsDoc = regDoc.courses.find((c) => c.courseID === this._courseId);
384
- if (crsDoc && crsDoc.settings) {
385
- return crsDoc.settings;
386
- } else {
387
- logger.warn(`no settings found during lookup on course ${this._courseId}`);
388
- return {};
559
+ static async allCourses() {
560
+ const resp = await _CourseLookup._db.allDocs({
561
+ include_docs: true
562
+ });
563
+ return resp.rows.map((row) => row.doc);
564
+ }
565
+ static async updateDisambiguator(courseID, disambiguator) {
566
+ const doc = await _CourseLookup._db.get(courseID);
567
+ doc.disambiguator = disambiguator;
568
+ return await _CourseLookup._db.put(doc);
569
+ }
570
+ static async isCourse(courseID) {
571
+ try {
572
+ await _CourseLookup._db.get(courseID);
573
+ return true;
574
+ } catch (error) {
575
+ logger.info(`Courselookup failed:`, error);
576
+ return false;
389
577
  }
390
578
  }
391
- updateCourseSettings(updates) {
392
- void this.user.updateCourseSettings(this._courseId, updates);
579
+ };
580
+ }
581
+ });
582
+
583
+ // src/core/navigators/elo.ts
584
+ var elo_exports = {};
585
+ __export(elo_exports, {
586
+ default: () => ELONavigator
587
+ });
588
+ var ELONavigator;
589
+ var init_elo = __esm({
590
+ "src/core/navigators/elo.ts"() {
591
+ "use strict";
592
+ init_navigators();
593
+ ELONavigator = class extends ContentNavigator {
594
+ user;
595
+ course;
596
+ constructor(user, course) {
597
+ super();
598
+ this.user = user;
599
+ this.course = course;
393
600
  }
394
- async getReviewstoDate(targetDate) {
395
- const keys = getStartAndEndKeys2(REVIEW_PREFIX2);
396
- const reviews = await this.user.remote().allDocs({
397
- startkey: keys.startkey,
398
- endkey: keys.endkey,
399
- include_docs: true
601
+ async getPendingReviews() {
602
+ const reviews = await this.user.getPendingReviews(this.course.getCourseID());
603
+ const elo = await this.course.getCardEloData(reviews.map((r) => r.cardId));
604
+ const ratedReviews = reviews.map((r, i) => {
605
+ const ratedR = {
606
+ ...r,
607
+ ...elo[i]
608
+ };
609
+ return ratedR;
400
610
  });
401
- logger.debug(
402
- `Fetching ${this.user.getUsername()}'s scheduled reviews for course ${this._courseId}.`
403
- );
404
- return reviews.rows.filter((r) => {
405
- if (r.id.startsWith(REVIEW_PREFIX2)) {
406
- const date = import_moment2.default.utc(r.id.substr(REVIEW_PREFIX2.length), REVIEW_TIME_FORMAT2);
407
- if (targetDate.isAfter(date)) {
408
- if (this._courseId === void 0 || r.doc.courseId === this._courseId) {
409
- return true;
410
- }
411
- }
611
+ ratedReviews.sort((a, b) => {
612
+ return a.global.score - b.global.score;
613
+ });
614
+ return ratedReviews.map((r) => {
615
+ return {
616
+ ...r,
617
+ contentSourceType: "course",
618
+ contentSourceID: this.course.getCourseID(),
619
+ cardID: r.cardId,
620
+ courseID: r.courseId,
621
+ qualifiedID: `${r.courseId}-${r.cardId}`,
622
+ reviewID: r._id,
623
+ status: "review"
624
+ };
625
+ });
626
+ }
627
+ async getNewCards(limit = 99) {
628
+ const activeCards = await this.user.getActiveCards();
629
+ return (await this.course.getCardsCenteredAtELO({ limit, elo: "user" }, (c) => {
630
+ if (activeCards.some((ac) => c.includes(ac))) {
631
+ return false;
632
+ } else {
633
+ return true;
412
634
  }
413
- }).map((r) => r.doc);
635
+ })).map((c) => {
636
+ return {
637
+ ...c,
638
+ status: "new"
639
+ };
640
+ });
414
641
  }
415
642
  };
416
643
  }
417
644
  });
418
645
 
419
- // src/impl/common/BaseUserDB.ts
420
- async function getOrCreateClassroomRegistrationsDoc(user) {
421
- let ret;
422
- try {
423
- ret = await getLocalUserDB(user).get(userClassroomsDoc);
424
- } catch (e) {
425
- const err = e;
426
- if (err.status === 404) {
427
- await getLocalUserDB(user).put({
428
- _id: userClassroomsDoc,
429
- registrations: []
430
- });
431
- ret = await getOrCreateClassroomRegistrationsDoc(user);
432
- } else {
433
- const errorDetails = {
434
- name: err.name,
435
- status: err.status,
436
- message: err.message,
437
- reason: err.reason,
438
- error: err.error
439
- };
440
- logger.error(
441
- "Database error in getOrCreateClassroomRegistrationsDoc (standalone function):",
442
- errorDetails
443
- );
444
- throw new Error(
445
- `Database error accessing classroom registrations: ${err.message || err.name || "Unknown error"} (status: ${err.status})`
446
- );
447
- }
646
+ // import("./**/*") in src/core/navigators/index.ts
647
+ var globImport;
648
+ var init_ = __esm({
649
+ 'import("./**/*") in src/core/navigators/index.ts'() {
650
+ globImport = __glob({
651
+ "./elo.ts": () => Promise.resolve().then(() => (init_elo(), elo_exports)),
652
+ "./index.ts": () => Promise.resolve().then(() => (init_navigators(), navigators_exports))
653
+ });
448
654
  }
449
- return ret;
655
+ });
656
+
657
+ // src/core/navigators/index.ts
658
+ var navigators_exports = {};
659
+ __export(navigators_exports, {
660
+ ContentNavigator: () => ContentNavigator,
661
+ Navigators: () => Navigators
662
+ });
663
+ var Navigators, ContentNavigator;
664
+ var init_navigators = __esm({
665
+ "src/core/navigators/index.ts"() {
666
+ "use strict";
667
+ init_logger();
668
+ init_();
669
+ Navigators = /* @__PURE__ */ ((Navigators2) => {
670
+ Navigators2["ELO"] = "elo";
671
+ return Navigators2;
672
+ })(Navigators || {});
673
+ ContentNavigator = class {
674
+ /**
675
+ *
676
+ * @param user
677
+ * @param strategyData
678
+ * @returns the runtime object used to steer a study session.
679
+ */
680
+ static async create(user, course, strategyData) {
681
+ const implementingClass = strategyData.implementingClass;
682
+ let NavigatorImpl;
683
+ const variations = ["", ".js", ".ts"];
684
+ for (const ext of variations) {
685
+ try {
686
+ const module2 = await globImport(`./${implementingClass}${ext}`);
687
+ NavigatorImpl = module2.default;
688
+ break;
689
+ } catch (e) {
690
+ logger.debug(`Failed to load with extension ${ext}:`, e);
691
+ }
692
+ }
693
+ if (!NavigatorImpl) {
694
+ throw new Error(`Could not load navigator implementation for: ${implementingClass}`);
695
+ }
696
+ return new NavigatorImpl(user, course, strategyData);
697
+ }
698
+ };
699
+ }
700
+ });
701
+
702
+ // src/impl/couch/courseDB.ts
703
+ function randIntWeightedTowardZero(n) {
704
+ return Math.floor(Math.random() * Math.random() * Math.random() * n);
450
705
  }
451
- async function getOrCreateCourseRegistrationsDoc(user) {
452
- let ret;
453
- try {
454
- ret = await getLocalUserDB(user).get(userCoursesDoc);
455
- } catch (e) {
456
- const err = e;
457
- if (err.status === 404) {
458
- await getLocalUserDB(user).put({
459
- _id: userCoursesDoc,
460
- courses: [],
461
- studyWeight: {}
462
- });
463
- ret = await getOrCreateCourseRegistrationsDoc(user);
464
- } else {
465
- throw new Error(
466
- `Unexpected error ${JSON.stringify(e)} in getOrCreateCourseRegistrationDoc...`
467
- );
468
- }
706
+ async function getCourseDataShapes(courseID) {
707
+ const cfg = await getCredentialledCourseConfig(courseID);
708
+ return cfg.dataShapes;
709
+ }
710
+ async function getCredentialledDataShapes(courseID) {
711
+ const cfg = await getCredentialledCourseConfig(courseID);
712
+ return cfg.dataShapes;
713
+ }
714
+ async function getCourseQuestionTypes(courseID) {
715
+ const cfg = await getCredentialledCourseConfig(courseID);
716
+ return cfg.questionTypes;
717
+ }
718
+ async function getCourseTagStubs(courseID) {
719
+ logger.debug(`Getting tag stubs for course: ${courseID}`);
720
+ const stubs = await filterAllDocsByPrefix(
721
+ getCourseDB2(courseID),
722
+ "TAG" /* TAG */.valueOf() + "-"
723
+ );
724
+ stubs.rows.forEach((row) => {
725
+ logger.debug(` Tag stub for doc: ${row.id}`);
726
+ });
727
+ return stubs;
728
+ }
729
+ async function deleteTag(courseID, tagName) {
730
+ tagName = getTagID(tagName);
731
+ const courseDB = getCourseDB2(courseID);
732
+ const doc = await courseDB.get("TAG" /* TAG */.valueOf() + "-" + tagName);
733
+ const resp = await courseDB.remove(doc);
734
+ return resp;
735
+ }
736
+ async function createTag(courseID, tagName, author) {
737
+ logger.debug(`Creating tag: ${tagName}...`);
738
+ const tagID = getTagID(tagName);
739
+ const courseDB = getCourseDB2(courseID);
740
+ const resp = await courseDB.put({
741
+ course: courseID,
742
+ docType: "TAG" /* TAG */,
743
+ name: tagName,
744
+ snippet: "",
745
+ taggedCards: [],
746
+ wiki: "",
747
+ author,
748
+ _id: tagID
749
+ });
750
+ return resp;
751
+ }
752
+ async function updateTag(tag) {
753
+ const prior = await getTag(tag.course, tag.name);
754
+ return await getCourseDB2(tag.course).put({
755
+ ...tag,
756
+ _rev: prior._rev
757
+ });
758
+ }
759
+ async function getTag(courseID, tagName) {
760
+ const tagID = getTagID(tagName);
761
+ const courseDB = getCourseDB2(courseID);
762
+ return courseDB.get(tagID);
763
+ }
764
+ async function removeTagFromCard(courseID, cardID, tagID) {
765
+ tagID = getTagID(tagID);
766
+ const courseDB = getCourseDB2(courseID);
767
+ const tag = await courseDB.get(tagID);
768
+ tag.taggedCards = tag.taggedCards.filter((taggedID) => {
769
+ return cardID !== taggedID;
770
+ });
771
+ return courseDB.put(tag);
772
+ }
773
+ function getAncestorTagIDs(courseID, tagID) {
774
+ tagID = getTagID(tagID);
775
+ const split = tagID.split(">");
776
+ if (split.length === 1) {
777
+ return [];
778
+ } else {
779
+ split.pop();
780
+ const parent = split.join(">");
781
+ return [parent].concat(getAncestorTagIDs(courseID, parent));
469
782
  }
470
- return ret;
471
783
  }
472
- async function updateUserElo(user, course_id, elo) {
473
- const regDoc = await getOrCreateCourseRegistrationsDoc(user);
474
- const course = regDoc.courses.find((c) => c.courseID === course_id);
475
- course.elo = elo;
476
- return getLocalUserDB(user).put(regDoc);
784
+ async function getChildTagStubs(courseID, tagID) {
785
+ return await filterAllDocsByPrefix(getCourseDB2(courseID), tagID + ">");
477
786
  }
478
- async function registerUserForClassroom(user, classID, registerAs) {
479
- log3(`Registering user: ${user} in course: ${classID}`);
480
- return getOrCreateClassroomRegistrationsDoc(user).then((doc) => {
481
- const regItem = {
482
- classID,
483
- registeredAs: registerAs
484
- };
485
- if (doc.registrations.filter((reg) => {
486
- return reg.classID === regItem.classID && reg.registeredAs === regItem.registeredAs;
487
- }).length === 0) {
488
- doc.registrations.push(regItem);
489
- } else {
490
- log3(`User ${user} is already registered for class ${classID}`);
491
- }
492
- return getLocalUserDB(user).put(doc);
787
+ async function getAppliedTags(id_course, id_card) {
788
+ const db = getCourseDB2(id_course);
789
+ const result = await db.query("getTags", {
790
+ startkey: id_card,
791
+ endkey: id_card
792
+ // include_docs: true
493
793
  });
794
+ return result;
494
795
  }
495
- async function dropUserFromClassroom(user, classID) {
496
- return getOrCreateClassroomRegistrationsDoc(user).then((doc) => {
497
- let index = -1;
498
- for (let i = 0; i < doc.registrations.length; i++) {
499
- if (doc.registrations[i].classID === classID) {
500
- index = i;
501
- }
502
- }
503
- if (index !== -1) {
504
- doc.registrations.splice(index, 1);
505
- }
506
- return getLocalUserDB(user).put(doc);
796
+ async function updateCardElo2(courseID, cardID, elo) {
797
+ if (elo) {
798
+ const cDB = getCourseDB2(courseID);
799
+ const card = await cDB.get(cardID);
800
+ logger.debug(`Replacing ${JSON.stringify(card.elo)} with ${JSON.stringify(elo)}`);
801
+ card.elo = elo;
802
+ return cDB.put(card);
803
+ }
804
+ }
805
+ async function updateCredentialledCourseConfig(courseID, config) {
806
+ logger.debug(`Updating course config:
807
+
808
+ ${JSON.stringify(config)}
809
+ `);
810
+ const db = getCourseDB2(courseID);
811
+ const old = await getCredentialledCourseConfig(courseID);
812
+ return await db.put({
813
+ ...config,
814
+ _rev: old._rev
507
815
  });
508
816
  }
509
- async function getUserClassrooms(user) {
510
- return getOrCreateClassroomRegistrationsDoc(user);
817
+ function isSuccessRow(row) {
818
+ return "doc" in row && row.doc !== null && row.doc !== void 0;
511
819
  }
512
- var import_common, import_moment3, log3, cardHistoryPrefix2, BaseUser, userCoursesDoc, userClassroomsDoc;
513
- var init_BaseUserDB = __esm({
514
- "src/impl/common/BaseUserDB.ts"() {
820
+ var import_common5, CoursesDB, CourseDB;
821
+ var init_courseDB = __esm({
822
+ "src/impl/couch/courseDB.ts"() {
515
823
  "use strict";
516
- init_util();
517
- import_common = require("@vue-skuilder/common");
518
- import_moment3 = __toESM(require("moment"));
824
+ import_common5 = require("@vue-skuilder/common");
825
+ init_couch();
826
+ init_updateQueue();
519
827
  init_types_legacy();
520
828
  init_logger();
521
- init_userDBHelpers();
522
- init_updateQueue();
523
- init_user_course_relDB();
524
- init_couch();
525
- log3 = (s) => {
526
- logger.info(s);
527
- };
528
- cardHistoryPrefix2 = "cardH-";
529
- BaseUser = class _BaseUser {
530
- static _instance;
531
- static _initialized = false;
532
- static Dummy(syncStrategy) {
533
- return new _BaseUser("Me", syncStrategy);
534
- }
535
- static DOC_IDS = {
536
- CONFIG: "CONFIG",
537
- COURSE_REGISTRATIONS: "CourseRegistrations",
538
- CLASSROOM_REGISTRATIONS: "ClassroomRegistrations"
539
- };
540
- // private email: string;
541
- _username;
542
- syncStrategy;
543
- getUsername() {
544
- return this._username;
545
- }
546
- isLoggedIn() {
547
- return !this._username.startsWith(GuestUsername);
548
- }
549
- remoteDB;
550
- remote() {
551
- return this.remoteDB;
829
+ init_clientCache();
830
+ init_courseAPI();
831
+ init_courseLookupDB();
832
+ init_navigators();
833
+ CoursesDB = class {
834
+ _courseIDs;
835
+ constructor(courseIDs) {
836
+ if (courseIDs && courseIDs.length > 0) {
837
+ this._courseIDs = courseIDs;
838
+ } else {
839
+ this._courseIDs = void 0;
840
+ }
552
841
  }
553
- localDB;
554
- updateQueue;
555
- async createAccount(username, password) {
556
- if (!this.syncStrategy.canCreateAccount()) {
557
- throw new Error("Account creation not supported by current sync strategy");
842
+ async getCourseList() {
843
+ let crsList = await CourseLookup.allCourses();
844
+ logger.debug(`AllCourses: ${crsList.map((c) => c.name + ", " + c._id + "\n ")}`);
845
+ if (this._courseIDs) {
846
+ crsList = crsList.filter((c) => this._courseIDs.includes(c._id));
558
847
  }
559
- if (!this._username.startsWith(GuestUsername)) {
560
- throw new Error(
561
- `Cannot create a new account while logged in:
562
- Currently logged-in as ${this._username}.`
563
- );
848
+ logger.debug(`AllCourses.filtered: ${crsList.map((c) => c.name + ", " + c._id + "\n ")}`);
849
+ const cfgs = await Promise.all(
850
+ crsList.map(async (c) => {
851
+ try {
852
+ const cfg = await getCredentialledCourseConfig(c._id);
853
+ logger.debug(`Found cfg: ${JSON.stringify(cfg)}`);
854
+ return cfg;
855
+ } catch (e) {
856
+ logger.warn(`Error fetching cfg for course ${c.name}, ${c._id}: ${e}`);
857
+ return void 0;
858
+ }
859
+ })
860
+ );
861
+ return cfgs.filter((c) => !!c);
862
+ }
863
+ async getCourseConfig(courseId) {
864
+ if (this._courseIDs && this._courseIDs.length && !this._courseIDs.includes(courseId)) {
865
+ throw new Error(`Course ${courseId} not in course list`);
564
866
  }
565
- const result = await this.syncStrategy.createAccount(username, password);
566
- if (result.status === import_common.Status.ok) {
567
- log3(`Account created successfully, updating username to ${username}`);
568
- this._username = username;
569
- localStorage.removeItem("dbUUID");
570
- await this.init();
867
+ const cfg = await getCredentialledCourseConfig(courseId);
868
+ if (cfg === void 0) {
869
+ throw new Error(`Error fetching cfg for course ${courseId}`);
870
+ } else {
871
+ return cfg;
571
872
  }
873
+ }
874
+ async disambiguateCourse(courseId, disambiguator) {
875
+ await CourseLookup.updateDisambiguator(courseId, disambiguator);
876
+ }
877
+ };
878
+ CourseDB = class {
879
+ // private log(msg: string): void {
880
+ // log(`CourseLog: ${this.id}\n ${msg}`);
881
+ // }
882
+ db;
883
+ id;
884
+ _getCurrentUser;
885
+ updateQueue;
886
+ constructor(id, userLookup) {
887
+ this.id = id;
888
+ this.db = getCourseDB2(this.id);
889
+ this._getCurrentUser = userLookup;
890
+ this.updateQueue = new UpdateQueue(this.db);
891
+ }
892
+ getCourseID() {
893
+ return this.id;
894
+ }
895
+ async getCourseInfo() {
896
+ const cardCount = (await this.db.find({
897
+ selector: {
898
+ docType: "CARD" /* CARD */
899
+ },
900
+ limit: 1e3
901
+ })).docs.length;
572
902
  return {
573
- status: result.status,
574
- error: result.error || ""
903
+ cardCount,
904
+ registeredUsers: 0
575
905
  };
576
906
  }
577
- async login(username, password) {
578
- if (!this.syncStrategy.canAuthenticate()) {
579
- throw new Error("Authentication not supported by current sync strategy");
580
- }
581
- if (!this._username.startsWith(GuestUsername)) {
582
- throw new Error(`Cannot change accounts while logged in.
583
- Log out of account ${this.getUsername()} before logging in as ${username}.`);
584
- }
585
- const loginResult = await this.syncStrategy.authenticate(username, password);
586
- if (loginResult.ok) {
587
- log3(`Logged in as ${username}`);
588
- this._username = username;
589
- localStorage.removeItem("dbUUID");
590
- await this.init();
591
- }
592
- return loginResult;
593
- }
594
- async resetUserData() {
595
- if (this.syncStrategy.canAuthenticate()) {
596
- return {
597
- status: import_common.Status.error,
598
- error: "Reset user data is only available for local-only mode. Use logout instead for remote sync."
599
- };
600
- }
601
- try {
602
- const localDB = getLocalUserDB(this._username);
603
- const allDocs = await localDB.allDocs({ include_docs: false });
604
- const docsToDelete = allDocs.rows.filter((row) => {
605
- const id = row.id;
606
- return id.startsWith(cardHistoryPrefix2) || // Card interaction history
607
- id.startsWith(REVIEW_PREFIX) || // Scheduled reviews
608
- id === _BaseUser.DOC_IDS.COURSE_REGISTRATIONS || // Course registrations
609
- id === _BaseUser.DOC_IDS.CLASSROOM_REGISTRATIONS || // Classroom registrations
610
- id === _BaseUser.DOC_IDS.CONFIG;
611
- }).map((row) => ({ _id: row.id, _rev: row.value.rev, _deleted: true }));
612
- if (docsToDelete.length > 0) {
613
- await localDB.bulkDocs(docsToDelete);
614
- }
615
- await this.init();
616
- return { status: import_common.Status.ok };
617
- } catch (error) {
618
- logger.error("Failed to reset user data:", error);
619
- return {
620
- status: import_common.Status.error,
621
- error: error instanceof Error ? error.message : "Unknown error during reset"
907
+ async getInexperiencedCards(limit = 2) {
908
+ return (await this.db.query("cardsByInexperience", {
909
+ limit
910
+ })).rows.map((r) => {
911
+ const ret = {
912
+ courseId: this.id,
913
+ cardId: r.id,
914
+ count: r.key,
915
+ elo: r.value
622
916
  };
623
- }
624
- }
625
- async logout() {
626
- if (!this.syncStrategy.canAuthenticate()) {
627
- this._username = await this.syncStrategy.getCurrentUsername();
628
- await this.init();
629
- return { ok: true };
630
- }
631
- const ret = await this.syncStrategy.logout();
632
- this._username = await this.syncStrategy.getCurrentUsername();
633
- await this.init();
634
- return ret;
917
+ return ret;
918
+ });
635
919
  }
636
- update(id, update) {
637
- return this.updateQueue.update(id, update);
920
+ async getCardsByEloLimits(options = {
921
+ low: 0,
922
+ high: Number.MIN_SAFE_INTEGER,
923
+ limit: 25,
924
+ page: 0
925
+ }) {
926
+ return (await this.db.query("elo", {
927
+ startkey: options.low,
928
+ endkey: options.high,
929
+ limit: options.limit,
930
+ skip: options.limit * options.page
931
+ })).rows.map((r) => {
932
+ return `${this.id}-${r.id}-${r.key}`;
933
+ });
638
934
  }
639
- async getCourseRegistrationsDoc() {
640
- logger.debug(`Fetching courseRegistrations for ${this.getUsername()}`);
641
- let ret;
642
- try {
643
- const regDoc = await this.localDB.get(
644
- _BaseUser.DOC_IDS.COURSE_REGISTRATIONS
645
- );
646
- return regDoc;
647
- } catch (e) {
648
- const err = e;
649
- if (err.status === 404) {
650
- await this.localDB.put({
651
- _id: _BaseUser.DOC_IDS.COURSE_REGISTRATIONS,
652
- courses: [],
653
- studyWeight: {}
654
- });
655
- ret = await this.getCourseRegistrationsDoc();
935
+ async getCardEloData(id) {
936
+ const docs = await this.db.allDocs({
937
+ keys: id,
938
+ include_docs: true
939
+ });
940
+ const ret = [];
941
+ docs.rows.forEach((r) => {
942
+ if (isSuccessRow(r)) {
943
+ if (r.doc && r.doc.elo) {
944
+ ret.push((0, import_common5.toCourseElo)(r.doc.elo));
945
+ } else {
946
+ logger.warn("no elo data for card: " + r.id);
947
+ ret.push((0, import_common5.blankCourseElo)());
948
+ }
656
949
  } else {
657
- throw new Error(
658
- `Unexpected error ${JSON.stringify(e)} in getOrCreateCourseRegistrationDoc...`
659
- );
950
+ logger.warn("no elo data for card: " + JSON.stringify(r));
951
+ ret.push((0, import_common5.blankCourseElo)());
660
952
  }
661
- }
662
- return ret;
663
- }
664
- async getActiveCourses() {
665
- const reg = await this.getCourseRegistrationsDoc();
666
- return reg.courses.filter((c) => {
667
- return c.status === void 0 || c.status === "active";
668
953
  });
954
+ return ret;
669
955
  }
670
956
  /**
671
- * Returns a promise of the card IDs that the user has
672
- * a scheduled review for.
673
- *
957
+ * Returns the lowest and highest `global` ELO ratings in the course
674
958
  */
675
- async getActiveCards() {
676
- const keys = getStartAndEndKeys(REVIEW_PREFIX);
677
- const reviews = await this.remoteDB.allDocs({
678
- startkey: keys.startkey,
679
- endkey: keys.endkey,
680
- include_docs: true
681
- });
682
- return reviews.rows.map((r) => `${r.doc.courseId}-${r.doc.cardId}`);
959
+ async getELOBounds() {
960
+ const [low, high] = await Promise.all([
961
+ (await this.db.query("elo", {
962
+ startkey: 0,
963
+ limit: 1,
964
+ include_docs: false
965
+ })).rows[0].key,
966
+ (await this.db.query("elo", {
967
+ limit: 1,
968
+ descending: true,
969
+ startkey: 1e5
970
+ })).rows[0].key
971
+ ]);
972
+ return {
973
+ low,
974
+ high
975
+ };
683
976
  }
684
- async getActivityRecords() {
685
- try {
686
- const hist = await this.getHistory();
687
- const allRecords = [];
688
- if (!Array.isArray(hist)) {
689
- logger.error("getHistory did not return an array:", hist);
690
- return allRecords;
691
- }
692
- let sampleCount = 0;
693
- for (let i = 0; i < hist.length; i++) {
694
- try {
695
- if (hist[i] && Array.isArray(hist[i].records)) {
696
- hist[i].records.forEach((record) => {
697
- try {
698
- if (!record.timeStamp) {
699
- return;
700
- }
701
- let timeStamp;
702
- if (typeof record.timeStamp === "object") {
703
- if (typeof record.timeStamp.toDate === "function") {
704
- timeStamp = record.timeStamp.toISOString();
705
- } else if (record.timeStamp instanceof Date) {
706
- timeStamp = record.timeStamp.toISOString();
707
- } else {
708
- if (sampleCount < 3) {
709
- logger.warn("Unknown timestamp object type:", record.timeStamp);
710
- sampleCount++;
711
- }
712
- return;
713
- }
714
- } else if (typeof record.timeStamp === "string") {
715
- const date = new Date(record.timeStamp);
716
- if (isNaN(date.getTime())) {
717
- return;
718
- }
719
- timeStamp = record.timeStamp;
720
- } else if (typeof record.timeStamp === "number") {
721
- timeStamp = new Date(record.timeStamp).toISOString();
722
- } else {
723
- return;
724
- }
725
- allRecords.push({
726
- timeStamp,
727
- courseID: record.courseID || "unknown",
728
- cardID: record.cardID || "unknown",
729
- timeSpent: record.timeSpent || 0,
730
- type: "card_view"
731
- });
732
- } catch (err) {
733
- }
734
- });
735
- }
736
- } catch (err) {
737
- logger.error("Error processing history item:", err);
738
- }
739
- }
740
- logger.debug(`Found ${allRecords.length} activity records`);
741
- return allRecords;
742
- } catch (err) {
743
- logger.error("Error in getActivityRecords:", err);
744
- return [];
977
+ async removeCard(id) {
978
+ const doc = await this.db.get(id);
979
+ if (!doc.docType || !(doc.docType === "CARD" /* CARD */)) {
980
+ throw new Error(`failed to remove ${id} from course ${this.id}. id does not point to a card`);
745
981
  }
982
+ return this.db.remove(doc);
746
983
  }
747
- async getReviewstoDate(targetDate, course_id) {
748
- const keys = getStartAndEndKeys(REVIEW_PREFIX);
749
- const reviews = await this.remoteDB.allDocs({
750
- startkey: keys.startkey,
751
- endkey: keys.endkey,
984
+ async getCardDisplayableDataIDs(id) {
985
+ logger.debug(id.join(", "));
986
+ const cards = await this.db.allDocs({
987
+ keys: id,
752
988
  include_docs: true
753
989
  });
754
- log3(
755
- `Fetching ${this._username}'s scheduled reviews${course_id ? ` for course ${course_id}` : ""}.`
756
- );
757
- return reviews.rows.filter((r) => {
758
- if (r.id.startsWith(REVIEW_PREFIX)) {
759
- const date = import_moment3.default.utc(r.id.substr(REVIEW_PREFIX.length), REVIEW_TIME_FORMAT);
760
- if (targetDate.isAfter(date)) {
761
- if (course_id === void 0 || r.doc.courseId === course_id) {
762
- return true;
763
- }
764
- }
990
+ const ret = {};
991
+ cards.rows.forEach((r) => {
992
+ if (isSuccessRow(r)) {
993
+ ret[r.id] = r.doc.id_displayable_data;
765
994
  }
766
- }).map((r) => r.doc);
995
+ });
996
+ await Promise.all(
997
+ cards.rows.map((r) => {
998
+ return async () => {
999
+ if (isSuccessRow(r)) {
1000
+ ret[r.id] = r.doc.id_displayable_data;
1001
+ }
1002
+ };
1003
+ })
1004
+ );
1005
+ return ret;
767
1006
  }
768
- async getReviewsForcast(daysCount) {
769
- const time = import_moment3.default.utc().add(daysCount, "days");
770
- return this.getReviewstoDate(time);
1007
+ async getCardsByELO(elo, cardLimit) {
1008
+ elo = parseInt(elo);
1009
+ const limit = cardLimit ? cardLimit : 25;
1010
+ const below = await this.db.query("elo", {
1011
+ limit: Math.ceil(limit / 2),
1012
+ startkey: elo,
1013
+ descending: true
1014
+ });
1015
+ const aboveLimit = limit - below.rows.length;
1016
+ const above = await this.db.query("elo", {
1017
+ limit: aboveLimit,
1018
+ startkey: elo + 1
1019
+ });
1020
+ let cards = below.rows;
1021
+ cards = cards.concat(above.rows);
1022
+ const ret = cards.sort((a, b) => {
1023
+ const s = Math.abs(a.key - elo) - Math.abs(b.key - elo);
1024
+ if (s === 0) {
1025
+ return Math.random() - 0.5;
1026
+ } else {
1027
+ return s;
1028
+ }
1029
+ }).map((c) => `${this.id}-${c.id}-${c.key}`);
1030
+ const str = `below:
1031
+ ${below.rows.map((r) => ` ${r.id}-${r.key}
1032
+ `)}
1033
+
1034
+ above:
1035
+ ${above.rows.map((r) => ` ${r.id}-${r.key}
1036
+ `)}`;
1037
+ logger.debug(`Getting ${limit} cards centered around elo: ${elo}:
1038
+
1039
+ ` + str);
1040
+ return ret;
771
1041
  }
772
- async getPendingReviews(course_id) {
773
- const now = import_moment3.default.utc();
774
- return this.getReviewstoDate(now, course_id);
1042
+ async getCourseConfig() {
1043
+ const ret = await getCredentialledCourseConfig(this.id);
1044
+ if (ret) {
1045
+ return ret;
1046
+ } else {
1047
+ throw new Error(`Course config not found for course ID: ${this.id}`);
1048
+ }
775
1049
  }
776
- async getScheduledReviewCount(course_id) {
777
- return (await this.getPendingReviews(course_id)).length;
1050
+ async updateCourseConfig(cfg) {
1051
+ logger.debug(`Updating: ${JSON.stringify(cfg)}`);
1052
+ try {
1053
+ return await updateCredentialledCourseConfig(this.id, cfg);
1054
+ } catch (error) {
1055
+ logger.error(`Error updating course config in course DB: ${error}`);
1056
+ throw error;
1057
+ }
778
1058
  }
779
- async getRegisteredCourses() {
780
- const regDoc = await this.getCourseRegistrationsDoc();
781
- return regDoc.courses.filter((c) => {
782
- return !c.status || c.status === "active" || c.status === "maintenance-mode";
783
- });
1059
+ async updateCardElo(cardId, elo) {
1060
+ if (!elo) {
1061
+ throw new Error(`Cannot update card elo with null or undefined value for card ID: ${cardId}`);
1062
+ }
1063
+ try {
1064
+ const result = await this.updateQueue.update(cardId, (card) => {
1065
+ logger.debug(`Replacing ${JSON.stringify(card.elo)} with ${JSON.stringify(elo)}`);
1066
+ card.elo = elo;
1067
+ return card;
1068
+ });
1069
+ return { ok: true, id: cardId, rev: result._rev };
1070
+ } catch (error) {
1071
+ logger.error(`Failed to update card elo for card ID: ${cardId}`, error);
1072
+ throw new Error(`Failed to update card elo for card ID: ${cardId}`);
1073
+ }
784
1074
  }
785
- async getCourseRegDoc(courseID) {
786
- const regDocs = await this.getCourseRegistrationsDoc();
787
- const ret = regDocs.courses.find((c) => c.courseID === courseID);
1075
+ async getAppliedTags(cardId) {
1076
+ const ret = await getAppliedTags(this.id, cardId);
788
1077
  if (ret) {
789
1078
  return ret;
790
1079
  } else {
791
- throw new Error(`Course registration not found for course ID: ${courseID}`);
1080
+ throw new Error(`Failed to find tags for card ${this.id}-${cardId}`);
792
1081
  }
793
1082
  }
794
- async registerForCourse(course_id, previewMode = false) {
795
- return this.getCourseRegistrationsDoc().then((doc) => {
796
- const status = previewMode ? "preview" : "active";
797
- logger.debug(`Registering for ${course_id} with status: ${status}`);
798
- const regItem = {
799
- status,
800
- courseID: course_id,
801
- user: true,
802
- admin: false,
803
- moderator: false,
804
- elo: {
805
- global: {
806
- score: 1e3,
807
- count: 0
808
- },
809
- tags: {},
810
- misc: {}
811
- }
812
- };
813
- if (doc.courses.filter((course) => {
814
- return course.courseID === regItem.courseID;
815
- }).length === 0) {
816
- log3(`It's a new course registration!`);
817
- doc.courses.push(regItem);
818
- doc.studyWeight[course_id] = 1;
819
- } else {
820
- doc.courses.forEach((c) => {
821
- log3(`Found the previously registered course!`);
822
- if (c.courseID === course_id) {
823
- c.status = status;
824
- }
825
- });
826
- }
827
- return this.localDB.put(doc);
828
- }).catch((e) => {
829
- log3(`Registration failed because of: ${JSON.stringify(e)}`);
830
- throw e;
831
- });
1083
+ async addTagToCard(cardId, tagId, updateELO) {
1084
+ return await addTagToCard(this.id, cardId, tagId, (await this._getCurrentUser()).getUsername(), updateELO);
832
1085
  }
833
- async dropCourse(course_id, dropStatus = "dropped") {
834
- return this.getCourseRegistrationsDoc().then((doc) => {
835
- let index = -1;
836
- for (let i = 0; i < doc.courses.length; i++) {
837
- if (doc.courses[i].courseID === course_id) {
838
- index = i;
839
- }
840
- }
841
- if (index !== -1) {
842
- delete doc.studyWeight[course_id];
843
- doc.courses[index].status = dropStatus;
844
- } else {
845
- throw new Error(
846
- `User ${this.getUsername()} is not currently registered for course ${course_id}`
847
- );
848
- }
849
- return this.localDB.put(doc);
850
- });
1086
+ async removeTagFromCard(cardId, tagId) {
1087
+ return await removeTagFromCard(this.id, cardId, tagId);
851
1088
  }
852
- async getCourseInterface(courseId) {
853
- return new UsrCrsData(this, courseId);
1089
+ async createTag(name, author) {
1090
+ return await createTag(this.id, name, author);
854
1091
  }
855
- async getUserEditableCourses() {
856
- let courseIDs = [];
857
- const registeredCourses = await this.getCourseRegistrationsDoc();
858
- courseIDs = courseIDs.concat(
859
- registeredCourses.courses.map((course) => {
860
- return course.courseID;
861
- })
862
- );
863
- const cfgs = await Promise.all(
864
- courseIDs.map(async (id) => {
865
- return await getCredentialledCourseConfig(id);
866
- })
867
- );
868
- return cfgs;
1092
+ async getTag(tagId) {
1093
+ return await getTag(this.id, tagId);
869
1094
  }
870
- async getConfig() {
871
- const defaultConfig = {
872
- _id: _BaseUser.DOC_IDS.CONFIG,
873
- darkMode: false,
874
- likesConfetti: false
875
- };
1095
+ async updateTag(tag) {
1096
+ if (tag.course !== this.id) {
1097
+ throw new Error(`Tag ${JSON.stringify(tag)} does not belong to course ${this.id}`);
1098
+ }
1099
+ return await updateTag(tag);
1100
+ }
1101
+ async getCourseTagStubs() {
1102
+ return getCourseTagStubs(this.id);
1103
+ }
1104
+ async addNote(codeCourse, shape, data, author, tags, uploads, elo = (0, import_common5.blankCourseElo)()) {
876
1105
  try {
877
- const cfg = await this.localDB.get(_BaseUser.DOC_IDS.CONFIG);
878
- logger.debug("Raw config from DB:", cfg);
879
- return cfg;
880
- } catch (e) {
881
- const err = e;
882
- if (err.name && err.name === "not_found") {
883
- await this.localDB.put(defaultConfig);
884
- return this.getConfig();
1106
+ const resp = await addNote55(this.id, codeCourse, shape, data, author, tags, uploads, elo);
1107
+ if (resp.ok) {
1108
+ if (resp.cardCreationFailed) {
1109
+ logger.warn(
1110
+ `[courseDB.addNote] Note added but card creation failed: ${resp.cardCreationError}`
1111
+ );
1112
+ return {
1113
+ status: import_common5.Status.error,
1114
+ message: `Note was added but no cards were created: ${resp.cardCreationError}`,
1115
+ id: resp.id
1116
+ };
1117
+ }
1118
+ return {
1119
+ status: import_common5.Status.ok,
1120
+ message: "",
1121
+ id: resp.id
1122
+ };
885
1123
  } else {
886
- logger.error(`Error setting user default config:`, e);
887
- throw new Error(`Error returning the user's configuration: ${JSON.stringify(e)}`);
1124
+ return {
1125
+ status: import_common5.Status.error,
1126
+ message: "Unexpected error adding note"
1127
+ };
888
1128
  }
1129
+ } catch (e) {
1130
+ const err = e;
1131
+ logger.error(
1132
+ `[addNote] error ${err.name}
1133
+ reason: ${err.reason}
1134
+ message: ${err.message}`
1135
+ );
1136
+ return {
1137
+ status: import_common5.Status.error,
1138
+ message: `Error adding note to course. ${e.reason || err.message}`
1139
+ };
889
1140
  }
890
1141
  }
891
- async setConfig(items) {
892
- logger.debug(`Setting Config items ${JSON.stringify(items)}`);
893
- const c = await this.getConfig();
894
- const put = await this.localDB.put({
895
- ...c,
896
- ...items
897
- });
898
- if (put.ok) {
899
- logger.debug(`Config items set: ${JSON.stringify(items)}`);
900
- } else {
901
- logger.error(`Error setting config items: ${JSON.stringify(put)}`);
902
- }
1142
+ async getCourseDoc(id, options) {
1143
+ return await getCourseDoc(this.id, id, options);
903
1144
  }
904
- /**
905
- *
906
- * This function should be called *only* by the pouchdb datalayer provider
907
- * auth store.
908
- *
909
- *
910
- * Anyone else seeking the current user should use the auth store's
911
- * exported `getCurrentUser` method.
912
- *
913
- */
914
- static async instance(syncStrategy, username) {
915
- if (username) {
916
- _BaseUser._instance = new _BaseUser(username, syncStrategy);
917
- await _BaseUser._instance.init();
918
- return _BaseUser._instance;
919
- } else if (_BaseUser._instance && _BaseUser._initialized) {
920
- return _BaseUser._instance;
921
- } else if (_BaseUser._instance) {
922
- return new Promise((resolve) => {
923
- (function waitForUser() {
924
- if (_BaseUser._initialized) {
925
- return resolve(_BaseUser._instance);
926
- } else {
927
- setTimeout(waitForUser, 50);
928
- }
929
- })();
930
- });
931
- } else {
932
- const guestUsername = await syncStrategy.getCurrentUsername();
933
- _BaseUser._instance = new _BaseUser(guestUsername, syncStrategy);
934
- await _BaseUser._instance.init();
935
- return _BaseUser._instance;
936
- }
1145
+ async getCourseDocs(ids, options = {}) {
1146
+ return await getCourseDocs(this.id, ids, options);
937
1147
  }
938
- constructor(username, syncStrategy) {
939
- _BaseUser._initialized = false;
940
- this._username = username;
941
- this.syncStrategy = syncStrategy;
942
- this.setDBandQ();
1148
+ ////////////////////////////////////
1149
+ // NavigationStrategyManager implementation
1150
+ ////////////////////////////////////
1151
+ getNavigationStrategy(id) {
1152
+ logger.debug(`[courseDB] Getting navigation strategy: ${id}`);
1153
+ const strategy = {
1154
+ id: "ELO",
1155
+ docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
1156
+ name: "ELO",
1157
+ description: "ELO-based navigation strategy for ordering content by difficulty",
1158
+ implementingClass: "elo" /* ELO */,
1159
+ course: this.id,
1160
+ serializedData: ""
1161
+ // serde is a noop for ELO navigator.
1162
+ };
1163
+ return Promise.resolve(strategy);
943
1164
  }
944
- setDBandQ() {
945
- this.localDB = getLocalUserDB(this._username);
946
- this.remoteDB = this.syncStrategy.setupRemoteDB(this._username);
947
- this.updateQueue = new UpdateQueue(this.localDB);
1165
+ getAllNavigationStrategies() {
1166
+ logger.debug("[courseDB] Returning hard-coded navigation strategies");
1167
+ const strategies = [
1168
+ {
1169
+ id: "ELO",
1170
+ docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
1171
+ name: "ELO",
1172
+ description: "ELO-based navigation strategy for ordering content by difficulty",
1173
+ implementingClass: "elo" /* ELO */,
1174
+ course: this.id,
1175
+ serializedData: ""
1176
+ // serde is a noop for ELO navigator.
1177
+ }
1178
+ ];
1179
+ return Promise.resolve(strategies);
948
1180
  }
949
- async init() {
950
- _BaseUser._initialized = false;
951
- this.setDBandQ();
952
- this.syncStrategy.startSync(this.localDB, this.remoteDB);
953
- void this.applyDesignDocs();
954
- void this.deduplicateReviews();
955
- _BaseUser._initialized = true;
1181
+ addNavigationStrategy(data) {
1182
+ logger.debug(`[courseDB] Adding navigation strategy: ${data.id}`);
1183
+ logger.debug(JSON.stringify(data));
1184
+ return Promise.resolve();
956
1185
  }
957
- static designDocs = [
958
- {
959
- _id: "_design/reviewCards",
960
- views: {
961
- reviewCards: {
962
- map: `function (doc) {
963
- if (doc._id && doc._id.indexOf('card_review') === 0 && doc.courseId && doc.cardId) {
964
- emit(doc._id, doc.courseId + '-' + doc.cardId);
965
- }
966
- }`
967
- }
968
- }
969
- }
970
- ];
971
- async applyDesignDocs() {
972
- for (const doc of _BaseUser.designDocs) {
973
- try {
974
- try {
975
- const existingDoc = await this.remoteDB.get(doc._id);
976
- await this.remoteDB.put({
977
- ...doc,
978
- _rev: existingDoc._rev
979
- });
980
- } catch (e) {
981
- if (e instanceof Error && e.name === "not_found") {
982
- await this.remoteDB.put(doc);
983
- } else {
984
- throw e;
985
- }
986
- }
987
- } catch (error) {
988
- if (error instanceof Error && error.name === "conflict") {
989
- logger.warn(`Design doc ${doc._id} update conflict - will retry`);
990
- await new Promise((resolve) => setTimeout(resolve, 1e3));
991
- await this.applyDesignDoc(doc);
992
- } else {
993
- logger.error(`Failed to apply design doc ${doc._id}:`, error);
994
- throw error;
995
- }
996
- }
997
- }
1186
+ updateNavigationStrategy(id, data) {
1187
+ logger.debug(`[courseDB] Updating navigation strategy: ${id}`);
1188
+ logger.debug(JSON.stringify(data));
1189
+ return Promise.resolve();
998
1190
  }
999
- // Helper method for single doc update with retry
1000
- async applyDesignDoc(doc, retries = 3) {
1191
+ async surfaceNavigationStrategy() {
1192
+ logger.warn(`Returning hard-coded default ELO navigator`);
1193
+ const ret = {
1194
+ id: "ELO",
1195
+ docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
1196
+ name: "ELO",
1197
+ description: "ELO-based navigation strategy",
1198
+ implementingClass: "elo" /* ELO */,
1199
+ course: this.id,
1200
+ serializedData: ""
1201
+ // serde is a noop for ELO navigator.
1202
+ };
1203
+ return Promise.resolve(ret);
1204
+ }
1205
+ ////////////////////////////////////
1206
+ // END NavigationStrategyManager implementation
1207
+ ////////////////////////////////////
1208
+ ////////////////////////////////////
1209
+ // StudyContentSource implementation
1210
+ ////////////////////////////////////
1211
+ async getNewCards(limit = 99) {
1212
+ const u = await this._getCurrentUser();
1001
1213
  try {
1002
- const existingDoc = await this.remoteDB.get(doc._id);
1003
- await this.remoteDB.put({
1004
- ...doc,
1005
- _rev: existingDoc._rev
1006
- });
1214
+ const strategy = await this.surfaceNavigationStrategy();
1215
+ const navigator = await ContentNavigator.create(u, this, strategy);
1216
+ return navigator.getNewCards(limit);
1007
1217
  } catch (e) {
1008
- if (e instanceof Error && e.name === "conflict" && retries > 0) {
1009
- await new Promise((resolve) => setTimeout(resolve, 1e3));
1010
- return this.applyDesignDoc(doc, retries - 1);
1011
- }
1218
+ logger.error(`[courseDB] Error surfacing a NavigationStrategy: ${e}`);
1012
1219
  throw e;
1013
1220
  }
1014
1221
  }
1015
- /**
1016
- * Logs a record of the user's interaction with the card and returns the card's
1017
- * up-to-date history
1018
- *
1019
- * // [ ] #db-refactor extract to a smaller scope - eg, UserStudySession
1020
- *
1021
- * @param record the recent recorded interaction between user and card
1022
- * @returns The updated state of the card's CardHistory data
1023
- */
1024
- async putCardRecord(record) {
1025
- const cardHistoryID = getCardHistoryID(record.courseID, record.cardID);
1026
- record.timeStamp = import_moment3.default.utc(record.timeStamp).toString();
1222
+ async getPendingReviews() {
1223
+ const u = await this._getCurrentUser();
1027
1224
  try {
1028
- const cardHistory = await this.update(
1029
- cardHistoryID,
1030
- function(h) {
1031
- h.records.push(record);
1032
- h.bestInterval = h.bestInterval || 0;
1033
- h.lapses = h.lapses || 0;
1034
- h.streak = h.streak || 0;
1035
- return h;
1036
- }
1037
- );
1038
- cardHistory.records = cardHistory.records.map((record2) => {
1039
- const ret = {
1040
- ...record2
1041
- };
1042
- ret.timeStamp = import_moment3.default.utc(record2.timeStamp);
1043
- return ret;
1044
- });
1045
- return cardHistory;
1225
+ const strategy = await this.surfaceNavigationStrategy();
1226
+ const navigator = await ContentNavigator.create(u, this, strategy);
1227
+ return navigator.getPendingReviews();
1046
1228
  } catch (e) {
1047
- const reason = e;
1048
- if (reason.status === 404) {
1049
- const initCardHistory = {
1050
- _id: cardHistoryID,
1051
- cardID: record.cardID,
1052
- courseID: record.courseID,
1053
- records: [record],
1054
- lapses: 0,
1055
- streak: 0,
1056
- bestInterval: 0
1057
- };
1058
- void this.remoteDB.put(initCardHistory);
1059
- return initCardHistory;
1060
- } else {
1061
- throw new Error(`putCardRecord failed because of:
1062
- name:${reason.name}
1063
- error: ${reason.error}
1064
- message: ${reason.message}`);
1065
- }
1066
- }
1067
- }
1068
- async deduplicateReviews() {
1069
- try {
1070
- log3("Starting deduplication of scheduled reviews...");
1071
- const reviewsMap = {};
1072
- const duplicateDocIds = [];
1073
- const scheduledReviews = await this.remoteDB.query("reviewCards/reviewCards");
1074
- log3(`Found ${scheduledReviews.rows.length} scheduled reviews to process`);
1075
- scheduledReviews.rows.forEach((r) => {
1076
- const qualifiedCardId = r.value;
1077
- const docId = r.key;
1078
- if (reviewsMap[qualifiedCardId]) {
1079
- log3(`Found duplicate scheduled review for card: ${qualifiedCardId}`);
1080
- log3(
1081
- `Marking earlier review ${reviewsMap[qualifiedCardId]} for deletion, keeping ${docId}`
1082
- );
1083
- duplicateDocIds.push(reviewsMap[qualifiedCardId]);
1084
- reviewsMap[qualifiedCardId] = docId;
1085
- } else {
1086
- reviewsMap[qualifiedCardId] = docId;
1087
- }
1088
- });
1089
- if (duplicateDocIds.length > 0) {
1090
- log3(`Removing ${duplicateDocIds.length} duplicate reviews...`);
1091
- const deletePromises = duplicateDocIds.map(async (docId) => {
1092
- try {
1093
- const doc = await this.remoteDB.get(docId);
1094
- await this.remoteDB.remove(doc);
1095
- log3(`Successfully removed duplicate review: ${docId}`);
1096
- } catch (error) {
1097
- log3(`Failed to remove duplicate review ${docId}: ${error}`);
1098
- }
1099
- });
1100
- await Promise.all(deletePromises);
1101
- log3(`Deduplication complete. Processed ${duplicateDocIds.length} duplicates`);
1102
- } else {
1103
- log3("No duplicate reviews found");
1104
- }
1105
- } catch (error) {
1106
- log3(`Error during review deduplication: ${error}`);
1107
- }
1108
- }
1109
- /**
1110
- * Returns a promise of the card IDs that the user has
1111
- * encountered in the past.
1112
- *
1113
- * @param course_id optional specification of individual course
1114
- */
1115
- async getSeenCards(course_id) {
1116
- let prefix = cardHistoryPrefix2;
1117
- if (course_id) {
1118
- prefix += course_id;
1229
+ logger.error(`[courseDB] Error surfacing a NavigationStrategy: ${e}`);
1230
+ throw e;
1119
1231
  }
1120
- const docs = await filterAllDocsByPrefix(this.localDB, prefix, {
1121
- include_docs: false
1122
- });
1123
- const ret = [];
1124
- docs.rows.forEach((row) => {
1125
- if (row.id.startsWith(cardHistoryPrefix2)) {
1126
- ret.push(row.id.substr(cardHistoryPrefix2.length));
1127
- }
1128
- });
1129
- return ret;
1130
1232
  }
1131
- /**
1132
- *
1133
- * @returns A promise of the cards that the user has seen in the past.
1134
- */
1135
- async getHistory() {
1136
- const cards = await filterAllDocsByPrefix(
1137
- this.remoteDB,
1138
- cardHistoryPrefix2,
1139
- {
1140
- include_docs: true,
1141
- attachments: false
1142
- }
1143
- );
1144
- return cards.rows.map((r) => r.doc);
1145
- }
1146
- async updateCourseSettings(course_id, settings) {
1147
- void this.getCourseRegistrationsDoc().then((doc) => {
1148
- const crs = doc.courses.find((c) => c.courseID === course_id);
1149
- if (crs) {
1150
- if (crs.settings === null || crs.settings === void 0) {
1151
- crs.settings = {};
1152
- }
1153
- settings.forEach((setting) => {
1154
- crs.settings[setting.key] = setting.value;
1233
+ async getCardsCenteredAtELO(options = {
1234
+ limit: 99,
1235
+ elo: "user"
1236
+ }, filter) {
1237
+ let targetElo;
1238
+ if (options.elo === "user") {
1239
+ const u = await this._getCurrentUser();
1240
+ targetElo = -1;
1241
+ try {
1242
+ const courseDoc = (await u.getCourseRegistrationsDoc()).courses.find((c) => {
1243
+ return c.courseID === this.id;
1155
1244
  });
1245
+ targetElo = (0, import_common5.EloToNumber)(courseDoc.elo);
1246
+ } catch {
1247
+ targetElo = 1e3;
1156
1248
  }
1157
- return this.localDB.put(doc);
1158
- });
1159
- }
1160
- async getCourseSettings(course_id) {
1161
- const regDoc = await this.getCourseRegistrationsDoc();
1162
- const crsDoc = regDoc.courses.find((c) => c.courseID === course_id);
1163
- if (crsDoc) {
1164
- return crsDoc.settings;
1249
+ } else if (options.elo === "random") {
1250
+ const bounds = await GET_CACHED(`elo-bounds-${this.id}`, () => this.getELOBounds());
1251
+ targetElo = Math.round(bounds.low + Math.random() * (bounds.high - bounds.low));
1165
1252
  } else {
1166
- throw new Error(`getCourseSettings Failed:
1167
- User is not registered for course ${course_id}`);
1253
+ targetElo = options.elo;
1168
1254
  }
1169
- }
1170
- async getOrCreateClassroomRegistrationsDoc() {
1171
- let ret;
1172
- try {
1173
- ret = await this.remoteDB.get(
1174
- _BaseUser.DOC_IDS.CLASSROOM_REGISTRATIONS
1175
- );
1176
- } catch (e) {
1177
- const err = e;
1178
- if (err.status === 404) {
1179
- await this.remoteDB.put({
1180
- _id: _BaseUser.DOC_IDS.CLASSROOM_REGISTRATIONS,
1181
- registrations: []
1182
- });
1183
- ret = await this.getOrCreateClassroomRegistrationsDoc();
1184
- } else {
1185
- const errorDetails = {
1186
- name: err.name,
1187
- status: err.status,
1188
- message: err.message,
1189
- reason: err.reason,
1190
- error: err.error
1191
- };
1192
- logger.error(
1193
- "Database error in getOrCreateClassroomRegistrationsDoc (private method):",
1194
- errorDetails
1195
- );
1196
- throw new Error(
1197
- `Database error accessing classroom registrations: ${err.message || err.name || "Unknown error"} (status: ${err.status})`
1198
- );
1255
+ let cards = [];
1256
+ let mult = 4;
1257
+ let previousCount = -1;
1258
+ let newCount = 0;
1259
+ while (cards.length < options.limit && newCount !== previousCount) {
1260
+ cards = await this.getCardsByELO(targetElo, mult * options.limit);
1261
+ previousCount = newCount;
1262
+ newCount = cards.length;
1263
+ logger.debug(`Found ${cards.length} elo neighbor cards...`);
1264
+ if (filter) {
1265
+ cards = cards.filter(filter);
1266
+ logger.debug(`Filtered to ${cards.length} cards...`);
1199
1267
  }
1268
+ mult *= 2;
1200
1269
  }
1201
- logger.debug(`Returning classroom registrations doc: ${JSON.stringify(ret)}`);
1202
- return ret;
1203
- }
1204
- /**
1205
- * Retrieves the list of active classroom IDs where the user is registered as a student.
1206
- *
1207
- * @returns Promise<string[]> - Array of classroom IDs, or empty array if classroom
1208
- * registration document is unavailable due to database errors
1209
- *
1210
- * @description This method gracefully handles database connectivity issues by returning
1211
- * an empty array when the classroom registrations document cannot be accessed.
1212
- * This ensures that users can still access other application features even
1213
- * when classroom functionality is temporarily unavailable.
1214
- */
1215
- async getActiveClasses() {
1216
- try {
1217
- return (await this.getOrCreateClassroomRegistrationsDoc()).registrations.filter((c) => c.registeredAs === "student").map((c) => c.classID);
1218
- } catch (error) {
1219
- logger.warn(
1220
- "Failed to load classroom registrations, continuing without classroom data:",
1221
- error
1222
- );
1223
- return [];
1270
+ const selectedCards = [];
1271
+ while (selectedCards.length < options.limit && cards.length > 0) {
1272
+ const index = randIntWeightedTowardZero(cards.length);
1273
+ const card = cards.splice(index, 1)[0];
1274
+ selectedCards.push(card);
1224
1275
  }
1225
- }
1226
- async scheduleCardReview(review) {
1227
- return scheduleCardReviewLocal(this.remoteDB, review);
1228
- }
1229
- async removeScheduledCardReview(reviewId) {
1230
- return removeScheduledCardReviewLocal(this.remoteDB, reviewId);
1231
- }
1232
- async registerForClassroom(_classId, _registerAs) {
1233
- return registerUserForClassroom(this._username, _classId, _registerAs);
1234
- }
1235
- async dropFromClassroom(classId) {
1236
- return dropUserFromClassroom(this._username, classId);
1237
- }
1238
- async getUserClassrooms() {
1239
- return getUserClassrooms(this._username);
1240
- }
1241
- async updateUserElo(courseId, elo) {
1242
- return updateUserElo(this._username, courseId, elo);
1243
- }
1244
- };
1245
- userCoursesDoc = "CourseRegistrations";
1246
- userClassroomsDoc = "ClassroomRegistrations";
1247
- }
1248
- });
1249
-
1250
- // src/impl/common/index.ts
1251
- var init_common = __esm({
1252
- "src/impl/common/index.ts"() {
1253
- "use strict";
1254
- init_SyncStrategy();
1255
- init_BaseUserDB();
1256
- init_userDBHelpers();
1257
- }
1258
- });
1259
-
1260
- // src/impl/couch/courseAPI.ts
1261
- async function addNote55(courseID, codeCourse, shape, data, author, tags, uploads, elo = (0, import_common3.blankCourseElo)()) {
1262
- const db = getCourseDB(courseID);
1263
- const payload = (0, import_common4.prepareNote55)(courseID, codeCourse, shape, data, author, tags, uploads);
1264
- const result = await db.post(payload);
1265
- const dataShapeId = import_common2.NameSpacer.getDataShapeString({
1266
- course: codeCourse,
1267
- dataShape: shape.name
1268
- });
1269
- if (result.ok) {
1270
- try {
1271
- await createCards(courseID, dataShapeId, result.id, tags, elo, author);
1272
- } catch (error) {
1273
- let errorMessage = "Unknown error";
1274
- if (error instanceof Error) {
1275
- errorMessage = error.message;
1276
- } else if (error && typeof error === "object" && "reason" in error) {
1277
- errorMessage = error.reason;
1278
- } else if (error && typeof error === "object" && "message" in error) {
1279
- errorMessage = error.message;
1280
- } else {
1281
- errorMessage = String(error);
1282
- }
1283
- logger.error(`[addNote55] Failed to create cards for note ${result.id}: ${errorMessage}`);
1284
- result.cardCreationFailed = true;
1285
- result.cardCreationError = errorMessage;
1286
- }
1287
- } else {
1288
- logger.error(`[addNote55] Error adding note. Result: ${JSON.stringify(result)}`);
1289
- }
1290
- return result;
1291
- }
1292
- async function createCards(courseID, datashapeID, noteID, tags, elo = (0, import_common3.blankCourseElo)(), author) {
1293
- const cfg = await getCredentialledCourseConfig(courseID);
1294
- const dsDescriptor = import_common2.NameSpacer.getDataShapeDescriptor(datashapeID);
1295
- let questionViewTypes = [];
1296
- for (const ds of cfg.dataShapes) {
1297
- if (ds.name === datashapeID) {
1298
- questionViewTypes = ds.questionTypes;
1299
- }
1300
- }
1301
- if (questionViewTypes.length === 0) {
1302
- const errorMsg = `No questionViewTypes found for datashapeID: ${datashapeID} in course config. Cards cannot be created.`;
1303
- logger.error(errorMsg);
1304
- throw new Error(errorMsg);
1305
- }
1306
- for (const questionView of questionViewTypes) {
1307
- await createCard(questionView, courseID, dsDescriptor, noteID, tags, elo, author);
1308
- }
1309
- }
1310
- async function createCard(questionViewName, courseID, dsDescriptor, noteID, tags, elo = (0, import_common3.blankCourseElo)(), author) {
1311
- const qDescriptor = import_common2.NameSpacer.getQuestionDescriptor(questionViewName);
1312
- const cfg = await getCredentialledCourseConfig(courseID);
1313
- for (const rQ of cfg.questionTypes) {
1314
- if (rQ.name === questionViewName) {
1315
- for (const view of rQ.viewList) {
1316
- await addCard(
1317
- courseID,
1318
- dsDescriptor.course,
1319
- [noteID],
1320
- import_common2.NameSpacer.getViewString({
1321
- course: qDescriptor.course,
1322
- questionType: qDescriptor.questionType,
1323
- view
1324
- }),
1325
- elo,
1326
- tags,
1327
- author
1328
- );
1329
- }
1330
- }
1331
- }
1332
- }
1333
- async function addCard(courseID, course, id_displayable_data, id_view, elo, tags, author) {
1334
- const db = getCourseDB(courseID);
1335
- const card = await db.post({
1336
- course,
1337
- id_displayable_data,
1338
- id_view,
1339
- docType: "CARD" /* CARD */,
1340
- elo: elo || (0, import_common3.toCourseElo)(990 + Math.round(20 * Math.random())),
1341
- author
1342
- });
1343
- for (const tag of tags) {
1344
- logger.info(`adding tag: ${tag} to card ${card.id}`);
1345
- await addTagToCard(courseID, card.id, tag, author, false);
1346
- }
1347
- return card;
1348
- }
1349
- async function getCredentialledCourseConfig(courseID) {
1350
- try {
1351
- const db = getCourseDB(courseID);
1352
- const ret = await db.get("CourseConfig");
1353
- ret.courseID = courseID;
1354
- logger.info(`Returning course config: ${JSON.stringify(ret)}`);
1355
- return ret;
1356
- } catch (e) {
1357
- logger.error(`Error fetching config for ${courseID}:`, e);
1358
- throw e;
1359
- }
1360
- }
1361
- async function addTagToCard(courseID, cardID, tagID, author, updateELO = true) {
1362
- const prefixedTagID = getTagID(tagID);
1363
- const courseDB = getCourseDB(courseID);
1364
- const courseApi = new CourseDB(courseID, async () => {
1365
- const dummySyncStrategy = {
1366
- setupRemoteDB: () => null,
1367
- startSync: () => {
1368
- },
1369
- canCreateAccount: () => false,
1370
- canAuthenticate: () => false,
1371
- getCurrentUsername: async () => "DummyUser"
1372
- };
1373
- return BaseUser.Dummy(dummySyncStrategy);
1374
- });
1375
- try {
1376
- logger.info(`Applying tag ${tagID} to card ${courseID + "-" + cardID}...`);
1377
- const tag = await courseDB.get(prefixedTagID);
1378
- if (!tag.taggedCards.includes(cardID)) {
1379
- tag.taggedCards.push(cardID);
1380
- if (updateELO) {
1381
- try {
1382
- const eloData = await courseApi.getCardEloData([cardID]);
1383
- const elo = eloData[0];
1384
- elo.tags[tagID] = {
1385
- count: 0,
1386
- score: elo.global.score
1387
- // todo: or 1000?
1276
+ return selectedCards.map((c) => {
1277
+ const split = c.split("-");
1278
+ return {
1279
+ courseID: this.id,
1280
+ cardID: split[1],
1281
+ qualifiedID: `${split[0]}-${split[1]}`,
1282
+ contentSourceType: "course",
1283
+ contentSourceID: this.id,
1284
+ status: "new"
1388
1285
  };
1389
- await updateCardElo(courseID, cardID, elo);
1390
- } catch (error) {
1391
- logger.error("Failed to update ELO data for card:", cardID, error);
1392
- }
1393
- }
1394
- return courseDB.put(tag);
1395
- } else throw new AlreadyTaggedErr(`Card ${cardID} is already tagged with ${tagID}`);
1396
- } catch (e) {
1397
- if (e instanceof AlreadyTaggedErr) {
1398
- throw e;
1399
- }
1400
- await createTag(courseID, tagID, author);
1401
- return addTagToCard(courseID, cardID, tagID, author, updateELO);
1402
- }
1403
- }
1404
- async function updateCardElo(courseID, cardID, elo) {
1405
- if (elo) {
1406
- const cDB = getCourseDB(courseID);
1407
- const card = await cDB.get(cardID);
1408
- logger.debug(`Replacing ${JSON.stringify(card.elo)} with ${JSON.stringify(elo)}`);
1409
- card.elo = elo;
1410
- return cDB.put(card);
1411
- }
1412
- }
1413
- function getTagID(tagName) {
1414
- const tagPrefix = "TAG" /* TAG */.valueOf() + "-";
1415
- if (tagName.indexOf(tagPrefix) === 0) {
1416
- return tagName;
1417
- } else {
1418
- return tagPrefix + tagName;
1419
- }
1420
- }
1421
- function getCourseDB(courseID) {
1422
- const dbName = `coursedb-${courseID}`;
1423
- return new pouchdb_setup_default(
1424
- ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + dbName,
1425
- pouchDBincludeCredentialsConfig
1426
- );
1427
- }
1428
- var import_common2, import_common3, import_common4, AlreadyTaggedErr;
1429
- var init_courseAPI = __esm({
1430
- "src/impl/couch/courseAPI.ts"() {
1431
- "use strict";
1432
- init_pouchdb_setup();
1433
- init_couch();
1434
- init_factory();
1435
- import_common2 = require("@vue-skuilder/common");
1436
- import_common3 = require("@vue-skuilder/common");
1437
- init_courseDB();
1438
- init_types_legacy();
1439
- import_common4 = require("@vue-skuilder/common");
1440
- init_common();
1441
- init_logger();
1442
- AlreadyTaggedErr = class extends Error {
1443
- constructor(message) {
1444
- super(message);
1445
- this.name = "AlreadyTaggedErr";
1286
+ });
1446
1287
  }
1447
1288
  };
1448
1289
  }
1449
1290
  });
1450
1291
 
1451
- // src/impl/couch/courseLookupDB.ts
1452
- var courseLookupDBTitle, CourseLookup;
1453
- var init_courseLookupDB = __esm({
1454
- "src/impl/couch/courseLookupDB.ts"() {
1292
+ // src/impl/couch/classroomDB.ts
1293
+ function getClassroomDB(classID, version) {
1294
+ const dbName = `classdb-${version}-${classID}`;
1295
+ logger.info(`Retrieving classroom db: ${dbName}`);
1296
+ return new pouchdb_setup_default(
1297
+ ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + dbName,
1298
+ pouchDBincludeCredentialsConfig
1299
+ );
1300
+ }
1301
+ async function getClassroomConfig(classID) {
1302
+ return await getClassroomDB(classID, "student").get(CLASSROOM_CONFIG);
1303
+ }
1304
+ var import_moment, classroomLookupDBTitle, CLASSROOM_CONFIG, ClassroomDBBase, StudentClassroomDB, TeacherClassroomDB, ClassroomLookupDB;
1305
+ var init_classroomDB2 = __esm({
1306
+ "src/impl/couch/classroomDB.ts"() {
1455
1307
  "use strict";
1456
- init_pouchdb_setup();
1457
1308
  init_factory();
1458
1309
  init_logger();
1459
- courseLookupDBTitle = "coursedb-lookup";
1460
- logger.debug(`COURSELOOKUP FILE RUNNING`);
1461
- CourseLookup = class _CourseLookup {
1462
- // [ ] this db should be read only for public, admin-only for write
1463
- // Cache for the PouchDB instance
1464
- static _dbInstance = null;
1465
- /**
1466
- * Static getter for the PouchDB database instance.
1467
- * Connects using ENV variables and caches the instance.
1468
- * Throws an error if required ENV variables are not set.
1469
- */
1470
- static get _db() {
1471
- if (this._dbInstance) {
1472
- return this._dbInstance;
1473
- }
1474
- if (ENV.COUCHDB_SERVER_URL === "NOT_SET" || !ENV.COUCHDB_SERVER_URL) {
1475
- throw new Error(
1476
- "CourseLookup.db: COUCHDB_SERVER_URL is not set. Ensure initializeDataLayer has been called with valid configuration."
1477
- );
1478
- }
1479
- if (ENV.COUCHDB_SERVER_PROTOCOL === "NOT_SET" || !ENV.COUCHDB_SERVER_PROTOCOL) {
1480
- throw new Error(
1481
- "CourseLookup.db: COUCHDB_SERVER_PROTOCOL is not set. Ensure initializeDataLayer has been called with valid configuration."
1482
- );
1483
- }
1484
- const dbUrl = `${ENV.COUCHDB_SERVER_PROTOCOL}://${ENV.COUCHDB_SERVER_URL}/${courseLookupDBTitle}`;
1485
- const options = {
1486
- skip_setup: true
1487
- // Keep the original option
1488
- // fetch: (url, opts) => { // Optional: Add for debugging network requests
1489
- // console.log('PouchDB fetch:', url, opts);
1490
- // return pouch.fetch(url, opts);
1491
- // }
1492
- };
1493
- if (ENV.COUCHDB_USERNAME && ENV.COUCHDB_PASSWORD) {
1494
- options.auth = {
1495
- username: ENV.COUCHDB_USERNAME,
1496
- password: ENV.COUCHDB_PASSWORD
1497
- };
1498
- logger.info(`CourseLookup: Connecting to ${dbUrl} with authentication.`);
1310
+ import_moment = __toESM(require("moment"));
1311
+ init_pouchdb_setup();
1312
+ init_couch();
1313
+ init_courseDB();
1314
+ classroomLookupDBTitle = "classdb-lookup";
1315
+ CLASSROOM_CONFIG = "ClassroomConfig";
1316
+ ClassroomDBBase = class {
1317
+ _id;
1318
+ _db;
1319
+ _cfg;
1320
+ _initComplete = false;
1321
+ _content_prefix = "content";
1322
+ get _content_searchkeys() {
1323
+ return getStartAndEndKeys(this._content_prefix);
1324
+ }
1325
+ async getAssignedContent() {
1326
+ logger.info(`Getting assigned content...`);
1327
+ const docRows = await this._db.allDocs({
1328
+ startkey: this._content_prefix,
1329
+ endkey: this._content_prefix + `\uFFF0`,
1330
+ include_docs: true
1331
+ });
1332
+ const ret = docRows.rows.map((row) => {
1333
+ return row.doc;
1334
+ });
1335
+ return ret;
1336
+ }
1337
+ getContentId(content) {
1338
+ if (content.type === "tag") {
1339
+ return `${this._content_prefix}-${content.courseID}-${content.tagID}`;
1499
1340
  } else {
1500
- logger.info(`CourseLookup: Connecting to ${dbUrl} without authentication.`);
1341
+ return `${this._content_prefix}-${content.courseID}`;
1501
1342
  }
1343
+ }
1344
+ get ready() {
1345
+ return this._initComplete;
1346
+ }
1347
+ getConfig() {
1348
+ return this._cfg;
1349
+ }
1350
+ };
1351
+ StudentClassroomDB = class _StudentClassroomDB extends ClassroomDBBase {
1352
+ // private readonly _prefix: string = 'content';
1353
+ userMessages;
1354
+ _user;
1355
+ constructor(classID, user) {
1356
+ super();
1357
+ this._id = classID;
1358
+ this._user = user;
1359
+ }
1360
+ async init() {
1361
+ const dbName = `classdb-student-${this._id}`;
1362
+ this._db = new pouchdb_setup_default(
1363
+ ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + dbName,
1364
+ pouchDBincludeCredentialsConfig
1365
+ );
1502
1366
  try {
1503
- this._dbInstance = new pouchdb_setup_default(dbUrl, options);
1504
- logger.info(`CourseLookup: Database instance created for ${courseLookupDBTitle}.`);
1505
- return this._dbInstance;
1506
- } catch (error) {
1507
- logger.error(`CourseLookup: Failed to create PouchDB instance for ${dbUrl}`, error);
1508
- this._dbInstance = null;
1509
- throw new Error(
1510
- `CourseLookup: Failed to initialize database connection: ${error instanceof Error ? error.message : String(error)}`
1511
- );
1367
+ const cfg = await this._db.get(CLASSROOM_CONFIG);
1368
+ this._cfg = cfg;
1369
+ this.userMessages = this._db.changes({
1370
+ since: "now",
1371
+ live: true,
1372
+ include_docs: true
1373
+ });
1374
+ this._initComplete = true;
1375
+ return;
1376
+ } catch (e) {
1377
+ throw new Error(`Error in StudentClassroomDB constructor: ${JSON.stringify(e)}`);
1512
1378
  }
1513
1379
  }
1514
- /**
1515
- * Adds a new course to the lookup database, and returns the courseID
1516
- * @param courseName
1517
- * @returns
1518
- */
1519
- static async add(courseName) {
1520
- const resp = await _CourseLookup._db.post({
1521
- name: courseName
1522
- });
1523
- return resp.id;
1380
+ static async factory(classID, user) {
1381
+ const ret = new _StudentClassroomDB(classID, user);
1382
+ await ret.init();
1383
+ return ret;
1524
1384
  }
1525
- /**
1526
- * Removes a course from the index
1527
- * @param courseID
1528
- */
1529
- static async delete(courseID) {
1530
- const doc = await _CourseLookup._db.get(courseID);
1531
- return await _CourseLookup._db.remove(doc);
1385
+ setChangeFcn(f) {
1386
+ void this.userMessages.on("change", f);
1532
1387
  }
1533
- static async allCourses() {
1534
- const resp = await _CourseLookup._db.allDocs({
1535
- include_docs: true
1388
+ async getPendingReviews() {
1389
+ const u = this._user;
1390
+ return (await u.getPendingReviews()).filter((r) => r.scheduledFor === "classroom" && r.schedulingAgentId === this._id).map((r) => {
1391
+ return {
1392
+ ...r,
1393
+ qualifiedID: `${r.courseId}-${r.cardId}`,
1394
+ courseID: r.courseId,
1395
+ cardID: r.cardId,
1396
+ contentSourceType: "classroom",
1397
+ contentSourceID: this._id,
1398
+ reviewID: r._id,
1399
+ status: "review"
1400
+ };
1536
1401
  });
1537
- return resp.rows.map((row) => row.doc);
1538
1402
  }
1539
- static async updateDisambiguator(courseID, disambiguator) {
1540
- const doc = await _CourseLookup._db.get(courseID);
1541
- doc.disambiguator = disambiguator;
1542
- return await _CourseLookup._db.put(doc);
1403
+ async getNewCards() {
1404
+ const activeCards = await this._user.getActiveCards();
1405
+ const now = import_moment.default.utc();
1406
+ const assigned = await this.getAssignedContent();
1407
+ const due = assigned.filter((c) => now.isAfter(import_moment.default.utc(c.activeOn, REVIEW_TIME_FORMAT)));
1408
+ logger.info(`Due content: ${JSON.stringify(due)}`);
1409
+ let ret = [];
1410
+ for (let i = 0; i < due.length; i++) {
1411
+ const content = due[i];
1412
+ if (content.type === "course") {
1413
+ const db = new CourseDB(content.courseID, async () => this._user);
1414
+ ret = ret.concat(await db.getNewCards());
1415
+ } else if (content.type === "tag") {
1416
+ const tagDoc = await getTag(content.courseID, content.tagID);
1417
+ ret = ret.concat(
1418
+ tagDoc.taggedCards.map((c) => {
1419
+ return {
1420
+ courseID: content.courseID,
1421
+ cardID: c,
1422
+ qualifiedID: `${content.courseID}-${c}`,
1423
+ contentSourceType: "classroom",
1424
+ contentSourceID: this._id,
1425
+ status: "new"
1426
+ };
1427
+ })
1428
+ );
1429
+ } else if (content.type === "card") {
1430
+ ret.push(await getCourseDB2(content.courseID).get(content.cardID));
1431
+ }
1432
+ }
1433
+ logger.info(`New Cards from classroom ${this._cfg.name}: ${ret.map((c) => c.qualifiedID)}`);
1434
+ return ret.filter((c) => {
1435
+ if (activeCards.some((ac) => c.qualifiedID.includes(ac))) {
1436
+ return false;
1437
+ } else {
1438
+ return true;
1439
+ }
1440
+ });
1543
1441
  }
1544
- static async isCourse(courseID) {
1442
+ };
1443
+ TeacherClassroomDB = class _TeacherClassroomDB extends ClassroomDBBase {
1444
+ _stuDb;
1445
+ constructor(classID) {
1446
+ super();
1447
+ this._id = classID;
1448
+ }
1449
+ async init() {
1450
+ const dbName = `classdb-teacher-${this._id}`;
1451
+ const stuDbName = `classdb-student-${this._id}`;
1452
+ this._db = new pouchdb_setup_default(
1453
+ ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + dbName,
1454
+ pouchDBincludeCredentialsConfig
1455
+ );
1456
+ this._stuDb = new pouchdb_setup_default(
1457
+ ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + stuDbName,
1458
+ pouchDBincludeCredentialsConfig
1459
+ );
1545
1460
  try {
1546
- await _CourseLookup._db.get(courseID);
1461
+ return this._db.get(CLASSROOM_CONFIG).then((cfg) => {
1462
+ this._cfg = cfg;
1463
+ this._initComplete = true;
1464
+ }).then(() => {
1465
+ return;
1466
+ });
1467
+ } catch (e) {
1468
+ throw new Error(`Error in TeacherClassroomDB constructor: ${JSON.stringify(e)}`);
1469
+ }
1470
+ }
1471
+ static async factory(classID) {
1472
+ const ret = new _TeacherClassroomDB(classID);
1473
+ await ret.init();
1474
+ return ret;
1475
+ }
1476
+ async removeContent(content) {
1477
+ const contentID = this.getContentId(content);
1478
+ try {
1479
+ const doc = await this._db.get(contentID);
1480
+ await this._db.remove(doc);
1481
+ void this._db.replicate.to(this._stuDb, {
1482
+ doc_ids: [contentID]
1483
+ });
1484
+ } catch (error) {
1485
+ logger.error("Failed to remove content:", contentID, error);
1486
+ }
1487
+ }
1488
+ async assignContent(content) {
1489
+ let put;
1490
+ const id = this.getContentId(content);
1491
+ if (content.type === "tag") {
1492
+ put = await this._db.put({
1493
+ courseID: content.courseID,
1494
+ tagID: content.tagID,
1495
+ type: "tag",
1496
+ _id: id,
1497
+ assignedBy: content.assignedBy,
1498
+ assignedOn: import_moment.default.utc(),
1499
+ activeOn: content.activeOn || import_moment.default.utc()
1500
+ });
1501
+ } else {
1502
+ put = await this._db.put({
1503
+ courseID: content.courseID,
1504
+ type: "course",
1505
+ _id: id,
1506
+ assignedBy: content.assignedBy,
1507
+ assignedOn: import_moment.default.utc(),
1508
+ activeOn: content.activeOn || import_moment.default.utc()
1509
+ });
1510
+ }
1511
+ if (put.ok) {
1512
+ void this._db.replicate.to(this._stuDb, {
1513
+ doc_ids: [id]
1514
+ });
1547
1515
  return true;
1548
- } catch (error) {
1549
- logger.info(`Courselookup failed:`, error);
1516
+ } else {
1550
1517
  return false;
1551
1518
  }
1552
1519
  }
1553
1520
  };
1521
+ ClassroomLookupDB = () => new pouchdb_setup_default(ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + classroomLookupDBTitle, {
1522
+ skip_setup: true
1523
+ });
1554
1524
  }
1555
1525
  });
1556
1526
 
1557
- // src/core/navigators/elo.ts
1558
- var elo_exports = {};
1559
- __export(elo_exports, {
1560
- default: () => ELONavigator
1527
+ // src/core/interfaces/contentSource.ts
1528
+ function isReview(item) {
1529
+ const ret = item.status === "review" || item.status === "failed-review" || "reviewID" in item;
1530
+ return ret;
1531
+ }
1532
+ async function getStudySource(source, user) {
1533
+ if (source.type === "classroom") {
1534
+ return await StudentClassroomDB.factory(source.id, user);
1535
+ } else {
1536
+ return getDataLayer().getCourseDB(source.id);
1537
+ }
1538
+ }
1539
+ var init_contentSource = __esm({
1540
+ "src/core/interfaces/contentSource.ts"() {
1541
+ "use strict";
1542
+ init_factory();
1543
+ init_classroomDB2();
1544
+ }
1561
1545
  });
1562
- var ELONavigator;
1563
- var init_elo = __esm({
1564
- "src/core/navigators/elo.ts"() {
1546
+
1547
+ // src/core/interfaces/courseDB.ts
1548
+ var init_courseDB2 = __esm({
1549
+ "src/core/interfaces/courseDB.ts"() {
1565
1550
  "use strict";
1566
- init_navigators();
1567
- ELONavigator = class extends ContentNavigator {
1568
- user;
1569
- course;
1570
- constructor(user, course) {
1571
- super();
1572
- this.user = user;
1573
- this.course = course;
1574
- }
1575
- async getPendingReviews() {
1576
- const reviews = await this.user.getPendingReviews(this.course.getCourseID());
1577
- const elo = await this.course.getCardEloData(reviews.map((r) => r.cardId));
1578
- const ratedReviews = reviews.map((r, i) => {
1579
- const ratedR = {
1580
- ...r,
1581
- ...elo[i]
1582
- };
1583
- return ratedR;
1584
- });
1585
- ratedReviews.sort((a, b) => {
1586
- return a.global.score - b.global.score;
1587
- });
1588
- return ratedReviews.map((r) => {
1589
- return {
1590
- ...r,
1591
- contentSourceType: "course",
1592
- contentSourceID: this.course.getCourseID(),
1593
- cardID: r.cardId,
1594
- courseID: r.courseId,
1595
- qualifiedID: `${r.courseId}-${r.cardId}`,
1596
- reviewID: r._id,
1597
- status: "review"
1598
- };
1599
- });
1600
- }
1601
- async getNewCards(limit = 99) {
1602
- const activeCards = await this.user.getActiveCards();
1603
- return (await this.course.getCardsCenteredAtELO({ limit, elo: "user" }, (c) => {
1604
- if (activeCards.some((ac) => c.includes(ac))) {
1605
- return false;
1606
- } else {
1607
- return true;
1608
- }
1609
- })).map((c) => {
1610
- return {
1611
- ...c,
1612
- status: "new"
1613
- };
1614
- });
1615
- }
1616
- };
1617
1551
  }
1618
1552
  });
1619
1553
 
1620
- // import("./**/*") in src/core/navigators/index.ts
1621
- var globImport;
1622
- var init_ = __esm({
1623
- 'import("./**/*") in src/core/navigators/index.ts'() {
1624
- globImport = __glob({
1625
- "./elo.ts": () => Promise.resolve().then(() => (init_elo(), elo_exports)),
1626
- "./index.ts": () => Promise.resolve().then(() => (init_navigators(), navigators_exports))
1627
- });
1554
+ // src/core/interfaces/dataLayerProvider.ts
1555
+ var init_dataLayerProvider = __esm({
1556
+ "src/core/interfaces/dataLayerProvider.ts"() {
1557
+ "use strict";
1628
1558
  }
1629
1559
  });
1630
1560
 
1631
- // src/core/navigators/index.ts
1632
- var navigators_exports = {};
1633
- __export(navigators_exports, {
1634
- ContentNavigator: () => ContentNavigator,
1635
- Navigators: () => Navigators
1561
+ // src/core/interfaces/userDB.ts
1562
+ var init_userDB = __esm({
1563
+ "src/core/interfaces/userDB.ts"() {
1564
+ "use strict";
1565
+ }
1636
1566
  });
1637
- var Navigators, ContentNavigator;
1638
- var init_navigators = __esm({
1639
- "src/core/navigators/index.ts"() {
1567
+
1568
+ // src/core/interfaces/index.ts
1569
+ var init_interfaces = __esm({
1570
+ "src/core/interfaces/index.ts"() {
1640
1571
  "use strict";
1641
- init_logger();
1642
- init_();
1643
- Navigators = /* @__PURE__ */ ((Navigators2) => {
1644
- Navigators2["ELO"] = "elo";
1645
- return Navigators2;
1646
- })(Navigators || {});
1647
- ContentNavigator = class {
1648
- /**
1649
- *
1650
- * @param user
1651
- * @param strategyData
1652
- * @returns the runtime object used to steer a study session.
1653
- */
1654
- static async create(user, course, strategyData) {
1655
- const implementingClass = strategyData.implementingClass;
1656
- let NavigatorImpl;
1657
- const variations = ["", ".js", ".ts"];
1658
- for (const ext of variations) {
1659
- try {
1660
- const module2 = await globImport(`./${implementingClass}${ext}`);
1661
- NavigatorImpl = module2.default;
1662
- break;
1663
- } catch (e) {
1664
- logger.debug(`Failed to load with extension ${ext}:`, e);
1665
- }
1666
- }
1667
- if (!NavigatorImpl) {
1668
- throw new Error(`Could not load navigator implementation for: ${implementingClass}`);
1669
- }
1670
- return new NavigatorImpl(user, course, strategyData);
1671
- }
1672
- };
1572
+ init_adminDB();
1573
+ init_classroomDB();
1574
+ init_contentSource();
1575
+ init_courseDB2();
1576
+ init_dataLayerProvider();
1577
+ init_userDB();
1673
1578
  }
1674
1579
  });
1675
1580
 
1676
- // src/impl/couch/courseDB.ts
1677
- function randIntWeightedTowardZero(n) {
1678
- return Math.floor(Math.random() * Math.random() * Math.random() * n);
1679
- }
1680
- async function getCourseDataShapes(courseID) {
1681
- const cfg = await getCredentialledCourseConfig(courseID);
1682
- return cfg.dataShapes;
1683
- }
1684
- async function getCredentialledDataShapes(courseID) {
1685
- const cfg = await getCredentialledCourseConfig(courseID);
1686
- return cfg.dataShapes;
1687
- }
1688
- async function getCourseQuestionTypes(courseID) {
1689
- const cfg = await getCredentialledCourseConfig(courseID);
1690
- return cfg.questionTypes;
1691
- }
1692
- async function getCourseTagStubs(courseID) {
1693
- logger.debug(`Getting tag stubs for course: ${courseID}`);
1694
- const stubs = await filterAllDocsByPrefix2(
1695
- getCourseDB2(courseID),
1696
- "TAG" /* TAG */.valueOf() + "-"
1697
- );
1698
- stubs.rows.forEach((row) => {
1699
- logger.debug(` Tag stub for doc: ${row.id}`);
1700
- });
1701
- return stubs;
1581
+ // src/core/types/user.ts
1582
+ var init_user = __esm({
1583
+ "src/core/types/user.ts"() {
1584
+ "use strict";
1585
+ }
1586
+ });
1587
+
1588
+ // src/core/util/index.ts
1589
+ function getCardHistoryID(courseID, cardID) {
1590
+ return `${DocTypePrefixes["CARDRECORD" /* CARDRECORD */]}-${courseID}-${cardID}`;
1702
1591
  }
1703
- async function deleteTag(courseID, tagName) {
1704
- tagName = getTagID(tagName);
1705
- const courseDB = getCourseDB2(courseID);
1706
- const doc = await courseDB.get("TAG" /* TAG */.valueOf() + "-" + tagName);
1707
- const resp = await courseDB.remove(doc);
1708
- return resp;
1592
+ var init_util = __esm({
1593
+ "src/core/util/index.ts"() {
1594
+ "use strict";
1595
+ init_types_legacy();
1596
+ }
1597
+ });
1598
+
1599
+ // src/core/bulkImport/cardProcessor.ts
1600
+ var import_common6;
1601
+ var init_cardProcessor = __esm({
1602
+ "src/core/bulkImport/cardProcessor.ts"() {
1603
+ "use strict";
1604
+ import_common6 = require("@vue-skuilder/common");
1605
+ init_logger();
1606
+ }
1607
+ });
1608
+
1609
+ // src/core/bulkImport/types.ts
1610
+ var init_types = __esm({
1611
+ "src/core/bulkImport/types.ts"() {
1612
+ "use strict";
1613
+ }
1614
+ });
1615
+
1616
+ // src/core/bulkImport/index.ts
1617
+ var init_bulkImport = __esm({
1618
+ "src/core/bulkImport/index.ts"() {
1619
+ "use strict";
1620
+ init_cardProcessor();
1621
+ init_types();
1622
+ }
1623
+ });
1624
+
1625
+ // src/core/index.ts
1626
+ var init_core = __esm({
1627
+ "src/core/index.ts"() {
1628
+ "use strict";
1629
+ init_interfaces();
1630
+ init_types_legacy();
1631
+ init_user();
1632
+ init_Loggable();
1633
+ init_util();
1634
+ init_navigators();
1635
+ init_bulkImport();
1636
+ }
1637
+ });
1638
+
1639
+ // src/util/tuiLogger.ts
1640
+ var init_tuiLogger = __esm({
1641
+ "src/util/tuiLogger.ts"() {
1642
+ "use strict";
1643
+ init_dataDirectory();
1644
+ }
1645
+ });
1646
+
1647
+ // src/util/dataDirectory.ts
1648
+ function getAppDataDirectory() {
1649
+ return path.join(os.homedir(), ".tuilder");
1709
1650
  }
1710
- async function createTag(courseID, tagName, author) {
1711
- logger.debug(`Creating tag: ${tagName}...`);
1712
- const tagID = getTagID(tagName);
1713
- const courseDB = getCourseDB2(courseID);
1714
- const resp = await courseDB.put({
1715
- course: courseID,
1716
- docType: "TAG" /* TAG */,
1717
- name: tagName,
1718
- snippet: "",
1719
- taggedCards: [],
1720
- wiki: "",
1721
- author,
1722
- _id: tagID
1723
- });
1724
- return resp;
1651
+ function getDbPath(dbName) {
1652
+ return path.join(getAppDataDirectory(), dbName);
1725
1653
  }
1726
- async function updateTag(tag) {
1727
- const prior = await getTag(tag.course, tag.name);
1728
- return await getCourseDB2(tag.course).put({
1729
- ...tag,
1730
- _rev: prior._rev
1731
- });
1654
+ var path, os;
1655
+ var init_dataDirectory = __esm({
1656
+ "src/util/dataDirectory.ts"() {
1657
+ "use strict";
1658
+ path = __toESM(require("path"));
1659
+ os = __toESM(require("os"));
1660
+ init_tuiLogger();
1661
+ }
1662
+ });
1663
+
1664
+ // src/impl/common/userDBHelpers.ts
1665
+ function hexEncode(str) {
1666
+ let hex;
1667
+ let returnStr = "";
1668
+ for (let i = 0; i < str.length; i++) {
1669
+ hex = str.charCodeAt(i).toString(16);
1670
+ returnStr += ("000" + hex).slice(3);
1671
+ }
1672
+ return returnStr;
1732
1673
  }
1733
- async function getTag(courseID, tagName) {
1734
- const tagID = getTagID(tagName);
1735
- const courseDB = getCourseDB2(courseID);
1736
- return courseDB.get(tagID);
1674
+ function filterAllDocsByPrefix2(db, prefix, opts) {
1675
+ const options = {
1676
+ startkey: prefix,
1677
+ endkey: prefix + "\uFFF0",
1678
+ include_docs: true
1679
+ };
1680
+ if (opts) {
1681
+ Object.assign(options, opts);
1682
+ }
1683
+ return db.allDocs(options);
1737
1684
  }
1738
- async function removeTagFromCard(courseID, cardID, tagID) {
1739
- tagID = getTagID(tagID);
1740
- const courseDB = getCourseDB2(courseID);
1741
- const tag = await courseDB.get(tagID);
1742
- tag.taggedCards = tag.taggedCards.filter((taggedID) => {
1743
- return cardID !== taggedID;
1685
+ function getStartAndEndKeys2(key) {
1686
+ return {
1687
+ startkey: key,
1688
+ endkey: key + "\uFFF0"
1689
+ };
1690
+ }
1691
+ function updateGuestAccountExpirationDate(guestDB) {
1692
+ const currentTime = import_moment2.default.utc();
1693
+ const expirationDate = currentTime.add(2, "months").toISOString();
1694
+ const expiryDocID2 = "GuestAccountExpirationDate";
1695
+ void guestDB.get(expiryDocID2).then((doc) => {
1696
+ return guestDB.put({
1697
+ _id: expiryDocID2,
1698
+ _rev: doc._rev,
1699
+ date: expirationDate
1700
+ });
1701
+ }).catch(() => {
1702
+ return guestDB.put({
1703
+ _id: expiryDocID2,
1704
+ date: expirationDate
1705
+ });
1744
1706
  });
1745
- return courseDB.put(tag);
1746
1707
  }
1747
- function getAncestorTagIDs(courseID, tagID) {
1748
- tagID = getTagID(tagID);
1749
- const split = tagID.split(">");
1750
- if (split.length === 1) {
1751
- return [];
1708
+ function getLocalUserDB(username) {
1709
+ const dbName = `userdb-${username}`;
1710
+ if (typeof window === "undefined") {
1711
+ return new pouchdb_setup_default(getDbPath(dbName), {});
1752
1712
  } else {
1753
- split.pop();
1754
- const parent = split.join(">");
1755
- return [parent].concat(getAncestorTagIDs(courseID, parent));
1713
+ return new pouchdb_setup_default(dbName, {});
1756
1714
  }
1757
1715
  }
1758
- async function getChildTagStubs(courseID, tagID) {
1759
- return await filterAllDocsByPrefix2(getCourseDB2(courseID), tagID + ">");
1716
+ function scheduleCardReviewLocal(userDB, review) {
1717
+ const now = import_moment2.default.utc();
1718
+ logger.info(`Scheduling for review in: ${review.time.diff(now, "h") / 24} days`);
1719
+ void userDB.put({
1720
+ _id: DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */] + review.time.format(REVIEW_TIME_FORMAT2),
1721
+ cardId: review.card_id,
1722
+ reviewTime: review.time.toISOString(),
1723
+ courseId: review.course_id,
1724
+ scheduledAt: now.toISOString(),
1725
+ scheduledFor: review.scheduledFor,
1726
+ schedulingAgentId: review.schedulingAgentId
1727
+ });
1760
1728
  }
1761
- async function getAppliedTags(id_course, id_card) {
1762
- const db = getCourseDB2(id_course);
1763
- const result = await db.query("getTags", {
1764
- startkey: id_card,
1765
- endkey: id_card
1766
- // include_docs: true
1729
+ async function removeScheduledCardReviewLocal(userDB, reviewDocID) {
1730
+ const reviewDoc = await userDB.get(reviewDocID);
1731
+ userDB.remove(reviewDoc).then((res) => {
1732
+ if (res.ok) {
1733
+ log2(`Removed Review Doc: ${reviewDocID}`);
1734
+ }
1735
+ }).catch((err) => {
1736
+ log2(`Failed to remove Review Doc: ${reviewDocID},
1737
+ ${JSON.stringify(err)}`);
1767
1738
  });
1768
- return result;
1769
1739
  }
1770
- async function updateCardElo2(courseID, cardID, elo) {
1771
- if (elo) {
1772
- const cDB = getCourseDB2(courseID);
1773
- const card = await cDB.get(cardID);
1774
- logger.debug(`Replacing ${JSON.stringify(card.elo)} with ${JSON.stringify(elo)}`);
1775
- card.elo = elo;
1776
- return cDB.put(card);
1740
+ var import_moment2, REVIEW_TIME_FORMAT2, log2;
1741
+ var init_userDBHelpers = __esm({
1742
+ "src/impl/common/userDBHelpers.ts"() {
1743
+ "use strict";
1744
+ import_moment2 = __toESM(require("moment"));
1745
+ init_core();
1746
+ init_logger();
1747
+ init_pouchdb_setup();
1748
+ init_dataDirectory();
1749
+ REVIEW_TIME_FORMAT2 = "YYYY-MM-DD--kk:mm:ss-SSS";
1750
+ log2 = (s) => {
1751
+ logger.info(s);
1752
+ };
1777
1753
  }
1778
- }
1779
- async function updateCredentialledCourseConfig(courseID, config) {
1780
- logger.debug(`Updating course config:
1754
+ });
1781
1755
 
1782
- ${JSON.stringify(config)}
1783
- `);
1784
- const db = getCourseDB2(courseID);
1785
- const old = await getCredentialledCourseConfig(courseID);
1786
- return await db.put({
1787
- ...config,
1788
- _rev: old._rev
1789
- });
1790
- }
1791
- function isSuccessRow(row) {
1792
- return "doc" in row && row.doc !== null && row.doc !== void 0;
1793
- }
1794
- var import_common6, CoursesDB, CourseDB;
1795
- var init_courseDB = __esm({
1796
- "src/impl/couch/courseDB.ts"() {
1756
+ // src/impl/couch/user-course-relDB.ts
1757
+ var import_moment3, UsrCrsData;
1758
+ var init_user_course_relDB = __esm({
1759
+ "src/impl/couch/user-course-relDB.ts"() {
1797
1760
  "use strict";
1798
- import_common6 = require("@vue-skuilder/common");
1799
- init_couch();
1800
- init_updateQueue();
1801
- init_types_legacy();
1761
+ import_moment3 = __toESM(require("moment"));
1802
1762
  init_logger();
1803
- init_clientCache();
1804
- init_courseAPI();
1805
- init_courseLookupDB();
1806
- init_navigators();
1807
- CoursesDB = class {
1808
- _courseIDs;
1809
- constructor(courseIDs) {
1810
- if (courseIDs && courseIDs.length > 0) {
1811
- this._courseIDs = courseIDs;
1812
- } else {
1813
- this._courseIDs = void 0;
1814
- }
1763
+ UsrCrsData = class {
1764
+ user;
1765
+ _courseId;
1766
+ constructor(user, courseId) {
1767
+ this.user = user;
1768
+ this._courseId = courseId;
1815
1769
  }
1816
- async getCourseList() {
1817
- let crsList = await CourseLookup.allCourses();
1818
- logger.debug(`AllCourses: ${crsList.map((c) => c.name + ", " + c._id + "\n ")}`);
1819
- if (this._courseIDs) {
1820
- crsList = crsList.filter((c) => this._courseIDs.includes(c._id));
1821
- }
1822
- logger.debug(`AllCourses.filtered: ${crsList.map((c) => c.name + ", " + c._id + "\n ")}`);
1823
- const cfgs = await Promise.all(
1824
- crsList.map(async (c) => {
1825
- try {
1826
- const cfg = await getCredentialledCourseConfig(c._id);
1827
- logger.debug(`Found cfg: ${JSON.stringify(cfg)}`);
1828
- return cfg;
1829
- } catch (e) {
1830
- logger.warn(`Error fetching cfg for course ${c.name}, ${c._id}: ${e}`);
1831
- return void 0;
1832
- }
1833
- })
1834
- );
1835
- return cfgs.filter((c) => !!c);
1770
+ async getReviewsForcast(daysCount) {
1771
+ const time = import_moment3.default.utc().add(daysCount, "days");
1772
+ return this.getReviewstoDate(time);
1836
1773
  }
1837
- async getCourseConfig(courseId) {
1838
- if (this._courseIDs && this._courseIDs.length && !this._courseIDs.includes(courseId)) {
1839
- throw new Error(`Course ${courseId} not in course list`);
1840
- }
1841
- const cfg = await getCredentialledCourseConfig(courseId);
1842
- if (cfg === void 0) {
1843
- throw new Error(`Error fetching cfg for course ${courseId}`);
1774
+ async getPendingReviews() {
1775
+ const now = import_moment3.default.utc();
1776
+ return this.getReviewstoDate(now);
1777
+ }
1778
+ async getScheduledReviewCount() {
1779
+ return (await this.getPendingReviews()).length;
1780
+ }
1781
+ async getCourseSettings() {
1782
+ const regDoc = await this.user.getCourseRegistrationsDoc();
1783
+ const crsDoc = regDoc.courses.find((c) => c.courseID === this._courseId);
1784
+ if (crsDoc && crsDoc.settings) {
1785
+ return crsDoc.settings;
1844
1786
  } else {
1845
- return cfg;
1787
+ logger.warn(`no settings found during lookup on course ${this._courseId}`);
1788
+ return {};
1846
1789
  }
1847
1790
  }
1848
- async disambiguateCourse(courseId, disambiguator) {
1849
- await CourseLookup.updateDisambiguator(courseId, disambiguator);
1791
+ updateCourseSettings(updates) {
1792
+ if ("updateCourseSettings" in this.user) {
1793
+ void this.user.updateCourseSettings(this._courseId, updates);
1794
+ }
1795
+ }
1796
+ async getReviewstoDate(targetDate) {
1797
+ const allReviews = await this.user.getPendingReviews(this._courseId);
1798
+ logger.debug(
1799
+ `Fetching ${this.user.getUsername()}'s scheduled reviews for course ${this._courseId}.`
1800
+ );
1801
+ return allReviews.filter((review) => {
1802
+ const reviewTime = import_moment3.default.utc(review.reviewTime);
1803
+ return targetDate.isAfter(reviewTime);
1804
+ });
1850
1805
  }
1851
1806
  };
1852
- CourseDB = class {
1853
- // private log(msg: string): void {
1854
- // log(`CourseLog: ${this.id}\n ${msg}`);
1855
- // }
1856
- db;
1857
- id;
1858
- _getCurrentUser;
1859
- updateQueue;
1860
- constructor(id, userLookup) {
1861
- this.id = id;
1862
- this.db = getCourseDB2(this.id);
1863
- this._getCurrentUser = userLookup;
1864
- this.updateQueue = new UpdateQueue(this.db);
1807
+ }
1808
+ });
1809
+
1810
+ // src/impl/common/BaseUserDB.ts
1811
+ async function getOrCreateClassroomRegistrationsDoc(user) {
1812
+ let ret;
1813
+ try {
1814
+ ret = await getLocalUserDB(user).get(userClassroomsDoc);
1815
+ } catch (e) {
1816
+ const err = e;
1817
+ if (err.status === 404) {
1818
+ await getLocalUserDB(user).put({
1819
+ _id: userClassroomsDoc,
1820
+ registrations: []
1821
+ });
1822
+ ret = await getOrCreateClassroomRegistrationsDoc(user);
1823
+ } else {
1824
+ const errorDetails = {
1825
+ name: err.name,
1826
+ status: err.status,
1827
+ message: err.message,
1828
+ reason: err.reason,
1829
+ error: err.error
1830
+ };
1831
+ logger.error(
1832
+ "Database error in getOrCreateClassroomRegistrationsDoc (standalone function):",
1833
+ errorDetails
1834
+ );
1835
+ throw new Error(
1836
+ `Database error accessing classroom registrations: ${err.message || err.name || "Unknown error"} (status: ${err.status})`
1837
+ );
1838
+ }
1839
+ }
1840
+ return ret;
1841
+ }
1842
+ async function getOrCreateCourseRegistrationsDoc(user) {
1843
+ let ret;
1844
+ try {
1845
+ ret = await getLocalUserDB(user).get(userCoursesDoc);
1846
+ } catch (e) {
1847
+ const err = e;
1848
+ if (err.status === 404) {
1849
+ await getLocalUserDB(user).put({
1850
+ _id: userCoursesDoc,
1851
+ courses: [],
1852
+ studyWeight: {}
1853
+ });
1854
+ ret = await getOrCreateCourseRegistrationsDoc(user);
1855
+ } else {
1856
+ throw new Error(
1857
+ `Unexpected error ${JSON.stringify(e)} in getOrCreateCourseRegistrationDoc...`
1858
+ );
1859
+ }
1860
+ }
1861
+ return ret;
1862
+ }
1863
+ async function updateUserElo(user, course_id, elo) {
1864
+ const regDoc = await getOrCreateCourseRegistrationsDoc(user);
1865
+ const course = regDoc.courses.find((c) => c.courseID === course_id);
1866
+ course.elo = elo;
1867
+ return getLocalUserDB(user).put(regDoc);
1868
+ }
1869
+ async function registerUserForClassroom(user, classID, registerAs) {
1870
+ log3(`Registering user: ${user} in course: ${classID}`);
1871
+ return getOrCreateClassroomRegistrationsDoc(user).then((doc) => {
1872
+ const regItem = {
1873
+ classID,
1874
+ registeredAs: registerAs
1875
+ };
1876
+ if (doc.registrations.filter((reg) => {
1877
+ return reg.classID === regItem.classID && reg.registeredAs === regItem.registeredAs;
1878
+ }).length === 0) {
1879
+ doc.registrations.push(regItem);
1880
+ } else {
1881
+ log3(`User ${user} is already registered for class ${classID}`);
1882
+ }
1883
+ return getLocalUserDB(user).put(doc);
1884
+ });
1885
+ }
1886
+ async function dropUserFromClassroom(user, classID) {
1887
+ return getOrCreateClassroomRegistrationsDoc(user).then((doc) => {
1888
+ let index = -1;
1889
+ for (let i = 0; i < doc.registrations.length; i++) {
1890
+ if (doc.registrations[i].classID === classID) {
1891
+ index = i;
1865
1892
  }
1866
- getCourseID() {
1867
- return this.id;
1893
+ }
1894
+ if (index !== -1) {
1895
+ doc.registrations.splice(index, 1);
1896
+ }
1897
+ return getLocalUserDB(user).put(doc);
1898
+ });
1899
+ }
1900
+ async function getUserClassrooms(user) {
1901
+ return getOrCreateClassroomRegistrationsDoc(user);
1902
+ }
1903
+ var import_common7, import_moment4, log3, BaseUser, userCoursesDoc, userClassroomsDoc;
1904
+ var init_BaseUserDB = __esm({
1905
+ "src/impl/common/BaseUserDB.ts"() {
1906
+ "use strict";
1907
+ init_core();
1908
+ init_util();
1909
+ import_common7 = require("@vue-skuilder/common");
1910
+ import_moment4 = __toESM(require("moment"));
1911
+ init_types_legacy();
1912
+ init_logger();
1913
+ init_userDBHelpers();
1914
+ init_updateQueue();
1915
+ init_user_course_relDB();
1916
+ init_couch();
1917
+ log3 = (s) => {
1918
+ logger.info(s);
1919
+ };
1920
+ BaseUser = class _BaseUser {
1921
+ static _instance;
1922
+ static _initialized = false;
1923
+ static Dummy(syncStrategy) {
1924
+ return new _BaseUser("Me", syncStrategy);
1868
1925
  }
1869
- async getCourseInfo() {
1870
- const cardCount = (await this.db.find({
1871
- selector: {
1872
- docType: "CARD" /* CARD */
1873
- },
1874
- limit: 1e3
1875
- })).docs.length;
1876
- return {
1877
- cardCount,
1878
- registeredUsers: 0
1879
- };
1926
+ static DOC_IDS = {
1927
+ CONFIG: "CONFIG",
1928
+ COURSE_REGISTRATIONS: "CourseRegistrations",
1929
+ CLASSROOM_REGISTRATIONS: "ClassroomRegistrations"
1930
+ };
1931
+ // private email: string;
1932
+ _username;
1933
+ syncStrategy;
1934
+ getUsername() {
1935
+ return this._username;
1880
1936
  }
1881
- async getInexperiencedCards(limit = 2) {
1882
- return (await this.db.query("cardsByInexperience", {
1883
- limit
1884
- })).rows.map((r) => {
1885
- const ret = {
1886
- courseId: this.id,
1887
- cardId: r.id,
1888
- count: r.key,
1889
- elo: r.value
1890
- };
1891
- return ret;
1892
- });
1937
+ isLoggedIn() {
1938
+ return !this._username.startsWith(GuestUsername);
1893
1939
  }
1894
- async getCardsByEloLimits(options = {
1895
- low: 0,
1896
- high: Number.MIN_SAFE_INTEGER,
1897
- limit: 25,
1898
- page: 0
1899
- }) {
1900
- return (await this.db.query("elo", {
1901
- startkey: options.low,
1902
- endkey: options.high,
1903
- limit: options.limit,
1904
- skip: options.limit * options.page
1905
- })).rows.map((r) => {
1906
- return `${this.id}-${r.id}-${r.key}`;
1907
- });
1940
+ remote() {
1941
+ return this.remoteDB;
1908
1942
  }
1909
- async getCardEloData(id) {
1910
- const docs = await this.db.allDocs({
1911
- keys: id,
1912
- include_docs: true
1913
- });
1914
- const ret = [];
1915
- docs.rows.forEach((r) => {
1916
- if (isSuccessRow(r)) {
1917
- if (r.doc && r.doc.elo) {
1918
- ret.push((0, import_common6.toCourseElo)(r.doc.elo));
1919
- } else {
1920
- logger.warn("no elo data for card: " + r.id);
1921
- ret.push((0, import_common6.blankCourseElo)());
1922
- }
1923
- } else {
1924
- logger.warn("no elo data for card: " + JSON.stringify(r));
1925
- ret.push((0, import_common6.blankCourseElo)());
1943
+ localDB;
1944
+ remoteDB;
1945
+ writeDB;
1946
+ // Database to use for write operations (local-first approach)
1947
+ updateQueue;
1948
+ async createAccount(username, password) {
1949
+ if (!this.syncStrategy.canCreateAccount()) {
1950
+ throw new Error("Account creation not supported by current sync strategy");
1951
+ }
1952
+ if (!this._username.startsWith(GuestUsername)) {
1953
+ throw new Error(
1954
+ `Cannot create a new account while logged in:
1955
+ Currently logged-in as ${this._username}.`
1956
+ );
1957
+ }
1958
+ const result = await this.syncStrategy.createAccount(username, password);
1959
+ if (result.status === import_common7.Status.ok) {
1960
+ log3(`Account created successfully, updating username to ${username}`);
1961
+ this._username = username;
1962
+ try {
1963
+ localStorage.removeItem("dbUUID");
1964
+ } catch (e) {
1965
+ logger.warn("localStorage not available (Node.js environment):", e);
1926
1966
  }
1927
- });
1928
- return ret;
1929
- }
1930
- /**
1931
- * Returns the lowest and highest `global` ELO ratings in the course
1932
- */
1933
- async getELOBounds() {
1934
- const [low, high] = await Promise.all([
1935
- (await this.db.query("elo", {
1936
- startkey: 0,
1937
- limit: 1,
1938
- include_docs: false
1939
- })).rows[0].key,
1940
- (await this.db.query("elo", {
1941
- limit: 1,
1942
- descending: true,
1943
- startkey: 1e5
1944
- })).rows[0].key
1945
- ]);
1967
+ await this.init();
1968
+ }
1946
1969
  return {
1947
- low,
1948
- high
1970
+ status: result.status,
1971
+ error: result.error || ""
1949
1972
  };
1950
1973
  }
1951
- async removeCard(id) {
1952
- const doc = await this.db.get(id);
1953
- if (!doc.docType || !(doc.docType === "CARD" /* CARD */)) {
1954
- throw new Error(`failed to remove ${id} from course ${this.id}. id does not point to a card`);
1974
+ async login(username, password) {
1975
+ if (!this.syncStrategy.canAuthenticate()) {
1976
+ throw new Error("Authentication not supported by current sync strategy");
1955
1977
  }
1956
- return this.db.remove(doc);
1957
- }
1958
- async getCardDisplayableDataIDs(id) {
1959
- logger.debug(id.join(", "));
1960
- const cards = await this.db.allDocs({
1961
- keys: id,
1962
- include_docs: true
1963
- });
1964
- const ret = {};
1965
- cards.rows.forEach((r) => {
1966
- if (isSuccessRow(r)) {
1967
- ret[r.id] = r.doc.id_displayable_data;
1968
- }
1969
- });
1970
- await Promise.all(
1971
- cards.rows.map((r) => {
1972
- return async () => {
1973
- if (isSuccessRow(r)) {
1974
- ret[r.id] = r.doc.id_displayable_data;
1975
- }
1976
- };
1977
- })
1978
- );
1979
- return ret;
1980
- }
1981
- async getCardsByELO(elo, cardLimit) {
1982
- elo = parseInt(elo);
1983
- const limit = cardLimit ? cardLimit : 25;
1984
- const below = await this.db.query("elo", {
1985
- limit: Math.ceil(limit / 2),
1986
- startkey: elo,
1987
- descending: true
1988
- });
1989
- const aboveLimit = limit - below.rows.length;
1990
- const above = await this.db.query("elo", {
1991
- limit: aboveLimit,
1992
- startkey: elo + 1
1993
- });
1994
- let cards = below.rows;
1995
- cards = cards.concat(above.rows);
1996
- const ret = cards.sort((a, b) => {
1997
- const s = Math.abs(a.key - elo) - Math.abs(b.key - elo);
1998
- if (s === 0) {
1999
- return Math.random() - 0.5;
2000
- } else {
2001
- return s;
1978
+ if (!this._username.startsWith(GuestUsername) && this._username != username) {
1979
+ if (this._username != username) {
1980
+ throw new Error(`Cannot change accounts while logged in.
1981
+ Log out of account ${this.getUsername()} before logging in as ${username}.`);
2002
1982
  }
2003
- }).map((c) => `${this.id}-${c.id}-${c.key}`);
2004
- const str = `below:
2005
- ${below.rows.map((r) => ` ${r.id}-${r.key}
2006
- `)}
2007
-
2008
- above:
2009
- ${above.rows.map((r) => ` ${r.id}-${r.key}
2010
- `)}`;
2011
- logger.debug(`Getting ${limit} cards centered around elo: ${elo}:
2012
-
2013
- ` + str);
2014
- return ret;
2015
- }
2016
- async getCourseConfig() {
2017
- const ret = await getCredentialledCourseConfig(this.id);
2018
- if (ret) {
2019
- return ret;
2020
- } else {
2021
- throw new Error(`Course config not found for course ID: ${this.id}`);
1983
+ logger.warn(`User ${this._username} is already logged in, but executing login again.`);
2022
1984
  }
2023
- }
2024
- async updateCourseConfig(cfg) {
2025
- logger.debug(`Updating: ${JSON.stringify(cfg)}`);
2026
- try {
2027
- return await updateCredentialledCourseConfig(this.id, cfg);
2028
- } catch (error) {
2029
- logger.error(`Error updating course config in course DB: ${error}`);
2030
- throw error;
1985
+ const loginResult = await this.syncStrategy.authenticate(username, password);
1986
+ if (loginResult.ok) {
1987
+ log3(`Logged in as ${username}`);
1988
+ this._username = username;
1989
+ try {
1990
+ localStorage.removeItem("dbUUID");
1991
+ } catch (e) {
1992
+ logger.warn("localStorage not available (Node.js environment):", e);
1993
+ }
1994
+ await this.init();
2031
1995
  }
1996
+ return loginResult;
2032
1997
  }
2033
- async updateCardElo(cardId, elo) {
2034
- if (!elo) {
2035
- throw new Error(`Cannot update card elo with null or undefined value for card ID: ${cardId}`);
1998
+ async resetUserData() {
1999
+ if (this.syncStrategy.canAuthenticate()) {
2000
+ return {
2001
+ status: import_common7.Status.error,
2002
+ error: "Reset user data is only available for local-only mode. Use logout instead for remote sync."
2003
+ };
2036
2004
  }
2037
2005
  try {
2038
- const result = await this.updateQueue.update(cardId, (card) => {
2039
- logger.debug(`Replacing ${JSON.stringify(card.elo)} with ${JSON.stringify(elo)}`);
2040
- card.elo = elo;
2041
- return card;
2042
- });
2043
- return { ok: true, id: cardId, rev: result._rev };
2006
+ const localDB = getLocalUserDB(this._username);
2007
+ const allDocs = await localDB.allDocs({ include_docs: false });
2008
+ const docsToDelete = allDocs.rows.filter((row) => {
2009
+ const id = row.id;
2010
+ return id.startsWith(DocTypePrefixes["CARDRECORD" /* CARDRECORD */]) || // Card interaction history
2011
+ id.startsWith(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */]) || // Scheduled reviews
2012
+ id === _BaseUser.DOC_IDS.COURSE_REGISTRATIONS || // Course registrations
2013
+ id === _BaseUser.DOC_IDS.CLASSROOM_REGISTRATIONS || // Classroom registrations
2014
+ id === _BaseUser.DOC_IDS.CONFIG;
2015
+ }).map((row) => ({ _id: row.id, _rev: row.value.rev, _deleted: true }));
2016
+ if (docsToDelete.length > 0) {
2017
+ await localDB.bulkDocs(docsToDelete);
2018
+ }
2019
+ await this.init();
2020
+ return { status: import_common7.Status.ok };
2044
2021
  } catch (error) {
2045
- logger.error(`Failed to update card elo for card ID: ${cardId}`, error);
2046
- throw new Error(`Failed to update card elo for card ID: ${cardId}`);
2047
- }
2048
- }
2049
- async getAppliedTags(cardId) {
2050
- const ret = await getAppliedTags(this.id, cardId);
2051
- if (ret) {
2052
- return ret;
2053
- } else {
2054
- throw new Error(`Failed to find tags for card ${this.id}-${cardId}`);
2022
+ logger.error("Failed to reset user data:", error);
2023
+ return {
2024
+ status: import_common7.Status.error,
2025
+ error: error instanceof Error ? error.message : "Unknown error during reset"
2026
+ };
2055
2027
  }
2056
2028
  }
2057
- async addTagToCard(cardId, tagId, updateELO) {
2058
- return await addTagToCard(this.id, cardId, tagId, (await this._getCurrentUser()).getUsername(), updateELO);
2059
- }
2060
- async removeTagFromCard(cardId, tagId) {
2061
- return await removeTagFromCard(this.id, cardId, tagId);
2062
- }
2063
- async createTag(name, author) {
2064
- return await createTag(this.id, name, author);
2065
- }
2066
- async getTag(tagId) {
2067
- return await getTag(this.id, tagId);
2068
- }
2069
- async updateTag(tag) {
2070
- if (tag.course !== this.id) {
2071
- throw new Error(`Tag ${JSON.stringify(tag)} does not belong to course ${this.id}`);
2029
+ async logout() {
2030
+ if (!this.syncStrategy.canAuthenticate()) {
2031
+ this._username = await this.syncStrategy.getCurrentUsername();
2032
+ await this.init();
2033
+ return { ok: true };
2072
2034
  }
2073
- return await updateTag(tag);
2035
+ const ret = await this.syncStrategy.logout();
2036
+ this._username = await this.syncStrategy.getCurrentUsername();
2037
+ await this.init();
2038
+ return ret;
2074
2039
  }
2075
- async getCourseTagStubs() {
2076
- return getCourseTagStubs(this.id);
2040
+ update(id, update) {
2041
+ return this.updateQueue.update(id, update);
2077
2042
  }
2078
- async addNote(codeCourse, shape, data, author, tags, uploads, elo = (0, import_common6.blankCourseElo)()) {
2043
+ async getCourseRegistrationsDoc() {
2044
+ logger.debug(`Fetching courseRegistrations for ${this.getUsername()}`);
2045
+ let ret;
2079
2046
  try {
2080
- const resp = await addNote55(this.id, codeCourse, shape, data, author, tags, uploads, elo);
2081
- if (resp.ok) {
2082
- if (resp.cardCreationFailed) {
2083
- logger.warn(
2084
- `[courseDB.addNote] Note added but card creation failed: ${resp.cardCreationError}`
2085
- );
2086
- return {
2087
- status: import_common6.Status.error,
2088
- message: `Note was added but no cards were created: ${resp.cardCreationError}`,
2089
- id: resp.id
2090
- };
2091
- }
2092
- return {
2093
- status: import_common6.Status.ok,
2094
- message: "",
2095
- id: resp.id
2096
- };
2097
- } else {
2098
- return {
2099
- status: import_common6.Status.error,
2100
- message: "Unexpected error adding note"
2101
- };
2102
- }
2047
+ const regDoc = await this.localDB.get(
2048
+ _BaseUser.DOC_IDS.COURSE_REGISTRATIONS
2049
+ );
2050
+ return regDoc;
2103
2051
  } catch (e) {
2104
2052
  const err = e;
2105
- logger.error(
2106
- `[addNote] error ${err.name}
2107
- reason: ${err.reason}
2108
- message: ${err.message}`
2109
- );
2110
- return {
2111
- status: import_common6.Status.error,
2112
- message: `Error adding note to course. ${e.reason || err.message}`
2113
- };
2053
+ if (err.status === 404) {
2054
+ await this.localDB.put({
2055
+ _id: _BaseUser.DOC_IDS.COURSE_REGISTRATIONS,
2056
+ courses: [],
2057
+ studyWeight: {}
2058
+ });
2059
+ ret = await this.getCourseRegistrationsDoc();
2060
+ } else {
2061
+ throw new Error(
2062
+ `Unexpected error ${JSON.stringify(e)} in getOrCreateCourseRegistrationDoc...`
2063
+ );
2064
+ }
2114
2065
  }
2066
+ return ret;
2115
2067
  }
2116
- async getCourseDoc(id, options) {
2117
- return await getCourseDoc(this.id, id, options);
2068
+ async getActiveCourses() {
2069
+ const reg = await this.getCourseRegistrationsDoc();
2070
+ return reg.courses.filter((c) => {
2071
+ return c.status === void 0 || c.status === "active";
2072
+ });
2118
2073
  }
2119
- async getCourseDocs(ids, options = {}) {
2120
- return await getCourseDocs(this.id, ids, options);
2074
+ /**
2075
+ * Returns a promise of the card IDs that the user has
2076
+ * a scheduled review for.
2077
+ *
2078
+ */
2079
+ async getActiveCards() {
2080
+ const keys = getStartAndEndKeys2(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */]);
2081
+ const reviews = await this.remoteDB.allDocs({
2082
+ startkey: keys.startkey,
2083
+ endkey: keys.endkey,
2084
+ include_docs: true
2085
+ });
2086
+ return reviews.rows.map((r) => `${r.doc.courseId}-${r.doc.cardId}`);
2121
2087
  }
2122
- ////////////////////////////////////
2123
- // NavigationStrategyManager implementation
2124
- ////////////////////////////////////
2125
- getNavigationStrategy(id) {
2126
- logger.debug(`[courseDB] Getting navigation strategy: ${id}`);
2127
- const strategy = {
2128
- id: "ELO",
2129
- docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
2130
- name: "ELO",
2131
- description: "ELO-based navigation strategy for ordering content by difficulty",
2132
- implementingClass: "elo" /* ELO */,
2133
- course: this.id,
2134
- serializedData: ""
2135
- // serde is a noop for ELO navigator.
2136
- };
2137
- return Promise.resolve(strategy);
2088
+ async getActivityRecords() {
2089
+ try {
2090
+ const hist = await this.getHistory();
2091
+ const allRecords = [];
2092
+ if (!Array.isArray(hist)) {
2093
+ logger.error("getHistory did not return an array:", hist);
2094
+ return allRecords;
2095
+ }
2096
+ let sampleCount = 0;
2097
+ for (let i = 0; i < hist.length; i++) {
2098
+ try {
2099
+ if (hist[i] && Array.isArray(hist[i].records)) {
2100
+ hist[i].records.forEach((record) => {
2101
+ try {
2102
+ if (!record.timeStamp) {
2103
+ return;
2104
+ }
2105
+ let timeStamp;
2106
+ if (typeof record.timeStamp === "object") {
2107
+ if (typeof record.timeStamp.toDate === "function") {
2108
+ timeStamp = record.timeStamp.toISOString();
2109
+ } else if (record.timeStamp instanceof Date) {
2110
+ timeStamp = record.timeStamp.toISOString();
2111
+ } else {
2112
+ if (sampleCount < 3) {
2113
+ logger.warn("Unknown timestamp object type:", record.timeStamp);
2114
+ sampleCount++;
2115
+ }
2116
+ return;
2117
+ }
2118
+ } else if (typeof record.timeStamp === "string") {
2119
+ const date = new Date(record.timeStamp);
2120
+ if (isNaN(date.getTime())) {
2121
+ return;
2122
+ }
2123
+ timeStamp = record.timeStamp;
2124
+ } else if (typeof record.timeStamp === "number") {
2125
+ timeStamp = new Date(record.timeStamp).toISOString();
2126
+ } else {
2127
+ return;
2128
+ }
2129
+ allRecords.push({
2130
+ timeStamp,
2131
+ courseID: record.courseID || "unknown",
2132
+ cardID: record.cardID || "unknown",
2133
+ timeSpent: record.timeSpent || 0,
2134
+ type: "card_view"
2135
+ });
2136
+ } catch (err) {
2137
+ }
2138
+ });
2139
+ }
2140
+ } catch (err) {
2141
+ logger.error("Error processing history item:", err);
2142
+ }
2143
+ }
2144
+ logger.debug(`Found ${allRecords.length} activity records`);
2145
+ return allRecords;
2146
+ } catch (err) {
2147
+ logger.error("Error in getActivityRecords:", err);
2148
+ return [];
2149
+ }
2138
2150
  }
2139
- getAllNavigationStrategies() {
2140
- logger.debug("[courseDB] Returning hard-coded navigation strategies");
2141
- const strategies = [
2142
- {
2143
- id: "ELO",
2144
- docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
2145
- name: "ELO",
2146
- description: "ELO-based navigation strategy for ordering content by difficulty",
2147
- implementingClass: "elo" /* ELO */,
2148
- course: this.id,
2149
- serializedData: ""
2150
- // serde is a noop for ELO navigator.
2151
+ async getReviewstoDate(targetDate, course_id) {
2152
+ const keys = getStartAndEndKeys2(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */]);
2153
+ const reviews = await this.remoteDB.allDocs({
2154
+ startkey: keys.startkey,
2155
+ endkey: keys.endkey,
2156
+ include_docs: true
2157
+ });
2158
+ log3(
2159
+ `Fetching ${this._username}'s scheduled reviews${course_id ? ` for course ${course_id}` : ""}.`
2160
+ );
2161
+ return reviews.rows.filter((r) => {
2162
+ if (r.id.startsWith(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */])) {
2163
+ const date = import_moment4.default.utc(
2164
+ r.id.substr(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */].length),
2165
+ REVIEW_TIME_FORMAT2
2166
+ );
2167
+ if (targetDate.isAfter(date)) {
2168
+ if (course_id === void 0 || r.doc.courseId === course_id) {
2169
+ return true;
2170
+ }
2171
+ }
2151
2172
  }
2152
- ];
2153
- return Promise.resolve(strategies);
2173
+ }).map((r) => r.doc);
2154
2174
  }
2155
- addNavigationStrategy(data) {
2156
- logger.debug(`[courseDB] Adding navigation strategy: ${data.id}`);
2157
- logger.debug(JSON.stringify(data));
2158
- return Promise.resolve();
2175
+ async getReviewsForcast(daysCount) {
2176
+ const time = import_moment4.default.utc().add(daysCount, "days");
2177
+ return this.getReviewstoDate(time);
2159
2178
  }
2160
- updateNavigationStrategy(id, data) {
2161
- logger.debug(`[courseDB] Updating navigation strategy: ${id}`);
2162
- logger.debug(JSON.stringify(data));
2163
- return Promise.resolve();
2179
+ async getPendingReviews(course_id) {
2180
+ const now = import_moment4.default.utc();
2181
+ return this.getReviewstoDate(now, course_id);
2164
2182
  }
2165
- async surfaceNavigationStrategy() {
2166
- logger.warn(`Returning hard-coded default ELO navigator`);
2167
- const ret = {
2168
- id: "ELO",
2169
- docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
2170
- name: "ELO",
2171
- description: "ELO-based navigation strategy",
2172
- implementingClass: "elo" /* ELO */,
2173
- course: this.id,
2174
- serializedData: ""
2175
- // serde is a noop for ELO navigator.
2176
- };
2177
- return Promise.resolve(ret);
2183
+ async getScheduledReviewCount(course_id) {
2184
+ return (await this.getPendingReviews(course_id)).length;
2178
2185
  }
2179
- ////////////////////////////////////
2180
- // END NavigationStrategyManager implementation
2181
- ////////////////////////////////////
2182
- ////////////////////////////////////
2183
- // StudyContentSource implementation
2184
- ////////////////////////////////////
2185
- async getNewCards(limit = 99) {
2186
- const u = await this._getCurrentUser();
2187
- try {
2188
- const strategy = await this.surfaceNavigationStrategy();
2189
- const navigator = await ContentNavigator.create(u, this, strategy);
2190
- return navigator.getNewCards(limit);
2191
- } catch (e) {
2192
- logger.error(`[courseDB] Error surfacing a NavigationStrategy: ${e}`);
2193
- throw e;
2194
- }
2186
+ async getRegisteredCourses() {
2187
+ const regDoc = await this.getCourseRegistrationsDoc();
2188
+ return regDoc.courses.filter((c) => {
2189
+ return !c.status || c.status === "active" || c.status === "maintenance-mode";
2190
+ });
2195
2191
  }
2196
- async getPendingReviews() {
2197
- const u = await this._getCurrentUser();
2198
- try {
2199
- const strategy = await this.surfaceNavigationStrategy();
2200
- const navigator = await ContentNavigator.create(u, this, strategy);
2201
- return navigator.getPendingReviews();
2202
- } catch (e) {
2203
- logger.error(`[courseDB] Error surfacing a NavigationStrategy: ${e}`);
2204
- throw e;
2192
+ async getCourseRegDoc(courseID) {
2193
+ const regDocs = await this.getCourseRegistrationsDoc();
2194
+ const ret = regDocs.courses.find((c) => c.courseID === courseID);
2195
+ if (ret) {
2196
+ return ret;
2197
+ } else {
2198
+ throw new Error(`Course registration not found for course ID: ${courseID}`);
2205
2199
  }
2206
2200
  }
2207
- async getCardsCenteredAtELO(options = {
2208
- limit: 99,
2209
- elo: "user"
2210
- }, filter) {
2211
- let targetElo;
2212
- if (options.elo === "user") {
2213
- const u = await this._getCurrentUser();
2214
- targetElo = -1;
2215
- try {
2216
- const courseDoc = (await u.getCourseRegistrationsDoc()).courses.find((c) => {
2217
- return c.courseID === this.id;
2201
+ async registerForCourse(course_id, previewMode = false) {
2202
+ return this.getCourseRegistrationsDoc().then((doc) => {
2203
+ const status = previewMode ? "preview" : "active";
2204
+ logger.debug(`Registering for ${course_id} with status: ${status}`);
2205
+ const regItem = {
2206
+ status,
2207
+ courseID: course_id,
2208
+ user: true,
2209
+ admin: false,
2210
+ moderator: false,
2211
+ elo: {
2212
+ global: {
2213
+ score: 1e3,
2214
+ count: 0
2215
+ },
2216
+ tags: {},
2217
+ misc: {}
2218
+ }
2219
+ };
2220
+ if (doc.courses.filter((course) => {
2221
+ return course.courseID === regItem.courseID;
2222
+ }).length === 0) {
2223
+ log3(`It's a new course registration!`);
2224
+ doc.courses.push(regItem);
2225
+ doc.studyWeight[course_id] = 1;
2226
+ } else {
2227
+ doc.courses.forEach((c) => {
2228
+ log3(`Found the previously registered course!`);
2229
+ if (c.courseID === course_id) {
2230
+ c.status = status;
2231
+ }
2218
2232
  });
2219
- targetElo = (0, import_common6.EloToNumber)(courseDoc.elo);
2220
- } catch {
2221
- targetElo = 1e3;
2222
2233
  }
2223
- } else if (options.elo === "random") {
2224
- const bounds = await GET_CACHED(`elo-bounds-${this.id}`, () => this.getELOBounds());
2225
- targetElo = Math.round(bounds.low + Math.random() * (bounds.high - bounds.low));
2226
- } else {
2227
- targetElo = options.elo;
2228
- }
2229
- let cards = [];
2230
- let mult = 4;
2231
- let previousCount = -1;
2232
- let newCount = 0;
2233
- while (cards.length < options.limit && newCount !== previousCount) {
2234
- cards = await this.getCardsByELO(targetElo, mult * options.limit);
2235
- previousCount = newCount;
2236
- newCount = cards.length;
2237
- logger.debug(`Found ${cards.length} elo neighbor cards...`);
2238
- if (filter) {
2239
- cards = cards.filter(filter);
2240
- logger.debug(`Filtered to ${cards.length} cards...`);
2234
+ return this.localDB.put(doc);
2235
+ }).catch((e) => {
2236
+ log3(`Registration failed because of: ${JSON.stringify(e)}`);
2237
+ throw e;
2238
+ });
2239
+ }
2240
+ async dropCourse(course_id, dropStatus = "dropped") {
2241
+ return this.getCourseRegistrationsDoc().then((doc) => {
2242
+ let index = -1;
2243
+ for (let i = 0; i < doc.courses.length; i++) {
2244
+ if (doc.courses[i].courseID === course_id) {
2245
+ index = i;
2246
+ }
2241
2247
  }
2242
- mult *= 2;
2243
- }
2244
- const selectedCards = [];
2245
- while (selectedCards.length < options.limit && cards.length > 0) {
2246
- const index = randIntWeightedTowardZero(cards.length);
2247
- const card = cards.splice(index, 1)[0];
2248
- selectedCards.push(card);
2249
- }
2250
- return selectedCards.map((c) => {
2251
- const split = c.split("-");
2252
- return {
2253
- courseID: this.id,
2254
- cardID: split[1],
2255
- qualifiedID: `${split[0]}-${split[1]}`,
2256
- contentSourceType: "course",
2257
- contentSourceID: this.id,
2258
- status: "new"
2259
- };
2248
+ if (index !== -1) {
2249
+ delete doc.studyWeight[course_id];
2250
+ doc.courses[index].status = dropStatus;
2251
+ } else {
2252
+ throw new Error(
2253
+ `User ${this.getUsername()} is not currently registered for course ${course_id}`
2254
+ );
2255
+ }
2256
+ return this.localDB.put(doc);
2260
2257
  });
2261
2258
  }
2262
- };
2263
- }
2264
- });
2265
-
2266
- // src/impl/couch/classroomDB.ts
2267
- function getClassroomDB(classID, version) {
2268
- const dbName = `classdb-${version}-${classID}`;
2269
- logger.info(`Retrieving classroom db: ${dbName}`);
2270
- return new pouchdb_setup_default(
2271
- ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + dbName,
2272
- pouchDBincludeCredentialsConfig
2273
- );
2274
- }
2275
- async function getClassroomConfig(classID) {
2276
- return await getClassroomDB(classID, "student").get(CLASSROOM_CONFIG);
2277
- }
2278
- var import_moment4, classroomLookupDBTitle, CLASSROOM_CONFIG, ClassroomDBBase, StudentClassroomDB, TeacherClassroomDB, ClassroomLookupDB;
2279
- var init_classroomDB = __esm({
2280
- "src/impl/couch/classroomDB.ts"() {
2281
- "use strict";
2282
- init_factory();
2283
- init_logger();
2284
- import_moment4 = __toESM(require("moment"));
2285
- init_pouchdb_setup();
2286
- init_couch();
2287
- init_courseDB();
2288
- classroomLookupDBTitle = "classdb-lookup";
2289
- CLASSROOM_CONFIG = "ClassroomConfig";
2290
- ClassroomDBBase = class {
2291
- _id;
2292
- _db;
2293
- _cfg;
2294
- _initComplete = false;
2295
- _content_prefix = "content";
2296
- get _content_searchkeys() {
2297
- return getStartAndEndKeys2(this._content_prefix);
2259
+ async getCourseInterface(courseId) {
2260
+ return new UsrCrsData(this, courseId);
2298
2261
  }
2299
- async getAssignedContent() {
2300
- logger.info(`Getting assigned content...`);
2301
- const docRows = await this._db.allDocs({
2302
- startkey: this._content_prefix,
2303
- endkey: this._content_prefix + `\uFFF0`,
2304
- include_docs: true
2305
- });
2306
- const ret = docRows.rows.map((row) => {
2307
- return row.doc;
2308
- });
2309
- return ret;
2262
+ async getUserEditableCourses() {
2263
+ let courseIDs = [];
2264
+ const registeredCourses = await this.getCourseRegistrationsDoc();
2265
+ courseIDs = courseIDs.concat(
2266
+ registeredCourses.courses.map((course) => {
2267
+ return course.courseID;
2268
+ })
2269
+ );
2270
+ const cfgs = await Promise.all(
2271
+ courseIDs.map(async (id) => {
2272
+ return await getCredentialledCourseConfig(id);
2273
+ })
2274
+ );
2275
+ return cfgs;
2310
2276
  }
2311
- getContentId(content) {
2312
- if (content.type === "tag") {
2313
- return `${this._content_prefix}-${content.courseID}-${content.tagID}`;
2277
+ async getConfig() {
2278
+ const defaultConfig = {
2279
+ _id: _BaseUser.DOC_IDS.CONFIG,
2280
+ darkMode: false,
2281
+ likesConfetti: false,
2282
+ sessionTimeLimit: 5
2283
+ };
2284
+ try {
2285
+ const cfg = await this.localDB.get(_BaseUser.DOC_IDS.CONFIG);
2286
+ logger.debug("Raw config from DB:", cfg);
2287
+ return cfg;
2288
+ } catch (e) {
2289
+ const err = e;
2290
+ if (err.name && err.name === "not_found") {
2291
+ await this.localDB.put(defaultConfig);
2292
+ return this.getConfig();
2293
+ } else {
2294
+ logger.error(`Error setting user default config:`, e);
2295
+ throw new Error(`Error returning the user's configuration: ${JSON.stringify(e)}`);
2296
+ }
2297
+ }
2298
+ }
2299
+ async setConfig(items) {
2300
+ logger.debug(`Setting Config items ${JSON.stringify(items)}`);
2301
+ const c = await this.getConfig();
2302
+ const put = await this.localDB.put({
2303
+ ...c,
2304
+ ...items
2305
+ });
2306
+ if (put.ok) {
2307
+ logger.debug(`Config items set: ${JSON.stringify(items)}`);
2314
2308
  } else {
2315
- return `${this._content_prefix}-${content.courseID}`;
2309
+ logger.error(`Error setting config items: ${JSON.stringify(put)}`);
2316
2310
  }
2317
2311
  }
2318
- get ready() {
2319
- return this._initComplete;
2312
+ /**
2313
+ *
2314
+ * This function should be called *only* by the pouchdb datalayer provider
2315
+ * auth store.
2316
+ *
2317
+ *
2318
+ * Anyone else seeking the current user should use the auth store's
2319
+ * exported `getCurrentUser` method.
2320
+ *
2321
+ */
2322
+ static async instance(syncStrategy, username) {
2323
+ if (username) {
2324
+ _BaseUser._instance = new _BaseUser(username, syncStrategy);
2325
+ await _BaseUser._instance.init();
2326
+ return _BaseUser._instance;
2327
+ } else if (_BaseUser._instance && _BaseUser._initialized) {
2328
+ return _BaseUser._instance;
2329
+ } else if (_BaseUser._instance) {
2330
+ return new Promise((resolve) => {
2331
+ (function waitForUser() {
2332
+ if (_BaseUser._initialized) {
2333
+ return resolve(_BaseUser._instance);
2334
+ } else {
2335
+ setTimeout(waitForUser, 50);
2336
+ }
2337
+ })();
2338
+ });
2339
+ } else {
2340
+ const guestUsername = await syncStrategy.getCurrentUsername();
2341
+ _BaseUser._instance = new _BaseUser(guestUsername, syncStrategy);
2342
+ await _BaseUser._instance.init();
2343
+ return _BaseUser._instance;
2344
+ }
2320
2345
  }
2321
- getConfig() {
2322
- return this._cfg;
2346
+ constructor(username, syncStrategy) {
2347
+ _BaseUser._initialized = false;
2348
+ this._username = username;
2349
+ this.syncStrategy = syncStrategy;
2350
+ this.setDBandQ();
2323
2351
  }
2324
- };
2325
- StudentClassroomDB = class _StudentClassroomDB extends ClassroomDBBase {
2326
- // private readonly _prefix: string = 'content';
2327
- userMessages;
2328
- _user;
2329
- constructor(classID, user) {
2330
- super();
2331
- this._id = classID;
2332
- this._user = user;
2352
+ setDBandQ() {
2353
+ this.localDB = getLocalUserDB(this._username);
2354
+ this.remoteDB = this.syncStrategy.setupRemoteDB(this._username);
2355
+ this.writeDB = this.syncStrategy.getWriteDB ? this.syncStrategy.getWriteDB(this._username) : this.localDB;
2356
+ this.updateQueue = new UpdateQueue(this.localDB, this.writeDB);
2333
2357
  }
2334
2358
  async init() {
2335
- const dbName = `classdb-student-${this._id}`;
2336
- this._db = new pouchdb_setup_default(
2337
- ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + dbName,
2338
- pouchDBincludeCredentialsConfig
2339
- );
2359
+ _BaseUser._initialized = false;
2360
+ if (this._username === "admin") {
2361
+ _BaseUser._initialized = true;
2362
+ return;
2363
+ }
2364
+ this.setDBandQ();
2365
+ this.syncStrategy.startSync(this.localDB, this.remoteDB);
2366
+ void this.applyDesignDocs();
2367
+ void this.deduplicateReviews();
2368
+ _BaseUser._initialized = true;
2369
+ }
2370
+ static designDocs = [
2371
+ {
2372
+ _id: "_design/reviewCards",
2373
+ views: {
2374
+ reviewCards: {
2375
+ map: `function (doc) {
2376
+ if (doc._id && doc._id.indexOf('card_review') === 0 && doc.courseId && doc.cardId) {
2377
+ emit(doc._id, doc.courseId + '-' + doc.cardId);
2378
+ }
2379
+ }`
2380
+ }
2381
+ }
2382
+ }
2383
+ ];
2384
+ async applyDesignDocs() {
2385
+ if (this._username === "admin") {
2386
+ return;
2387
+ }
2388
+ for (const doc of _BaseUser.designDocs) {
2389
+ try {
2390
+ try {
2391
+ const existingDoc = await this.remoteDB.get(doc._id);
2392
+ await this.remoteDB.put({
2393
+ ...doc,
2394
+ _rev: existingDoc._rev
2395
+ });
2396
+ } catch (e) {
2397
+ if (e instanceof Error && e.name === "not_found") {
2398
+ await this.remoteDB.put(doc);
2399
+ } else {
2400
+ throw e;
2401
+ }
2402
+ }
2403
+ } catch (error) {
2404
+ if (error.name && error.name === "conflict") {
2405
+ logger.warn(`Design doc ${doc._id} update conflict - will retry`);
2406
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
2407
+ await this.applyDesignDoc(doc);
2408
+ } else {
2409
+ logger.error(`Failed to apply design doc ${doc._id}:`, error);
2410
+ throw error;
2411
+ }
2412
+ }
2413
+ }
2414
+ }
2415
+ // Helper method for single doc update with retry
2416
+ async applyDesignDoc(doc, retries = 3) {
2340
2417
  try {
2341
- const cfg = await this._db.get(CLASSROOM_CONFIG);
2342
- this._cfg = cfg;
2343
- this.userMessages = this._db.changes({
2344
- since: "now",
2345
- live: true,
2346
- include_docs: true
2418
+ const existingDoc = await this.remoteDB.get(doc._id);
2419
+ await this.remoteDB.put({
2420
+ ...doc,
2421
+ _rev: existingDoc._rev
2347
2422
  });
2348
- this._initComplete = true;
2349
- return;
2350
2423
  } catch (e) {
2351
- throw new Error(`Error in StudentClassroomDB constructor: ${JSON.stringify(e)}`);
2424
+ if (e instanceof Error && e.name === "conflict" && retries > 0) {
2425
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
2426
+ return this.applyDesignDoc(doc, retries - 1);
2427
+ }
2428
+ throw e;
2352
2429
  }
2353
2430
  }
2354
- static async factory(classID, user) {
2355
- const ret = new _StudentClassroomDB(classID, user);
2356
- await ret.init();
2357
- return ret;
2431
+ /**
2432
+ * Logs a record of the user's interaction with the card and returns the card's
2433
+ * up-to-date history
2434
+ *
2435
+ * // [ ] #db-refactor extract to a smaller scope - eg, UserStudySession
2436
+ *
2437
+ * @param record the recent recorded interaction between user and card
2438
+ * @returns The updated state of the card's CardHistory data
2439
+ */
2440
+ async putCardRecord(record) {
2441
+ const cardHistoryID = getCardHistoryID(record.courseID, record.cardID);
2442
+ record.timeStamp = import_moment4.default.utc(record.timeStamp).toString();
2443
+ try {
2444
+ const cardHistory = await this.update(
2445
+ cardHistoryID,
2446
+ function(h) {
2447
+ h.records.push(record);
2448
+ h.bestInterval = h.bestInterval || 0;
2449
+ h.lapses = h.lapses || 0;
2450
+ h.streak = h.streak || 0;
2451
+ return h;
2452
+ }
2453
+ );
2454
+ cardHistory.records = cardHistory.records.map((record2) => {
2455
+ const ret = {
2456
+ ...record2
2457
+ };
2458
+ ret.timeStamp = import_moment4.default.utc(record2.timeStamp);
2459
+ return ret;
2460
+ });
2461
+ return cardHistory;
2462
+ } catch (e) {
2463
+ const reason = e;
2464
+ if (reason.status === 404) {
2465
+ const initCardHistory = {
2466
+ _id: cardHistoryID,
2467
+ cardID: record.cardID,
2468
+ courseID: record.courseID,
2469
+ records: [record],
2470
+ lapses: 0,
2471
+ streak: 0,
2472
+ bestInterval: 0
2473
+ };
2474
+ const putResult = await this.writeDB.put(initCardHistory);
2475
+ return { ...initCardHistory, _rev: putResult.rev };
2476
+ } else {
2477
+ throw new Error(`putCardRecord failed because of:
2478
+ name:${reason.name}
2479
+ error: ${reason.error}
2480
+ message: ${reason.message}`);
2481
+ }
2482
+ }
2358
2483
  }
2359
- setChangeFcn(f) {
2360
- void this.userMessages.on("change", f);
2484
+ async deduplicateReviews() {
2485
+ try {
2486
+ log3("Starting deduplication of scheduled reviews...");
2487
+ const reviewsMap = {};
2488
+ const duplicateDocIds = [];
2489
+ const scheduledReviews = await this.remoteDB.query("reviewCards/reviewCards");
2490
+ log3(`Found ${scheduledReviews.rows.length} scheduled reviews to process`);
2491
+ scheduledReviews.rows.forEach((r) => {
2492
+ const qualifiedCardId = r.value;
2493
+ const docId = r.key;
2494
+ if (reviewsMap[qualifiedCardId]) {
2495
+ log3(`Found duplicate scheduled review for card: ${qualifiedCardId}`);
2496
+ log3(
2497
+ `Marking earlier review ${reviewsMap[qualifiedCardId]} for deletion, keeping ${docId}`
2498
+ );
2499
+ duplicateDocIds.push(reviewsMap[qualifiedCardId]);
2500
+ reviewsMap[qualifiedCardId] = docId;
2501
+ } else {
2502
+ reviewsMap[qualifiedCardId] = docId;
2503
+ }
2504
+ });
2505
+ if (duplicateDocIds.length > 0) {
2506
+ log3(`Removing ${duplicateDocIds.length} duplicate reviews...`);
2507
+ const deletePromises = duplicateDocIds.map(async (docId) => {
2508
+ try {
2509
+ const doc = await this.remoteDB.get(docId);
2510
+ await this.writeDB.remove(doc);
2511
+ log3(`Successfully removed duplicate review: ${docId}`);
2512
+ } catch (error) {
2513
+ log3(`Failed to remove duplicate review ${docId}: ${error}`);
2514
+ }
2515
+ });
2516
+ await Promise.all(deletePromises);
2517
+ log3(`Deduplication complete. Processed ${duplicateDocIds.length} duplicates`);
2518
+ } else {
2519
+ log3("No duplicate reviews found");
2520
+ }
2521
+ } catch (error) {
2522
+ log3(`Error during review deduplication: ${error}`);
2523
+ }
2361
2524
  }
2362
- async getPendingReviews() {
2363
- const u = this._user;
2364
- return (await u.getPendingReviews()).filter((r) => r.scheduledFor === "classroom" && r.schedulingAgentId === this._id).map((r) => {
2365
- return {
2366
- ...r,
2367
- qualifiedID: `${r.courseId}-${r.cardId}`,
2368
- courseID: r.courseId,
2369
- cardID: r.cardId,
2370
- contentSourceType: "classroom",
2371
- contentSourceID: this._id,
2372
- reviewID: r._id,
2373
- status: "review"
2374
- };
2525
+ /**
2526
+ * Returns a promise of the card IDs that the user has
2527
+ * encountered in the past.
2528
+ *
2529
+ * @param course_id optional specification of individual course
2530
+ */
2531
+ async getSeenCards(course_id) {
2532
+ let prefix = DocTypePrefixes["CARDRECORD" /* CARDRECORD */];
2533
+ if (course_id) {
2534
+ prefix += course_id;
2535
+ }
2536
+ const docs = await filterAllDocsByPrefix2(this.localDB, prefix, {
2537
+ include_docs: false
2538
+ });
2539
+ const ret = [];
2540
+ docs.rows.forEach((row) => {
2541
+ if (row.id.startsWith(DocTypePrefixes["CARDRECORD" /* CARDRECORD */])) {
2542
+ ret.push(row.id.substr(DocTypePrefixes["CARDRECORD" /* CARDRECORD */].length));
2543
+ }
2375
2544
  });
2545
+ return ret;
2376
2546
  }
2377
- async getNewCards() {
2378
- const activeCards = await this._user.getActiveCards();
2379
- const now = import_moment4.default.utc();
2380
- const assigned = await this.getAssignedContent();
2381
- const due = assigned.filter((c) => now.isAfter(import_moment4.default.utc(c.activeOn, REVIEW_TIME_FORMAT2)));
2382
- logger.info(`Due content: ${JSON.stringify(due)}`);
2383
- let ret = [];
2384
- for (let i = 0; i < due.length; i++) {
2385
- const content = due[i];
2386
- if (content.type === "course") {
2387
- const db = new CourseDB(content.courseID, async () => this._user);
2388
- ret = ret.concat(await db.getNewCards());
2389
- } else if (content.type === "tag") {
2390
- const tagDoc = await getTag(content.courseID, content.tagID);
2391
- ret = ret.concat(
2392
- tagDoc.taggedCards.map((c) => {
2393
- return {
2394
- courseID: content.courseID,
2395
- cardID: c,
2396
- qualifiedID: `${content.courseID}-${c}`,
2397
- contentSourceType: "classroom",
2398
- contentSourceID: this._id,
2399
- status: "new"
2400
- };
2401
- })
2402
- );
2403
- } else if (content.type === "card") {
2404
- ret.push(await getCourseDB2(content.courseID).get(content.cardID));
2547
+ /**
2548
+ *
2549
+ * @returns A promise of the cards that the user has seen in the past.
2550
+ */
2551
+ async getHistory() {
2552
+ const cards = await filterAllDocsByPrefix2(
2553
+ this.remoteDB,
2554
+ DocTypePrefixes["CARDRECORD" /* CARDRECORD */],
2555
+ {
2556
+ include_docs: true,
2557
+ attachments: false
2405
2558
  }
2406
- }
2407
- logger.info(`New Cards from classroom ${this._cfg.name}: ${ret.map((c) => c.qualifiedID)}`);
2408
- return ret.filter((c) => {
2409
- if (activeCards.some((ac) => c.qualifiedID.includes(ac))) {
2410
- return false;
2411
- } else {
2412
- return true;
2559
+ );
2560
+ return cards.rows.map((r) => r.doc);
2561
+ }
2562
+ async updateCourseSettings(course_id, settings) {
2563
+ void this.getCourseRegistrationsDoc().then((doc) => {
2564
+ const crs = doc.courses.find((c) => c.courseID === course_id);
2565
+ if (crs) {
2566
+ if (crs.settings === null || crs.settings === void 0) {
2567
+ crs.settings = {};
2568
+ }
2569
+ settings.forEach((setting) => {
2570
+ crs.settings[setting.key] = setting.value;
2571
+ });
2413
2572
  }
2573
+ return this.localDB.put(doc);
2414
2574
  });
2415
2575
  }
2416
- };
2417
- TeacherClassroomDB = class _TeacherClassroomDB extends ClassroomDBBase {
2418
- _stuDb;
2419
- constructor(classID) {
2420
- super();
2421
- this._id = classID;
2576
+ async getCourseSettings(course_id) {
2577
+ const regDoc = await this.getCourseRegistrationsDoc();
2578
+ const crsDoc = regDoc.courses.find((c) => c.courseID === course_id);
2579
+ if (crsDoc) {
2580
+ return crsDoc.settings;
2581
+ } else {
2582
+ throw new Error(`getCourseSettings Failed:
2583
+ User is not registered for course ${course_id}`);
2584
+ }
2422
2585
  }
2423
- async init() {
2424
- const dbName = `classdb-teacher-${this._id}`;
2425
- const stuDbName = `classdb-student-${this._id}`;
2426
- this._db = new pouchdb_setup_default(
2427
- ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + dbName,
2428
- pouchDBincludeCredentialsConfig
2429
- );
2430
- this._stuDb = new pouchdb_setup_default(
2431
- ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + stuDbName,
2432
- pouchDBincludeCredentialsConfig
2433
- );
2586
+ async getOrCreateClassroomRegistrationsDoc() {
2587
+ let ret;
2434
2588
  try {
2435
- return this._db.get(CLASSROOM_CONFIG).then((cfg) => {
2436
- this._cfg = cfg;
2437
- this._initComplete = true;
2438
- }).then(() => {
2439
- return;
2440
- });
2589
+ ret = await this.remoteDB.get(
2590
+ _BaseUser.DOC_IDS.CLASSROOM_REGISTRATIONS
2591
+ );
2441
2592
  } catch (e) {
2442
- throw new Error(`Error in TeacherClassroomDB constructor: ${JSON.stringify(e)}`);
2593
+ const err = e;
2594
+ if (err.status === 404) {
2595
+ await this.writeDB.put({
2596
+ _id: _BaseUser.DOC_IDS.CLASSROOM_REGISTRATIONS,
2597
+ registrations: []
2598
+ });
2599
+ ret = await this.getOrCreateClassroomRegistrationsDoc();
2600
+ } else {
2601
+ const errorDetails = {
2602
+ name: err.name,
2603
+ status: err.status,
2604
+ message: err.message,
2605
+ reason: err.reason,
2606
+ error: err.error
2607
+ };
2608
+ logger.error(
2609
+ "Database error in getOrCreateClassroomRegistrationsDoc (private method):",
2610
+ errorDetails
2611
+ );
2612
+ throw new Error(
2613
+ `Database error accessing classroom registrations: ${err.message || err.name || "Unknown error"} (status: ${err.status})`
2614
+ );
2615
+ }
2443
2616
  }
2444
- }
2445
- static async factory(classID) {
2446
- const ret = new _TeacherClassroomDB(classID);
2447
- await ret.init();
2617
+ logger.debug(`Returning classroom registrations doc: ${JSON.stringify(ret)}`);
2448
2618
  return ret;
2449
2619
  }
2450
- async removeContent(content) {
2451
- const contentID = this.getContentId(content);
2620
+ /**
2621
+ * Retrieves the list of active classroom IDs where the user is registered as a student.
2622
+ *
2623
+ * @returns Promise<string[]> - Array of classroom IDs, or empty array if classroom
2624
+ * registration document is unavailable due to database errors
2625
+ *
2626
+ * @description This method gracefully handles database connectivity issues by returning
2627
+ * an empty array when the classroom registrations document cannot be accessed.
2628
+ * This ensures that users can still access other application features even
2629
+ * when classroom functionality is temporarily unavailable.
2630
+ */
2631
+ async getActiveClasses() {
2452
2632
  try {
2453
- const doc = await this._db.get(contentID);
2454
- await this._db.remove(doc);
2455
- void this._db.replicate.to(this._stuDb, {
2456
- doc_ids: [contentID]
2457
- });
2633
+ return (await this.getOrCreateClassroomRegistrationsDoc()).registrations.filter((c) => c.registeredAs === "student").map((c) => c.classID);
2458
2634
  } catch (error) {
2459
- logger.error("Failed to remove content:", contentID, error);
2635
+ logger.warn(
2636
+ "Failed to load classroom registrations, continuing without classroom data:",
2637
+ error
2638
+ );
2639
+ return [];
2460
2640
  }
2461
2641
  }
2462
- async assignContent(content) {
2463
- let put;
2464
- const id = this.getContentId(content);
2465
- if (content.type === "tag") {
2466
- put = await this._db.put({
2467
- courseID: content.courseID,
2468
- tagID: content.tagID,
2469
- type: "tag",
2470
- _id: id,
2471
- assignedBy: content.assignedBy,
2472
- assignedOn: import_moment4.default.utc(),
2473
- activeOn: content.activeOn || import_moment4.default.utc()
2474
- });
2475
- } else {
2476
- put = await this._db.put({
2477
- courseID: content.courseID,
2478
- type: "course",
2479
- _id: id,
2480
- assignedBy: content.assignedBy,
2481
- assignedOn: import_moment4.default.utc(),
2482
- activeOn: content.activeOn || import_moment4.default.utc()
2483
- });
2484
- }
2485
- if (put.ok) {
2486
- void this._db.replicate.to(this._stuDb, {
2487
- doc_ids: [id]
2488
- });
2489
- return true;
2490
- } else {
2491
- return false;
2492
- }
2642
+ async scheduleCardReview(review) {
2643
+ return scheduleCardReviewLocal(this.writeDB, review);
2644
+ }
2645
+ async removeScheduledCardReview(reviewId) {
2646
+ return removeScheduledCardReviewLocal(this.writeDB, reviewId);
2647
+ }
2648
+ async registerForClassroom(_classId, _registerAs) {
2649
+ return registerUserForClassroom(this._username, _classId, _registerAs);
2650
+ }
2651
+ async dropFromClassroom(classId) {
2652
+ return dropUserFromClassroom(this._username, classId);
2653
+ }
2654
+ async getUserClassrooms() {
2655
+ return getUserClassrooms(this._username);
2656
+ }
2657
+ async updateUserElo(courseId, elo) {
2658
+ return updateUserElo(this._username, courseId, elo);
2493
2659
  }
2494
2660
  };
2495
- ClassroomLookupDB = () => new pouchdb_setup_default(ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + classroomLookupDBTitle, {
2496
- skip_setup: true
2497
- });
2661
+ userCoursesDoc = "CourseRegistrations";
2662
+ userClassroomsDoc = "ClassroomRegistrations";
2498
2663
  }
2499
2664
  });
2500
2665
 
2501
- // src/core/interfaces/contentSource.ts
2502
- function isReview(item) {
2503
- const ret = item.status === "review" || item.status === "failed-review" || "reviewID" in item;
2504
- return ret;
2505
- }
2506
- async function getStudySource(source, user) {
2507
- if (source.type === "classroom") {
2508
- return await StudentClassroomDB.factory(source.id, user);
2509
- } else {
2510
- return getDataLayer().getCourseDB(source.id);
2666
+ // src/impl/common/index.ts
2667
+ var init_common = __esm({
2668
+ "src/impl/common/index.ts"() {
2669
+ "use strict";
2670
+ init_SyncStrategy();
2671
+ init_BaseUserDB();
2672
+ init_userDBHelpers();
2511
2673
  }
2674
+ });
2675
+
2676
+ // src/factory.ts
2677
+ function getDataLayer() {
2678
+ if (!dataLayerInstance) {
2679
+ throw new Error("Data layer not initialized. Call initializeDataLayer first.");
2680
+ }
2681
+ return dataLayerInstance;
2512
2682
  }
2513
- var init_contentSource = __esm({
2514
- "src/core/interfaces/contentSource.ts"() {
2683
+ var ENV, dataLayerInstance;
2684
+ var init_factory = __esm({
2685
+ "src/factory.ts"() {
2515
2686
  "use strict";
2516
- init_factory();
2517
- init_classroomDB();
2687
+ init_common();
2688
+ init_logger();
2689
+ ENV = {
2690
+ COUCHDB_SERVER_PROTOCOL: "NOT_SET",
2691
+ COUCHDB_SERVER_URL: "NOT_SET"
2692
+ };
2693
+ dataLayerInstance = null;
2518
2694
  }
2519
2695
  });
2520
2696
 
2521
2697
  // src/impl/couch/adminDB.ts
2522
2698
  var AdminDB;
2523
- var init_adminDB = __esm({
2699
+ var init_adminDB2 = __esm({
2524
2700
  "src/impl/couch/adminDB.ts"() {
2525
2701
  "use strict";
2526
2702
  init_pouchdb_setup();
2527
2703
  init_factory();
2528
2704
  init_couch();
2529
- init_classroomDB();
2705
+ init_classroomDB2();
2530
2706
  init_courseLookupDB();
2531
2707
  init_logger();
2532
2708
  AdminDB = class {
@@ -2540,7 +2716,7 @@ var init_adminDB = __esm({
2540
2716
  async getUsers() {
2541
2717
  return (await this.usersDB.allDocs({
2542
2718
  include_docs: true,
2543
- ...getStartAndEndKeys2("org.couchdb.user:")
2719
+ ...getStartAndEndKeys("org.couchdb.user:")
2544
2720
  })).rows.map((r) => r.doc);
2545
2721
  }
2546
2722
  async getCourses() {
@@ -2633,14 +2809,14 @@ var init_auth = __esm({
2633
2809
  });
2634
2810
 
2635
2811
  // src/impl/couch/CouchDBSyncStrategy.ts
2636
- var import_common7, log4, CouchDBSyncStrategy;
2812
+ var import_common9, log4, CouchDBSyncStrategy;
2637
2813
  var init_CouchDBSyncStrategy = __esm({
2638
2814
  "src/impl/couch/CouchDBSyncStrategy.ts"() {
2639
2815
  "use strict";
2640
2816
  init_factory();
2641
2817
  init_types_legacy();
2642
2818
  init_logger();
2643
- import_common7 = require("@vue-skuilder/common");
2819
+ import_common9 = require("@vue-skuilder/common");
2644
2820
  init_common();
2645
2821
  init_pouchdb_setup();
2646
2822
  init_couch();
@@ -2658,6 +2834,13 @@ var init_CouchDBSyncStrategy = __esm({
2658
2834
  return this.getUserDB(username);
2659
2835
  }
2660
2836
  }
2837
+ getWriteDB(username) {
2838
+ if (username === GuestUsername || username.startsWith(GuestUsername)) {
2839
+ return getLocalUserDB(username);
2840
+ } else {
2841
+ return this.getUserDB(username);
2842
+ }
2843
+ }
2661
2844
  startSync(localDB, remoteDB) {
2662
2845
  if (localDB !== remoteDB) {
2663
2846
  this.syncHandle = pouchdb_setup_default.sync(localDB, remoteDB, {
@@ -2692,32 +2875,32 @@ var init_CouchDBSyncStrategy = __esm({
2692
2875
  log4(`CREATEACCOUNT: logged in as new user: ${loginResult.ok}`);
2693
2876
  if (loginResult.ok) {
2694
2877
  return {
2695
- status: import_common7.Status.ok,
2878
+ status: import_common9.Status.ok,
2696
2879
  error: void 0
2697
2880
  };
2698
2881
  } else {
2699
2882
  return {
2700
- status: import_common7.Status.error,
2883
+ status: import_common9.Status.error,
2701
2884
  error: "Failed to log in after account creation"
2702
2885
  };
2703
2886
  }
2704
2887
  } else {
2705
2888
  logger.warn(`Signup not OK: ${JSON.stringify(signupRequest)}`);
2706
2889
  return {
2707
- status: import_common7.Status.error,
2890
+ status: import_common9.Status.error,
2708
2891
  error: "Account creation failed"
2709
2892
  };
2710
2893
  }
2711
2894
  } catch (e) {
2712
2895
  if (e.reason === "Document update conflict.") {
2713
2896
  return {
2714
- status: import_common7.Status.error,
2897
+ status: import_common9.Status.error,
2715
2898
  error: "This username is taken!"
2716
2899
  };
2717
2900
  }
2718
2901
  logger.error(`Error on signup: ${JSON.stringify(e)}`);
2719
2902
  return {
2720
- status: import_common7.Status.error,
2903
+ status: import_common9.Status.error,
2721
2904
  error: e.message || "Unknown error during account creation"
2722
2905
  };
2723
2906
  }
@@ -2811,15 +2994,14 @@ __export(couch_exports, {
2811
2994
  CouchDBSyncStrategy: () => CouchDBSyncStrategy,
2812
2995
  CourseDB: () => CourseDB,
2813
2996
  CoursesDB: () => CoursesDB,
2814
- REVIEW_PREFIX: () => REVIEW_PREFIX2,
2815
- REVIEW_TIME_FORMAT: () => REVIEW_TIME_FORMAT2,
2997
+ REVIEW_TIME_FORMAT: () => REVIEW_TIME_FORMAT,
2816
2998
  StudentClassroomDB: () => StudentClassroomDB,
2817
2999
  TeacherClassroomDB: () => TeacherClassroomDB,
2818
3000
  addNote55: () => addNote55,
2819
3001
  addTagToCard: () => addTagToCard,
2820
3002
  createTag: () => createTag,
2821
3003
  deleteTag: () => deleteTag,
2822
- filterAllDocsByPrefix: () => filterAllDocsByPrefix2,
3004
+ filterAllDocsByPrefix: () => filterAllDocsByPrefix,
2823
3005
  getAncestorTagIDs: () => getAncestorTagIDs,
2824
3006
  getAppliedTags: () => getAppliedTags,
2825
3007
  getChildTagStubs: () => getChildTagStubs,
@@ -2836,7 +3018,7 @@ __export(couch_exports, {
2836
3018
  getCredentialledDataShapes: () => getCredentialledDataShapes,
2837
3019
  getLatestVersion: () => getLatestVersion,
2838
3020
  getRandomCards: () => getRandomCards,
2839
- getStartAndEndKeys: () => getStartAndEndKeys2,
3021
+ getStartAndEndKeys: () => getStartAndEndKeys,
2840
3022
  getStudySource: () => getStudySource,
2841
3023
  getTag: () => getTag,
2842
3024
  getTagID: () => getTagID,
@@ -2964,16 +3146,16 @@ function scheduleCardReview(review) {
2964
3146
  const now = import_moment5.default.utc();
2965
3147
  logger.info(`Scheduling for review in: ${review.time.diff(now, "h") / 24} days`);
2966
3148
  void getCouchUserDB(review.user).put({
2967
- _id: REVIEW_PREFIX2 + review.time.format(REVIEW_TIME_FORMAT2),
3149
+ _id: DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */] + review.time.format(REVIEW_TIME_FORMAT),
2968
3150
  cardId: review.card_id,
2969
- reviewTime: review.time,
3151
+ reviewTime: review.time.toISOString(),
2970
3152
  courseId: review.course_id,
2971
- scheduledAt: now,
3153
+ scheduledAt: now.toISOString(),
2972
3154
  scheduledFor: review.scheduledFor,
2973
3155
  schedulingAgentId: review.schedulingAgentId
2974
3156
  });
2975
3157
  }
2976
- function filterAllDocsByPrefix2(db, prefix, opts) {
3158
+ function filterAllDocsByPrefix(db, prefix, opts) {
2977
3159
  const options = {
2978
3160
  startkey: prefix,
2979
3161
  endkey: prefix + "\uFFF0",
@@ -2984,13 +3166,13 @@ function filterAllDocsByPrefix2(db, prefix, opts) {
2984
3166
  }
2985
3167
  return db.allDocs(options);
2986
3168
  }
2987
- function getStartAndEndKeys2(key) {
3169
+ function getStartAndEndKeys(key) {
2988
3170
  return {
2989
3171
  startkey: key,
2990
3172
  endkey: key + "\uFFF0"
2991
3173
  };
2992
3174
  }
2993
- var import_moment5, import_process, isBrowser, expiryDocID, GUEST_LOCAL_DB, localUserDB, pouchDBincludeCredentialsConfig, REVIEW_PREFIX2, REVIEW_TIME_FORMAT2;
3175
+ var import_moment5, import_process, isBrowser, expiryDocID, GUEST_LOCAL_DB, localUserDB, pouchDBincludeCredentialsConfig, REVIEW_TIME_FORMAT;
2994
3176
  var init_couch = __esm({
2995
3177
  "src/impl/couch/index.ts"() {
2996
3178
  init_factory();
@@ -3000,8 +3182,8 @@ var init_couch = __esm({
3000
3182
  init_pouchdb_setup();
3001
3183
  import_process = __toESM(require("process"));
3002
3184
  init_contentSource();
3003
- init_adminDB();
3004
- init_classroomDB();
3185
+ init_adminDB2();
3186
+ init_classroomDB2();
3005
3187
  init_courseAPI();
3006
3188
  init_courseDB();
3007
3189
  init_CouchDBSyncStrategy();
@@ -3018,8 +3200,7 @@ var init_couch = __esm({
3018
3200
  return pouchdb_setup_default.fetch(url, opts);
3019
3201
  }
3020
3202
  };
3021
- REVIEW_PREFIX2 = "card_review_";
3022
- REVIEW_TIME_FORMAT2 = "YYYY-MM-DD--kk:mm:ss-SSS";
3203
+ REVIEW_TIME_FORMAT = "YYYY-MM-DD--kk:mm:ss-SSS";
3023
3204
  }
3024
3205
  });
3025
3206
  init_couch();
@@ -3031,7 +3212,6 @@ init_couch();
3031
3212
  CouchDBSyncStrategy,
3032
3213
  CourseDB,
3033
3214
  CoursesDB,
3034
- REVIEW_PREFIX,
3035
3215
  REVIEW_TIME_FORMAT,
3036
3216
  StudentClassroomDB,
3037
3217
  TeacherClassroomDB,