@vue-skuilder/db 0.1.7 → 0.1.8-0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/{SyncStrategy-DnJRj-Xp.d.mts → SyncStrategy-CyATpyLQ.d.mts} +6 -0
  2. package/dist/{SyncStrategy-DnJRj-Xp.d.ts → SyncStrategy-CyATpyLQ.d.ts} +6 -0
  3. package/dist/core/index.d.mts +5 -5
  4. package/dist/core/index.d.ts +5 -5
  5. package/dist/core/index.js +131 -118
  6. package/dist/core/index.js.map +1 -1
  7. package/dist/core/index.mjs +128 -115
  8. package/dist/core/index.mjs.map +1 -1
  9. package/dist/{dataLayerProvider-BbW9EnZK.d.mts → dataLayerProvider-BInqI_RF.d.mts} +1 -1
  10. package/dist/{dataLayerProvider-6stCgDME.d.ts → dataLayerProvider-DqtNroSh.d.ts} +1 -1
  11. package/dist/impl/couch/index.d.mts +6 -6
  12. package/dist/impl/couch/index.d.ts +6 -6
  13. package/dist/impl/couch/index.js +1365 -1252
  14. package/dist/impl/couch/index.js.map +1 -1
  15. package/dist/impl/couch/index.mjs +1359 -1246
  16. package/dist/impl/couch/index.mjs.map +1 -1
  17. package/dist/impl/static/index.d.mts +8 -6
  18. package/dist/impl/static/index.d.ts +8 -6
  19. package/dist/impl/static/index.js +253 -843
  20. package/dist/impl/static/index.js.map +1 -1
  21. package/dist/impl/static/index.mjs +250 -842
  22. package/dist/impl/static/index.mjs.map +1 -1
  23. package/dist/index-CLL31bEy.d.ts +137 -0
  24. package/dist/index-CUNnL38E.d.mts +137 -0
  25. package/dist/index.d.mts +10 -55
  26. package/dist/index.d.ts +10 -55
  27. package/dist/index.js +343 -170
  28. package/dist/index.js.map +1 -1
  29. package/dist/index.mjs +340 -167
  30. package/dist/index.mjs.map +1 -1
  31. package/dist/{types-BvzcRAys.d.ts → types-BefDGkKa.d.ts} +1 -1
  32. package/dist/{types-CQQ80R5N.d.mts → types-DC-ckZug.d.mts} +1 -1
  33. package/dist/{types-legacy-CtrmkOLu.d.mts → types-legacy-Birv-Jx6.d.mts} +2 -2
  34. package/dist/{types-legacy-CtrmkOLu.d.ts → types-legacy-Birv-Jx6.d.ts} +2 -2
  35. package/dist/{userDB-DUY63VMN.d.ts → userDB-C33Hzjgn.d.mts} +10 -3
  36. package/dist/{userDB-7fM4tpgr.d.mts → userDB-DusL7OXe.d.ts} +10 -3
  37. package/dist/util/packer/index.d.mts +3 -63
  38. package/dist/util/packer/index.d.ts +3 -63
  39. package/dist/util/packer/index.js +53 -1
  40. package/dist/util/packer/index.js.map +1 -1
  41. package/dist/util/packer/index.mjs +53 -1
  42. package/dist/util/packer/index.mjs.map +1 -1
  43. package/package.json +7 -4
  44. package/src/core/types/types-legacy.ts +13 -1
  45. package/src/core/types/user.ts +9 -2
  46. package/src/core/util/index.ts +5 -4
  47. package/src/impl/common/BaseUserDB.ts +33 -22
  48. package/src/impl/common/SyncStrategy.ts +7 -0
  49. package/src/impl/common/index.ts +0 -1
  50. package/src/impl/common/userDBHelpers.ts +4 -4
  51. package/src/impl/couch/CouchDBSyncStrategy.ts +10 -0
  52. package/src/impl/couch/courseAPI.ts +7 -6
  53. package/src/impl/couch/index.ts +10 -5
  54. package/src/impl/couch/updateQueue.ts +12 -8
  55. package/src/impl/couch/user-course-relDB.ts +17 -27
  56. package/src/impl/static/NoOpSyncStrategy.ts +5 -0
  57. package/src/impl/static/StaticDataUnpacker.ts +18 -36
  58. package/src/impl/static/courseDB.ts +135 -17
  59. package/src/util/migrator/FileSystemAdapter.ts +20 -0
  60. package/src/util/migrator/StaticToCouchDBMigrator.ts +6 -0
  61. package/src/util/packer/CouchDBToStaticPacker.ts +92 -2
@@ -42,6 +42,20 @@ var init_SyncStrategy = __esm({
42
42
  }
43
43
  });
44
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
+
45
59
  // src/util/logger.ts
46
60
  var isDevelopment, logger;
47
61
  var init_logger = __esm({
@@ -87,31 +101,6 @@ var init_logger = __esm({
87
101
  }
88
102
  });
89
103
 
90
- // src/core/types/types-legacy.ts
91
- var GuestUsername, log, cardHistoryPrefix;
92
- var init_types_legacy = __esm({
93
- "src/core/types/types-legacy.ts"() {
94
- "use strict";
95
- init_logger();
96
- GuestUsername = "Guest";
97
- log = (message) => {
98
- logger.log(message);
99
- };
100
- cardHistoryPrefix = "cardH";
101
- }
102
- });
103
-
104
- // src/core/util/index.ts
105
- function getCardHistoryID(courseID, cardID) {
106
- return `${cardHistoryPrefix}-${courseID}-${cardID}`;
107
- }
108
- var init_util = __esm({
109
- "src/core/util/index.ts"() {
110
- "use strict";
111
- init_types_legacy();
112
- }
113
- });
114
-
115
104
  // src/impl/couch/pouchdb-setup.ts
116
105
  var import_pouchdb, import_pouchdb_find, import_pouchdb_authentication, pouchdb_setup_default;
117
106
  var init_pouchdb_setup = __esm({
@@ -131,123 +120,6 @@ var init_pouchdb_setup = __esm({
131
120
  }
132
121
  });
133
122
 
134
- // src/util/tuiLogger.ts
135
- var init_tuiLogger = __esm({
136
- "src/util/tuiLogger.ts"() {
137
- "use strict";
138
- init_dataDirectory();
139
- }
140
- });
141
-
142
- // src/util/dataDirectory.ts
143
- function getAppDataDirectory() {
144
- return path.join(os.homedir(), ".tuilder");
145
- }
146
- function getDbPath(dbName) {
147
- return path.join(getAppDataDirectory(), dbName);
148
- }
149
- var path, os;
150
- var init_dataDirectory = __esm({
151
- "src/util/dataDirectory.ts"() {
152
- "use strict";
153
- path = __toESM(require("path"));
154
- os = __toESM(require("os"));
155
- init_tuiLogger();
156
- }
157
- });
158
-
159
- // src/impl/common/userDBHelpers.ts
160
- function hexEncode(str) {
161
- let hex;
162
- let returnStr = "";
163
- for (let i = 0; i < str.length; i++) {
164
- hex = str.charCodeAt(i).toString(16);
165
- returnStr += ("000" + hex).slice(3);
166
- }
167
- return returnStr;
168
- }
169
- function filterAllDocsByPrefix(db, prefix, opts) {
170
- const options = {
171
- startkey: prefix,
172
- endkey: prefix + "\uFFF0",
173
- include_docs: true
174
- };
175
- if (opts) {
176
- Object.assign(options, opts);
177
- }
178
- return db.allDocs(options);
179
- }
180
- function getStartAndEndKeys(key) {
181
- return {
182
- startkey: key,
183
- endkey: key + "\uFFF0"
184
- };
185
- }
186
- function updateGuestAccountExpirationDate(guestDB) {
187
- const currentTime = import_moment.default.utc();
188
- const expirationDate = currentTime.add(2, "months").toISOString();
189
- const expiryDocID2 = "GuestAccountExpirationDate";
190
- void guestDB.get(expiryDocID2).then((doc) => {
191
- return guestDB.put({
192
- _id: expiryDocID2,
193
- _rev: doc._rev,
194
- date: expirationDate
195
- });
196
- }).catch(() => {
197
- return guestDB.put({
198
- _id: expiryDocID2,
199
- date: expirationDate
200
- });
201
- });
202
- }
203
- function getLocalUserDB(username) {
204
- const dbName = `userdb-${username}`;
205
- if (typeof window === "undefined") {
206
- return new pouchdb_setup_default(getDbPath(dbName), {});
207
- } else {
208
- return new pouchdb_setup_default(dbName, {});
209
- }
210
- }
211
- function scheduleCardReviewLocal(userDB, review) {
212
- const now = import_moment.default.utc();
213
- logger.info(`Scheduling for review in: ${review.time.diff(now, "h") / 24} days`);
214
- void userDB.put({
215
- _id: REVIEW_PREFIX + review.time.format(REVIEW_TIME_FORMAT),
216
- cardId: review.card_id,
217
- reviewTime: review.time,
218
- courseId: review.course_id,
219
- scheduledAt: now,
220
- scheduledFor: review.scheduledFor,
221
- schedulingAgentId: review.schedulingAgentId
222
- });
223
- }
224
- async function removeScheduledCardReviewLocal(userDB, reviewDocID) {
225
- const reviewDoc = await userDB.get(reviewDocID);
226
- userDB.remove(reviewDoc).then((res) => {
227
- if (res.ok) {
228
- log2(`Removed Review Doc: ${reviewDocID}`);
229
- }
230
- }).catch((err) => {
231
- log2(`Failed to remove Review Doc: ${reviewDocID},
232
- ${JSON.stringify(err)}`);
233
- });
234
- }
235
- var import_moment, REVIEW_PREFIX, REVIEW_TIME_FORMAT, log2;
236
- var init_userDBHelpers = __esm({
237
- "src/impl/common/userDBHelpers.ts"() {
238
- "use strict";
239
- import_moment = __toESM(require("moment"));
240
- init_logger();
241
- init_pouchdb_setup();
242
- init_dataDirectory();
243
- REVIEW_PREFIX = "card_review_";
244
- REVIEW_TIME_FORMAT = "YYYY-MM-DD--kk:mm:ss-SSS";
245
- log2 = (s) => {
246
- logger.info(s);
247
- };
248
- }
249
- });
250
-
251
123
  // src/util/Loggable.ts
252
124
  var Loggable;
253
125
  var init_Loggable = __esm({
@@ -275,7 +147,10 @@ var init_updateQueue = __esm({
275
147
  _className = "UpdateQueue";
276
148
  pendingUpdates = {};
277
149
  inprogressUpdates = {};
278
- db;
150
+ readDB;
151
+ // Database for read operations
152
+ writeDB;
153
+ // Database for write operations (local-first)
279
154
  update(id, update) {
280
155
  logger.debug(`Update requested on doc: ${id}`);
281
156
  if (this.pendingUpdates[id]) {
@@ -285,24 +160,25 @@ var init_updateQueue = __esm({
285
160
  }
286
161
  return this.applyUpdates(id);
287
162
  }
288
- constructor(db) {
163
+ constructor(readDB, writeDB) {
289
164
  super();
290
- this.db = db;
165
+ this.readDB = readDB;
166
+ this.writeDB = writeDB || readDB;
291
167
  logger.debug(`UpdateQ initialized...`);
292
- void this.db.info().then((i) => {
168
+ void this.readDB.info().then((i) => {
293
169
  logger.debug(`db info: ${JSON.stringify(i)}`);
294
170
  });
295
171
  }
296
172
  async applyUpdates(id) {
297
173
  logger.debug(`Applying updates on doc: ${id}`);
298
174
  if (this.inprogressUpdates[id]) {
299
- await this.db.info();
175
+ await this.readDB.info();
300
176
  return this.applyUpdates(id);
301
177
  } else {
302
178
  if (this.pendingUpdates[id] && this.pendingUpdates[id].length > 0) {
303
179
  this.inprogressUpdates[id] = true;
304
180
  try {
305
- let doc = await this.db.get(id);
181
+ let doc = await this.readDB.get(id);
306
182
  logger.debug(`Retrieved doc: ${id}`);
307
183
  while (this.pendingUpdates[id].length !== 0) {
308
184
  const update = this.pendingUpdates[id].splice(0, 1)[0];
@@ -315,7 +191,7 @@ var init_updateQueue = __esm({
315
191
  };
316
192
  }
317
193
  }
318
- await this.db.put(doc);
194
+ await this.writeDB.put(doc);
319
195
  logger.debug(`Put doc: ${id}`);
320
196
  if (this.pendingUpdates[id].length === 0) {
321
197
  this.inprogressUpdates[id] = false;
@@ -341,6 +217,32 @@ var init_updateQueue = __esm({
341
217
  }
342
218
  });
343
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
+
344
246
  // src/impl/couch/clientCache.ts
345
247
  async function GET_CACHED(k, f) {
346
248
  if (CLIENT_CACHE[k]) {
@@ -364,7 +266,8 @@ var init_clientCache = __esm({
364
266
  async function addNote55(courseID, codeCourse, shape, data, author, tags, uploads, elo = (0, import_common2.blankCourseElo)()) {
365
267
  const db = getCourseDB(courseID);
366
268
  const payload = (0, import_common3.prepareNote55)(courseID, codeCourse, shape, data, author, tags, uploads);
367
- const result = await db.post(payload);
269
+ const _id = `${DocTypePrefixes["DISPLAYABLE_DATA" /* DISPLAYABLE_DATA */]}-${(0, import_uuid.v4)()}`;
270
+ const result = await db.put({ ...payload, _id });
368
271
  const dataShapeId = import_common.NameSpacer.getDataShapeString({
369
272
  course: codeCourse,
370
273
  dataShape: shape.name
@@ -435,7 +338,9 @@ async function createCard(questionViewName, courseID, dsDescriptor, noteID, tags
435
338
  }
436
339
  async function addCard(courseID, course, id_displayable_data, id_view, elo, tags, author) {
437
340
  const db = getCourseDB(courseID);
438
- const card = await db.post({
341
+ const _id = `${DocTypePrefixes["CARD" /* CARD */]}-${(0, import_uuid.v4)()}`;
342
+ const card = await db.put({
343
+ _id,
439
344
  course,
440
345
  id_displayable_data,
441
346
  id_view,
@@ -528,7 +433,7 @@ function getCourseDB(courseID) {
528
433
  pouchDBincludeCredentialsConfig
529
434
  );
530
435
  }
531
- var import_common, import_common2, import_common3, AlreadyTaggedErr;
436
+ var import_common, import_common2, import_common3, import_uuid, AlreadyTaggedErr;
532
437
  var init_courseAPI = __esm({
533
438
  "src/impl/couch/courseAPI.ts"() {
534
439
  "use strict";
@@ -542,6 +447,7 @@ var init_courseAPI = __esm({
542
447
  import_common3 = require("@vue-skuilder/common");
543
448
  init_common();
544
449
  init_logger();
450
+ import_uuid = require("uuid");
545
451
  AlreadyTaggedErr = class extends Error {
546
452
  constructor(message) {
547
453
  super(message);
@@ -811,7 +717,7 @@ async function getCourseQuestionTypes(courseID) {
811
717
  }
812
718
  async function getCourseTagStubs(courseID) {
813
719
  logger.debug(`Getting tag stubs for course: ${courseID}`);
814
- const stubs = await filterAllDocsByPrefix2(
720
+ const stubs = await filterAllDocsByPrefix(
815
721
  getCourseDB2(courseID),
816
722
  "TAG" /* TAG */.valueOf() + "-"
817
723
  );
@@ -876,7 +782,7 @@ function getAncestorTagIDs(courseID, tagID) {
876
782
  }
877
783
  }
878
784
  async function getChildTagStubs(courseID, tagID) {
879
- return await filterAllDocsByPrefix2(getCourseDB2(courseID), tagID + ">");
785
+ return await filterAllDocsByPrefix(getCourseDB2(courseID), tagID + ">");
880
786
  }
881
787
  async function getAppliedTags(id_course, id_card) {
882
788
  const db = getCourseDB2(id_course);
@@ -1383,1217 +1289,1420 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
1383
1289
  }
1384
1290
  });
1385
1291
 
1386
- // src/impl/couch/user-course-relDB.ts
1387
- var import_moment2, UsrCrsData;
1388
- var init_user_course_relDB = __esm({
1389
- "src/impl/couch/user-course-relDB.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"() {
1390
1307
  "use strict";
1391
- import_moment2 = __toESM(require("moment"));
1308
+ init_factory();
1309
+ init_logger();
1310
+ import_moment = __toESM(require("moment"));
1311
+ init_pouchdb_setup();
1392
1312
  init_couch();
1393
1313
  init_courseDB();
1394
- init_logger();
1395
- UsrCrsData = class {
1396
- user;
1397
- course;
1398
- _courseId;
1399
- constructor(user, courseId) {
1400
- this.user = user;
1401
- this.course = new CourseDB(courseId, async () => this.user);
1402
- this._courseId = courseId;
1403
- }
1404
- async getReviewsForcast(daysCount) {
1405
- const time = import_moment2.default.utc().add(daysCount, "days");
1406
- return this.getReviewstoDate(time);
1407
- }
1408
- async getPendingReviews() {
1409
- const now = import_moment2.default.utc();
1410
- return this.getReviewstoDate(now);
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);
1411
1324
  }
1412
- async getScheduledReviewCount() {
1413
- return (await this.getPendingReviews()).length;
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;
1414
1336
  }
1415
- async getCourseSettings() {
1416
- const regDoc = await this.user.getCourseRegistrationsDoc();
1417
- const crsDoc = regDoc.courses.find((c) => c.courseID === this._courseId);
1418
- if (crsDoc && crsDoc.settings) {
1419
- return crsDoc.settings;
1337
+ getContentId(content) {
1338
+ if (content.type === "tag") {
1339
+ return `${this._content_prefix}-${content.courseID}-${content.tagID}`;
1420
1340
  } else {
1421
- logger.warn(`no settings found during lookup on course ${this._courseId}`);
1422
- return {};
1341
+ return `${this._content_prefix}-${content.courseID}`;
1423
1342
  }
1424
1343
  }
1425
- updateCourseSettings(updates) {
1426
- void this.user.updateCourseSettings(this._courseId, updates);
1344
+ get ready() {
1345
+ return this._initComplete;
1427
1346
  }
1428
- async getReviewstoDate(targetDate) {
1429
- const keys = getStartAndEndKeys2(REVIEW_PREFIX2);
1430
- const reviews = await this.user.remote().allDocs({
1431
- startkey: keys.startkey,
1432
- endkey: keys.endkey,
1433
- include_docs: true
1434
- });
1435
- logger.debug(
1436
- `Fetching ${this.user.getUsername()}'s scheduled reviews for course ${this._courseId}.`
1437
- );
1438
- return reviews.rows.filter((r) => {
1439
- if (r.id.startsWith(REVIEW_PREFIX2)) {
1440
- const date = import_moment2.default.utc(r.id.substr(REVIEW_PREFIX2.length), REVIEW_TIME_FORMAT2);
1441
- if (targetDate.isAfter(date)) {
1442
- if (this._courseId === void 0 || r.doc.courseId === this._courseId) {
1443
- return true;
1444
- }
1445
- }
1446
- }
1447
- }).map((r) => r.doc);
1347
+ getConfig() {
1348
+ return this._cfg;
1448
1349
  }
1449
1350
  };
1450
- }
1451
- });
1452
-
1453
- // src/impl/common/BaseUserDB.ts
1454
- async function getOrCreateClassroomRegistrationsDoc(user) {
1455
- let ret;
1456
- try {
1457
- ret = await getLocalUserDB(user).get(userClassroomsDoc);
1458
- } catch (e) {
1459
- const err = e;
1460
- if (err.status === 404) {
1461
- await getLocalUserDB(user).put({
1462
- _id: userClassroomsDoc,
1463
- registrations: []
1464
- });
1465
- ret = await getOrCreateClassroomRegistrationsDoc(user);
1466
- } else {
1467
- const errorDetails = {
1468
- name: err.name,
1469
- status: err.status,
1470
- message: err.message,
1471
- reason: err.reason,
1472
- error: err.error
1473
- };
1474
- logger.error(
1475
- "Database error in getOrCreateClassroomRegistrationsDoc (standalone function):",
1476
- errorDetails
1477
- );
1478
- throw new Error(
1479
- `Database error accessing classroom registrations: ${err.message || err.name || "Unknown error"} (status: ${err.status})`
1480
- );
1481
- }
1482
- }
1483
- return ret;
1484
- }
1485
- async function getOrCreateCourseRegistrationsDoc(user) {
1486
- let ret;
1487
- try {
1488
- ret = await getLocalUserDB(user).get(userCoursesDoc);
1489
- } catch (e) {
1490
- const err = e;
1491
- if (err.status === 404) {
1492
- await getLocalUserDB(user).put({
1493
- _id: userCoursesDoc,
1494
- courses: [],
1495
- studyWeight: {}
1496
- });
1497
- ret = await getOrCreateCourseRegistrationsDoc(user);
1498
- } else {
1499
- throw new Error(
1500
- `Unexpected error ${JSON.stringify(e)} in getOrCreateCourseRegistrationDoc...`
1501
- );
1502
- }
1503
- }
1504
- return ret;
1505
- }
1506
- async function updateUserElo(user, course_id, elo) {
1507
- const regDoc = await getOrCreateCourseRegistrationsDoc(user);
1508
- const course = regDoc.courses.find((c) => c.courseID === course_id);
1509
- course.elo = elo;
1510
- return getLocalUserDB(user).put(regDoc);
1511
- }
1512
- async function registerUserForClassroom(user, classID, registerAs) {
1513
- log3(`Registering user: ${user} in course: ${classID}`);
1514
- return getOrCreateClassroomRegistrationsDoc(user).then((doc) => {
1515
- const regItem = {
1516
- classID,
1517
- registeredAs: registerAs
1518
- };
1519
- if (doc.registrations.filter((reg) => {
1520
- return reg.classID === regItem.classID && reg.registeredAs === regItem.registeredAs;
1521
- }).length === 0) {
1522
- doc.registrations.push(regItem);
1523
- } else {
1524
- log3(`User ${user} is already registered for class ${classID}`);
1525
- }
1526
- return getLocalUserDB(user).put(doc);
1527
- });
1528
- }
1529
- async function dropUserFromClassroom(user, classID) {
1530
- return getOrCreateClassroomRegistrationsDoc(user).then((doc) => {
1531
- let index = -1;
1532
- for (let i = 0; i < doc.registrations.length; i++) {
1533
- if (doc.registrations[i].classID === classID) {
1534
- index = i;
1535
- }
1536
- }
1537
- if (index !== -1) {
1538
- doc.registrations.splice(index, 1);
1539
- }
1540
- return getLocalUserDB(user).put(doc);
1541
- });
1542
- }
1543
- async function getUserClassrooms(user) {
1544
- return getOrCreateClassroomRegistrationsDoc(user);
1545
- }
1546
- var import_common6, import_moment3, log3, cardHistoryPrefix2, BaseUser, userCoursesDoc, userClassroomsDoc;
1547
- var init_BaseUserDB = __esm({
1548
- "src/impl/common/BaseUserDB.ts"() {
1549
- "use strict";
1550
- init_util();
1551
- import_common6 = require("@vue-skuilder/common");
1552
- import_moment3 = __toESM(require("moment"));
1553
- init_types_legacy();
1554
- init_logger();
1555
- init_userDBHelpers();
1556
- init_updateQueue();
1557
- init_user_course_relDB();
1558
- init_couch();
1559
- log3 = (s) => {
1560
- logger.info(s);
1561
- };
1562
- cardHistoryPrefix2 = "cardH-";
1563
- BaseUser = class _BaseUser {
1564
- static _instance;
1565
- static _initialized = false;
1566
- static Dummy(syncStrategy) {
1567
- return new _BaseUser("Me", syncStrategy);
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;
1568
1359
  }
1569
- static DOC_IDS = {
1570
- CONFIG: "CONFIG",
1571
- COURSE_REGISTRATIONS: "CourseRegistrations",
1572
- CLASSROOM_REGISTRATIONS: "ClassroomRegistrations"
1573
- };
1574
- // private email: string;
1575
- _username;
1576
- syncStrategy;
1577
- getUsername() {
1578
- return this._username;
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
+ );
1366
+ try {
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)}`);
1378
+ }
1579
1379
  }
1580
- isLoggedIn() {
1581
- return !this._username.startsWith(GuestUsername);
1380
+ static async factory(classID, user) {
1381
+ const ret = new _StudentClassroomDB(classID, user);
1382
+ await ret.init();
1383
+ return ret;
1582
1384
  }
1583
- remoteDB;
1584
- remote() {
1585
- return this.remoteDB;
1385
+ setChangeFcn(f) {
1386
+ void this.userMessages.on("change", f);
1586
1387
  }
1587
- localDB;
1588
- updateQueue;
1589
- async createAccount(username, password) {
1590
- if (!this.syncStrategy.canCreateAccount()) {
1591
- throw new Error("Account creation not supported by current sync strategy");
1592
- }
1593
- if (!this._username.startsWith(GuestUsername)) {
1594
- throw new Error(
1595
- `Cannot create a new account while logged in:
1596
- Currently logged-in as ${this._username}.`
1597
- );
1598
- }
1599
- const result = await this.syncStrategy.createAccount(username, password);
1600
- if (result.status === import_common6.Status.ok) {
1601
- log3(`Account created successfully, updating username to ${username}`);
1602
- this._username = username;
1603
- try {
1604
- localStorage.removeItem("dbUUID");
1605
- } catch (e) {
1606
- logger.warn("localStorage not available (Node.js environment):", e);
1607
- }
1608
- await this.init();
1609
- }
1610
- return {
1611
- status: result.status,
1612
- error: result.error || ""
1613
- };
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
+ };
1401
+ });
1614
1402
  }
1615
- async login(username, password) {
1616
- if (!this.syncStrategy.canAuthenticate()) {
1617
- throw new Error("Authentication not supported by current sync strategy");
1618
- }
1619
- if (!this._username.startsWith(GuestUsername) && this._username != username) {
1620
- if (this._username != username) {
1621
- throw new Error(`Cannot change accounts while logged in.
1622
- Log out of account ${this.getUsername()} before logging in as ${username}.`);
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));
1623
1431
  }
1624
- logger.warn(`User ${this._username} is already logged in, but executing login again.`);
1625
1432
  }
1626
- const loginResult = await this.syncStrategy.authenticate(username, password);
1627
- if (loginResult.ok) {
1628
- log3(`Logged in as ${username}`);
1629
- this._username = username;
1630
- try {
1631
- localStorage.removeItem("dbUUID");
1632
- } catch (e) {
1633
- logger.warn("localStorage not available (Node.js environment):", e);
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;
1634
1439
  }
1635
- await this.init();
1636
- }
1637
- return loginResult;
1440
+ });
1638
1441
  }
1639
- async resetUserData() {
1640
- if (this.syncStrategy.canAuthenticate()) {
1641
- return {
1642
- status: import_common6.Status.error,
1643
- error: "Reset user data is only available for local-only mode. Use logout instead for remote sync."
1644
- };
1645
- }
1646
- try {
1647
- const localDB = getLocalUserDB(this._username);
1648
- const allDocs = await localDB.allDocs({ include_docs: false });
1649
- const docsToDelete = allDocs.rows.filter((row) => {
1650
- const id = row.id;
1651
- return id.startsWith(cardHistoryPrefix2) || // Card interaction history
1652
- id.startsWith(REVIEW_PREFIX) || // Scheduled reviews
1653
- id === _BaseUser.DOC_IDS.COURSE_REGISTRATIONS || // Course registrations
1654
- id === _BaseUser.DOC_IDS.CLASSROOM_REGISTRATIONS || // Classroom registrations
1655
- id === _BaseUser.DOC_IDS.CONFIG;
1656
- }).map((row) => ({ _id: row.id, _rev: row.value.rev, _deleted: true }));
1657
- if (docsToDelete.length > 0) {
1658
- await localDB.bulkDocs(docsToDelete);
1659
- }
1660
- await this.init();
1661
- return { status: import_common6.Status.ok };
1662
- } catch (error) {
1663
- logger.error("Failed to reset user data:", error);
1664
- return {
1665
- status: import_common6.Status.error,
1666
- error: error instanceof Error ? error.message : "Unknown error during reset"
1667
- };
1668
- }
1669
- }
1670
- async logout() {
1671
- if (!this.syncStrategy.canAuthenticate()) {
1672
- this._username = await this.syncStrategy.getCurrentUsername();
1673
- await this.init();
1674
- return { ok: true };
1675
- }
1676
- const ret = await this.syncStrategy.logout();
1677
- this._username = await this.syncStrategy.getCurrentUsername();
1678
- await this.init();
1679
- return ret;
1680
- }
1681
- update(id, update) {
1682
- return this.updateQueue.update(id, update);
1442
+ };
1443
+ TeacherClassroomDB = class _TeacherClassroomDB extends ClassroomDBBase {
1444
+ _stuDb;
1445
+ constructor(classID) {
1446
+ super();
1447
+ this._id = classID;
1683
1448
  }
1684
- async getCourseRegistrationsDoc() {
1685
- logger.debug(`Fetching courseRegistrations for ${this.getUsername()}`);
1686
- let ret;
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
+ );
1687
1460
  try {
1688
- const regDoc = await this.localDB.get(
1689
- _BaseUser.DOC_IDS.COURSE_REGISTRATIONS
1690
- );
1691
- return regDoc;
1461
+ return this._db.get(CLASSROOM_CONFIG).then((cfg) => {
1462
+ this._cfg = cfg;
1463
+ this._initComplete = true;
1464
+ }).then(() => {
1465
+ return;
1466
+ });
1692
1467
  } catch (e) {
1693
- const err = e;
1694
- if (err.status === 404) {
1695
- await this.localDB.put({
1696
- _id: _BaseUser.DOC_IDS.COURSE_REGISTRATIONS,
1697
- courses: [],
1698
- studyWeight: {}
1699
- });
1700
- ret = await this.getCourseRegistrationsDoc();
1701
- } else {
1702
- throw new Error(
1703
- `Unexpected error ${JSON.stringify(e)} in getOrCreateCourseRegistrationDoc...`
1704
- );
1705
- }
1468
+ throw new Error(`Error in TeacherClassroomDB constructor: ${JSON.stringify(e)}`);
1706
1469
  }
1707
- return ret;
1708
1470
  }
1709
- async getActiveCourses() {
1710
- const reg = await this.getCourseRegistrationsDoc();
1711
- return reg.courses.filter((c) => {
1712
- return c.status === void 0 || c.status === "active";
1713
- });
1714
- }
1715
- /**
1716
- * Returns a promise of the card IDs that the user has
1717
- * a scheduled review for.
1718
- *
1719
- */
1720
- async getActiveCards() {
1721
- const keys = getStartAndEndKeys(REVIEW_PREFIX);
1722
- const reviews = await this.remoteDB.allDocs({
1723
- startkey: keys.startkey,
1724
- endkey: keys.endkey,
1725
- include_docs: true
1726
- });
1727
- return reviews.rows.map((r) => `${r.doc.courseId}-${r.doc.cardId}`);
1471
+ static async factory(classID) {
1472
+ const ret = new _TeacherClassroomDB(classID);
1473
+ await ret.init();
1474
+ return ret;
1728
1475
  }
1729
- async getActivityRecords() {
1476
+ async removeContent(content) {
1477
+ const contentID = this.getContentId(content);
1730
1478
  try {
1731
- const hist = await this.getHistory();
1732
- const allRecords = [];
1733
- if (!Array.isArray(hist)) {
1734
- logger.error("getHistory did not return an array:", hist);
1735
- return allRecords;
1736
- }
1737
- let sampleCount = 0;
1738
- for (let i = 0; i < hist.length; i++) {
1739
- try {
1740
- if (hist[i] && Array.isArray(hist[i].records)) {
1741
- hist[i].records.forEach((record) => {
1742
- try {
1743
- if (!record.timeStamp) {
1744
- return;
1745
- }
1746
- let timeStamp;
1747
- if (typeof record.timeStamp === "object") {
1748
- if (typeof record.timeStamp.toDate === "function") {
1749
- timeStamp = record.timeStamp.toISOString();
1750
- } else if (record.timeStamp instanceof Date) {
1751
- timeStamp = record.timeStamp.toISOString();
1752
- } else {
1753
- if (sampleCount < 3) {
1754
- logger.warn("Unknown timestamp object type:", record.timeStamp);
1755
- sampleCount++;
1756
- }
1757
- return;
1758
- }
1759
- } else if (typeof record.timeStamp === "string") {
1760
- const date = new Date(record.timeStamp);
1761
- if (isNaN(date.getTime())) {
1762
- return;
1763
- }
1764
- timeStamp = record.timeStamp;
1765
- } else if (typeof record.timeStamp === "number") {
1766
- timeStamp = new Date(record.timeStamp).toISOString();
1767
- } else {
1768
- return;
1769
- }
1770
- allRecords.push({
1771
- timeStamp,
1772
- courseID: record.courseID || "unknown",
1773
- cardID: record.cardID || "unknown",
1774
- timeSpent: record.timeSpent || 0,
1775
- type: "card_view"
1776
- });
1777
- } catch (err) {
1778
- }
1779
- });
1780
- }
1781
- } catch (err) {
1782
- logger.error("Error processing history item:", err);
1783
- }
1784
- }
1785
- logger.debug(`Found ${allRecords.length} activity records`);
1786
- return allRecords;
1787
- } catch (err) {
1788
- logger.error("Error in getActivityRecords:", err);
1789
- return [];
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);
1790
1486
  }
1791
1487
  }
1792
- async getReviewstoDate(targetDate, course_id) {
1793
- const keys = getStartAndEndKeys(REVIEW_PREFIX);
1794
- const reviews = await this.remoteDB.allDocs({
1795
- startkey: keys.startkey,
1796
- endkey: keys.endkey,
1797
- include_docs: true
1798
- });
1799
- log3(
1800
- `Fetching ${this._username}'s scheduled reviews${course_id ? ` for course ${course_id}` : ""}.`
1801
- );
1802
- return reviews.rows.filter((r) => {
1803
- if (r.id.startsWith(REVIEW_PREFIX)) {
1804
- const date = import_moment3.default.utc(r.id.substr(REVIEW_PREFIX.length), REVIEW_TIME_FORMAT);
1805
- if (targetDate.isAfter(date)) {
1806
- if (course_id === void 0 || r.doc.courseId === course_id) {
1807
- return true;
1808
- }
1809
- }
1810
- }
1811
- }).map((r) => r.doc);
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
+ });
1515
+ return true;
1516
+ } else {
1517
+ return false;
1518
+ }
1519
+ }
1520
+ };
1521
+ ClassroomLookupDB = () => new pouchdb_setup_default(ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + classroomLookupDBTitle, {
1522
+ skip_setup: true
1523
+ });
1524
+ }
1525
+ });
1526
+
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
+ }
1545
+ });
1546
+
1547
+ // src/core/interfaces/courseDB.ts
1548
+ var init_courseDB2 = __esm({
1549
+ "src/core/interfaces/courseDB.ts"() {
1550
+ "use strict";
1551
+ }
1552
+ });
1553
+
1554
+ // src/core/interfaces/dataLayerProvider.ts
1555
+ var init_dataLayerProvider = __esm({
1556
+ "src/core/interfaces/dataLayerProvider.ts"() {
1557
+ "use strict";
1558
+ }
1559
+ });
1560
+
1561
+ // src/core/interfaces/userDB.ts
1562
+ var init_userDB = __esm({
1563
+ "src/core/interfaces/userDB.ts"() {
1564
+ "use strict";
1565
+ }
1566
+ });
1567
+
1568
+ // src/core/interfaces/index.ts
1569
+ var init_interfaces = __esm({
1570
+ "src/core/interfaces/index.ts"() {
1571
+ "use strict";
1572
+ init_adminDB();
1573
+ init_classroomDB();
1574
+ init_contentSource();
1575
+ init_courseDB2();
1576
+ init_dataLayerProvider();
1577
+ init_userDB();
1578
+ }
1579
+ });
1580
+
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}`;
1591
+ }
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");
1650
+ }
1651
+ function getDbPath(dbName) {
1652
+ return path.join(getAppDataDirectory(), dbName);
1653
+ }
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;
1673
+ }
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);
1684
+ }
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
+ });
1706
+ });
1707
+ }
1708
+ function getLocalUserDB(username) {
1709
+ const dbName = `userdb-${username}`;
1710
+ if (typeof window === "undefined") {
1711
+ return new pouchdb_setup_default(getDbPath(dbName), {});
1712
+ } else {
1713
+ return new pouchdb_setup_default(dbName, {});
1714
+ }
1715
+ }
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
+ });
1728
+ }
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)}`);
1738
+ });
1739
+ }
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
+ };
1753
+ }
1754
+ });
1755
+
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"() {
1760
+ "use strict";
1761
+ import_moment3 = __toESM(require("moment"));
1762
+ init_logger();
1763
+ UsrCrsData = class {
1764
+ user;
1765
+ _courseId;
1766
+ constructor(user, courseId) {
1767
+ this.user = user;
1768
+ this._courseId = courseId;
1812
1769
  }
1813
1770
  async getReviewsForcast(daysCount) {
1814
1771
  const time = import_moment3.default.utc().add(daysCount, "days");
1815
1772
  return this.getReviewstoDate(time);
1816
1773
  }
1817
- async getPendingReviews(course_id) {
1774
+ async getPendingReviews() {
1818
1775
  const now = import_moment3.default.utc();
1819
- return this.getReviewstoDate(now, course_id);
1820
- }
1821
- async getScheduledReviewCount(course_id) {
1822
- return (await this.getPendingReviews(course_id)).length;
1776
+ return this.getReviewstoDate(now);
1823
1777
  }
1824
- async getRegisteredCourses() {
1825
- const regDoc = await this.getCourseRegistrationsDoc();
1826
- return regDoc.courses.filter((c) => {
1827
- return !c.status || c.status === "active" || c.status === "maintenance-mode";
1828
- });
1778
+ async getScheduledReviewCount() {
1779
+ return (await this.getPendingReviews()).length;
1829
1780
  }
1830
- async getCourseRegDoc(courseID) {
1831
- const regDocs = await this.getCourseRegistrationsDoc();
1832
- const ret = regDocs.courses.find((c) => c.courseID === courseID);
1833
- if (ret) {
1834
- return ret;
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;
1835
1786
  } else {
1836
- throw new Error(`Course registration not found for course ID: ${courseID}`);
1787
+ logger.warn(`no settings found during lookup on course ${this._courseId}`);
1788
+ return {};
1837
1789
  }
1838
1790
  }
1839
- async registerForCourse(course_id, previewMode = false) {
1840
- return this.getCourseRegistrationsDoc().then((doc) => {
1841
- const status = previewMode ? "preview" : "active";
1842
- logger.debug(`Registering for ${course_id} with status: ${status}`);
1843
- const regItem = {
1844
- status,
1845
- courseID: course_id,
1846
- user: true,
1847
- admin: false,
1848
- moderator: false,
1849
- elo: {
1850
- global: {
1851
- score: 1e3,
1852
- count: 0
1853
- },
1854
- tags: {},
1855
- misc: {}
1856
- }
1857
- };
1858
- if (doc.courses.filter((course) => {
1859
- return course.courseID === regItem.courseID;
1860
- }).length === 0) {
1861
- log3(`It's a new course registration!`);
1862
- doc.courses.push(regItem);
1863
- doc.studyWeight[course_id] = 1;
1864
- } else {
1865
- doc.courses.forEach((c) => {
1866
- log3(`Found the previously registered course!`);
1867
- if (c.courseID === course_id) {
1868
- c.status = status;
1869
- }
1870
- });
1871
- }
1872
- return this.localDB.put(doc);
1873
- }).catch((e) => {
1874
- log3(`Registration failed because of: ${JSON.stringify(e)}`);
1875
- throw e;
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);
1876
1804
  });
1877
1805
  }
1878
- async dropCourse(course_id, dropStatus = "dropped") {
1879
- return this.getCourseRegistrationsDoc().then((doc) => {
1880
- let index = -1;
1881
- for (let i = 0; i < doc.courses.length; i++) {
1882
- if (doc.courses[i].courseID === course_id) {
1883
- index = i;
1884
- }
1885
- }
1886
- if (index !== -1) {
1887
- delete doc.studyWeight[course_id];
1888
- doc.courses[index].status = dropStatus;
1889
- } else {
1890
- throw new Error(
1891
- `User ${this.getUsername()} is not currently registered for course ${course_id}`
1892
- );
1893
- }
1894
- return this.localDB.put(doc);
1895
- });
1806
+ };
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;
1892
+ }
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);
1925
+ }
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;
1896
1936
  }
1897
- async getCourseInterface(courseId) {
1898
- return new UsrCrsData(this, courseId);
1937
+ isLoggedIn() {
1938
+ return !this._username.startsWith(GuestUsername);
1899
1939
  }
1900
- async getUserEditableCourses() {
1901
- let courseIDs = [];
1902
- const registeredCourses = await this.getCourseRegistrationsDoc();
1903
- courseIDs = courseIDs.concat(
1904
- registeredCourses.courses.map((course) => {
1905
- return course.courseID;
1906
- })
1907
- );
1908
- const cfgs = await Promise.all(
1909
- courseIDs.map(async (id) => {
1910
- return await getCredentialledCourseConfig(id);
1911
- })
1912
- );
1913
- return cfgs;
1940
+ remote() {
1941
+ return this.remoteDB;
1914
1942
  }
1915
- async getConfig() {
1916
- const defaultConfig = {
1917
- _id: _BaseUser.DOC_IDS.CONFIG,
1918
- darkMode: false,
1919
- likesConfetti: false
1920
- };
1921
- try {
1922
- const cfg = await this.localDB.get(_BaseUser.DOC_IDS.CONFIG);
1923
- logger.debug("Raw config from DB:", cfg);
1924
- return cfg;
1925
- } catch (e) {
1926
- const err = e;
1927
- if (err.name && err.name === "not_found") {
1928
- await this.localDB.put(defaultConfig);
1929
- return this.getConfig();
1930
- } else {
1931
- logger.error(`Error setting user default config:`, e);
1932
- throw new Error(`Error returning the user's configuration: ${JSON.stringify(e)}`);
1933
- }
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");
1934
1951
  }
1935
- }
1936
- async setConfig(items) {
1937
- logger.debug(`Setting Config items ${JSON.stringify(items)}`);
1938
- const c = await this.getConfig();
1939
- const put = await this.localDB.put({
1940
- ...c,
1941
- ...items
1942
- });
1943
- if (put.ok) {
1944
- logger.debug(`Config items set: ${JSON.stringify(items)}`);
1945
- } else {
1946
- logger.error(`Error setting config items: ${JSON.stringify(put)}`);
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
+ );
1947
1957
  }
1948
- }
1949
- /**
1950
- *
1951
- * This function should be called *only* by the pouchdb datalayer provider
1952
- * auth store.
1953
- *
1954
- *
1955
- * Anyone else seeking the current user should use the auth store's
1956
- * exported `getCurrentUser` method.
1957
- *
1958
- */
1959
- static async instance(syncStrategy, username) {
1960
- if (username) {
1961
- _BaseUser._instance = new _BaseUser(username, syncStrategy);
1962
- await _BaseUser._instance.init();
1963
- return _BaseUser._instance;
1964
- } else if (_BaseUser._instance && _BaseUser._initialized) {
1965
- return _BaseUser._instance;
1966
- } else if (_BaseUser._instance) {
1967
- return new Promise((resolve) => {
1968
- (function waitForUser() {
1969
- if (_BaseUser._initialized) {
1970
- return resolve(_BaseUser._instance);
1971
- } else {
1972
- setTimeout(waitForUser, 50);
1973
- }
1974
- })();
1975
- });
1976
- } else {
1977
- const guestUsername = await syncStrategy.getCurrentUsername();
1978
- _BaseUser._instance = new _BaseUser(guestUsername, syncStrategy);
1979
- await _BaseUser._instance.init();
1980
- return _BaseUser._instance;
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);
1966
+ }
1967
+ await this.init();
1981
1968
  }
1969
+ return {
1970
+ status: result.status,
1971
+ error: result.error || ""
1972
+ };
1982
1973
  }
1983
- constructor(username, syncStrategy) {
1984
- _BaseUser._initialized = false;
1985
- this._username = username;
1986
- this.syncStrategy = syncStrategy;
1987
- this.setDBandQ();
1988
- }
1989
- setDBandQ() {
1990
- this.localDB = getLocalUserDB(this._username);
1991
- this.remoteDB = this.syncStrategy.setupRemoteDB(this._username);
1992
- this.updateQueue = new UpdateQueue(this.localDB);
1993
- }
1994
- async init() {
1995
- _BaseUser._initialized = false;
1996
- if (this._username === "admin") {
1997
- _BaseUser._initialized = true;
1998
- return;
1974
+ async login(username, password) {
1975
+ if (!this.syncStrategy.canAuthenticate()) {
1976
+ throw new Error("Authentication not supported by current sync strategy");
1999
1977
  }
2000
- this.setDBandQ();
2001
- this.syncStrategy.startSync(this.localDB, this.remoteDB);
2002
- void this.applyDesignDocs();
2003
- void this.deduplicateReviews();
2004
- _BaseUser._initialized = true;
2005
- }
2006
- static designDocs = [
2007
- {
2008
- _id: "_design/reviewCards",
2009
- views: {
2010
- reviewCards: {
2011
- map: `function (doc) {
2012
- if (doc._id && doc._id.indexOf('card_review') === 0 && doc.courseId && doc.cardId) {
2013
- emit(doc._id, doc.courseId + '-' + doc.cardId);
2014
- }
2015
- }`
2016
- }
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}.`);
2017
1982
  }
1983
+ logger.warn(`User ${this._username} is already logged in, but executing login again.`);
2018
1984
  }
2019
- ];
2020
- async applyDesignDocs() {
2021
- if (this._username === "admin") {
2022
- return;
2023
- }
2024
- for (const doc of _BaseUser.designDocs) {
1985
+ const loginResult = await this.syncStrategy.authenticate(username, password);
1986
+ if (loginResult.ok) {
1987
+ log3(`Logged in as ${username}`);
1988
+ this._username = username;
2025
1989
  try {
2026
- try {
2027
- const existingDoc = await this.remoteDB.get(doc._id);
2028
- await this.remoteDB.put({
2029
- ...doc,
2030
- _rev: existingDoc._rev
2031
- });
2032
- } catch (e) {
2033
- if (e instanceof Error && e.name === "not_found") {
2034
- await this.remoteDB.put(doc);
2035
- } else {
2036
- throw e;
2037
- }
2038
- }
2039
- } catch (error) {
2040
- if (error.name && error.name === "conflict") {
2041
- logger.warn(`Design doc ${doc._id} update conflict - will retry`);
2042
- await new Promise((resolve) => setTimeout(resolve, 1e3));
2043
- await this.applyDesignDoc(doc);
2044
- } else {
2045
- logger.error(`Failed to apply design doc ${doc._id}:`, error);
2046
- throw error;
2047
- }
1990
+ localStorage.removeItem("dbUUID");
1991
+ } catch (e) {
1992
+ logger.warn("localStorage not available (Node.js environment):", e);
1993
+ }
1994
+ await this.init();
1995
+ }
1996
+ return loginResult;
1997
+ }
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
+ };
2004
+ }
2005
+ try {
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);
2048
2018
  }
2019
+ await this.init();
2020
+ return { status: import_common7.Status.ok };
2021
+ } catch (error) {
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
+ };
2027
+ }
2028
+ }
2029
+ async logout() {
2030
+ if (!this.syncStrategy.canAuthenticate()) {
2031
+ this._username = await this.syncStrategy.getCurrentUsername();
2032
+ await this.init();
2033
+ return { ok: true };
2049
2034
  }
2035
+ const ret = await this.syncStrategy.logout();
2036
+ this._username = await this.syncStrategy.getCurrentUsername();
2037
+ await this.init();
2038
+ return ret;
2050
2039
  }
2051
- // Helper method for single doc update with retry
2052
- async applyDesignDoc(doc, retries = 3) {
2040
+ update(id, update) {
2041
+ return this.updateQueue.update(id, update);
2042
+ }
2043
+ async getCourseRegistrationsDoc() {
2044
+ logger.debug(`Fetching courseRegistrations for ${this.getUsername()}`);
2045
+ let ret;
2053
2046
  try {
2054
- const existingDoc = await this.remoteDB.get(doc._id);
2055
- await this.remoteDB.put({
2056
- ...doc,
2057
- _rev: existingDoc._rev
2058
- });
2047
+ const regDoc = await this.localDB.get(
2048
+ _BaseUser.DOC_IDS.COURSE_REGISTRATIONS
2049
+ );
2050
+ return regDoc;
2059
2051
  } catch (e) {
2060
- if (e instanceof Error && e.name === "conflict" && retries > 0) {
2061
- await new Promise((resolve) => setTimeout(resolve, 1e3));
2062
- return this.applyDesignDoc(doc, retries - 1);
2052
+ const err = e;
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
+ );
2063
2064
  }
2064
- throw e;
2065
2065
  }
2066
+ return ret;
2067
+ }
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
+ });
2066
2073
  }
2067
2074
  /**
2068
- * Logs a record of the user's interaction with the card and returns the card's
2069
- * up-to-date history
2070
- *
2071
- * // [ ] #db-refactor extract to a smaller scope - eg, UserStudySession
2075
+ * Returns a promise of the card IDs that the user has
2076
+ * a scheduled review for.
2072
2077
  *
2073
- * @param record the recent recorded interaction between user and card
2074
- * @returns The updated state of the card's CardHistory data
2075
2078
  */
2076
- async putCardRecord(record) {
2077
- const cardHistoryID = getCardHistoryID(record.courseID, record.cardID);
2078
- record.timeStamp = import_moment3.default.utc(record.timeStamp).toString();
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}`);
2087
+ }
2088
+ async getActivityRecords() {
2079
2089
  try {
2080
- const cardHistory = await this.update(
2081
- cardHistoryID,
2082
- function(h) {
2083
- h.records.push(record);
2084
- h.bestInterval = h.bestInterval || 0;
2085
- h.lapses = h.lapses || 0;
2086
- h.streak = h.streak || 0;
2087
- return h;
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);
2088
2142
  }
2089
- );
2090
- cardHistory.records = cardHistory.records.map((record2) => {
2091
- const ret = {
2092
- ...record2
2093
- };
2094
- ret.timeStamp = import_moment3.default.utc(record2.timeStamp);
2095
- return ret;
2096
- });
2097
- return cardHistory;
2098
- } catch (e) {
2099
- const reason = e;
2100
- if (reason.status === 404) {
2101
- const initCardHistory = {
2102
- _id: cardHistoryID,
2103
- cardID: record.cardID,
2104
- courseID: record.courseID,
2105
- records: [record],
2106
- lapses: 0,
2107
- streak: 0,
2108
- bestInterval: 0
2109
- };
2110
- void this.remoteDB.put(initCardHistory);
2111
- return initCardHistory;
2112
- } else {
2113
- throw new Error(`putCardRecord failed because of:
2114
- name:${reason.name}
2115
- error: ${reason.error}
2116
- message: ${reason.message}`);
2117
2143
  }
2144
+ logger.debug(`Found ${allRecords.length} activity records`);
2145
+ return allRecords;
2146
+ } catch (err) {
2147
+ logger.error("Error in getActivityRecords:", err);
2148
+ return [];
2118
2149
  }
2119
2150
  }
2120
- async deduplicateReviews() {
2121
- try {
2122
- log3("Starting deduplication of scheduled reviews...");
2123
- const reviewsMap = {};
2124
- const duplicateDocIds = [];
2125
- const scheduledReviews = await this.remoteDB.query("reviewCards/reviewCards");
2126
- log3(`Found ${scheduledReviews.rows.length} scheduled reviews to process`);
2127
- scheduledReviews.rows.forEach((r) => {
2128
- const qualifiedCardId = r.value;
2129
- const docId = r.key;
2130
- if (reviewsMap[qualifiedCardId]) {
2131
- log3(`Found duplicate scheduled review for card: ${qualifiedCardId}`);
2132
- log3(
2133
- `Marking earlier review ${reviewsMap[qualifiedCardId]} for deletion, keeping ${docId}`
2134
- );
2135
- duplicateDocIds.push(reviewsMap[qualifiedCardId]);
2136
- reviewsMap[qualifiedCardId] = docId;
2137
- } else {
2138
- reviewsMap[qualifiedCardId] = docId;
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
+ }
2139
2171
  }
2140
- });
2141
- if (duplicateDocIds.length > 0) {
2142
- log3(`Removing ${duplicateDocIds.length} duplicate reviews...`);
2143
- const deletePromises = duplicateDocIds.map(async (docId) => {
2144
- try {
2145
- const doc = await this.remoteDB.get(docId);
2146
- await this.remoteDB.remove(doc);
2147
- log3(`Successfully removed duplicate review: ${docId}`);
2148
- } catch (error) {
2149
- log3(`Failed to remove duplicate review ${docId}: ${error}`);
2172
+ }
2173
+ }).map((r) => r.doc);
2174
+ }
2175
+ async getReviewsForcast(daysCount) {
2176
+ const time = import_moment4.default.utc().add(daysCount, "days");
2177
+ return this.getReviewstoDate(time);
2178
+ }
2179
+ async getPendingReviews(course_id) {
2180
+ const now = import_moment4.default.utc();
2181
+ return this.getReviewstoDate(now, course_id);
2182
+ }
2183
+ async getScheduledReviewCount(course_id) {
2184
+ return (await this.getPendingReviews(course_id)).length;
2185
+ }
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
+ });
2191
+ }
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}`);
2199
+ }
2200
+ }
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;
2150
2231
  }
2151
2232
  });
2152
- await Promise.all(deletePromises);
2153
- log3(`Deduplication complete. Processed ${duplicateDocIds.length} duplicates`);
2154
- } else {
2155
- log3("No duplicate reviews found");
2156
- }
2157
- } catch (error) {
2158
- log3(`Error during review deduplication: ${error}`);
2159
- }
2160
- }
2161
- /**
2162
- * Returns a promise of the card IDs that the user has
2163
- * encountered in the past.
2164
- *
2165
- * @param course_id optional specification of individual course
2166
- */
2167
- async getSeenCards(course_id) {
2168
- let prefix = cardHistoryPrefix2;
2169
- if (course_id) {
2170
- prefix += course_id;
2171
- }
2172
- const docs = await filterAllDocsByPrefix(this.localDB, prefix, {
2173
- include_docs: false
2174
- });
2175
- const ret = [];
2176
- docs.rows.forEach((row) => {
2177
- if (row.id.startsWith(cardHistoryPrefix2)) {
2178
- ret.push(row.id.substr(cardHistoryPrefix2.length));
2179
2233
  }
2234
+ return this.localDB.put(doc);
2235
+ }).catch((e) => {
2236
+ log3(`Registration failed because of: ${JSON.stringify(e)}`);
2237
+ throw e;
2180
2238
  });
2181
- return ret;
2182
- }
2183
- /**
2184
- *
2185
- * @returns A promise of the cards that the user has seen in the past.
2186
- */
2187
- async getHistory() {
2188
- const cards = await filterAllDocsByPrefix(
2189
- this.remoteDB,
2190
- cardHistoryPrefix2,
2191
- {
2192
- include_docs: true,
2193
- attachments: false
2194
- }
2195
- );
2196
- return cards.rows.map((r) => r.doc);
2197
2239
  }
2198
- async updateCourseSettings(course_id, settings) {
2199
- void this.getCourseRegistrationsDoc().then((doc) => {
2200
- const crs = doc.courses.find((c) => c.courseID === course_id);
2201
- if (crs) {
2202
- if (crs.settings === null || crs.settings === void 0) {
2203
- crs.settings = {};
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;
2204
2246
  }
2205
- settings.forEach((setting) => {
2206
- crs.settings[setting.key] = setting.value;
2207
- });
2247
+ }
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
+ );
2208
2255
  }
2209
2256
  return this.localDB.put(doc);
2210
2257
  });
2211
2258
  }
2212
- async getCourseSettings(course_id) {
2213
- const regDoc = await this.getCourseRegistrationsDoc();
2214
- const crsDoc = regDoc.courses.find((c) => c.courseID === course_id);
2215
- if (crsDoc) {
2216
- return crsDoc.settings;
2217
- } else {
2218
- throw new Error(`getCourseSettings Failed:
2219
- User is not registered for course ${course_id}`);
2220
- }
2259
+ async getCourseInterface(courseId) {
2260
+ return new UsrCrsData(this, courseId);
2221
2261
  }
2222
- async getOrCreateClassroomRegistrationsDoc() {
2223
- let 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;
2276
+ }
2277
+ async getConfig() {
2278
+ const defaultConfig = {
2279
+ _id: _BaseUser.DOC_IDS.CONFIG,
2280
+ darkMode: false,
2281
+ likesConfetti: false,
2282
+ sessionTimeLimit: 5
2283
+ };
2224
2284
  try {
2225
- ret = await this.remoteDB.get(
2226
- _BaseUser.DOC_IDS.CLASSROOM_REGISTRATIONS
2227
- );
2285
+ const cfg = await this.localDB.get(_BaseUser.DOC_IDS.CONFIG);
2286
+ logger.debug("Raw config from DB:", cfg);
2287
+ return cfg;
2228
2288
  } catch (e) {
2229
2289
  const err = e;
2230
- if (err.status === 404) {
2231
- await this.remoteDB.put({
2232
- _id: _BaseUser.DOC_IDS.CLASSROOM_REGISTRATIONS,
2233
- registrations: []
2234
- });
2235
- ret = await this.getOrCreateClassroomRegistrationsDoc();
2290
+ if (err.name && err.name === "not_found") {
2291
+ await this.localDB.put(defaultConfig);
2292
+ return this.getConfig();
2236
2293
  } else {
2237
- const errorDetails = {
2238
- name: err.name,
2239
- status: err.status,
2240
- message: err.message,
2241
- reason: err.reason,
2242
- error: err.error
2243
- };
2244
- logger.error(
2245
- "Database error in getOrCreateClassroomRegistrationsDoc (private method):",
2246
- errorDetails
2247
- );
2248
- throw new Error(
2249
- `Database error accessing classroom registrations: ${err.message || err.name || "Unknown error"} (status: ${err.status})`
2250
- );
2294
+ logger.error(`Error setting user default config:`, e);
2295
+ throw new Error(`Error returning the user's configuration: ${JSON.stringify(e)}`);
2251
2296
  }
2252
2297
  }
2253
- logger.debug(`Returning classroom registrations doc: ${JSON.stringify(ret)}`);
2254
- return ret;
2255
- }
2256
- /**
2257
- * Retrieves the list of active classroom IDs where the user is registered as a student.
2258
- *
2259
- * @returns Promise<string[]> - Array of classroom IDs, or empty array if classroom
2260
- * registration document is unavailable due to database errors
2261
- *
2262
- * @description This method gracefully handles database connectivity issues by returning
2263
- * an empty array when the classroom registrations document cannot be accessed.
2264
- * This ensures that users can still access other application features even
2265
- * when classroom functionality is temporarily unavailable.
2266
- */
2267
- async getActiveClasses() {
2268
- try {
2269
- return (await this.getOrCreateClassroomRegistrationsDoc()).registrations.filter((c) => c.registeredAs === "student").map((c) => c.classID);
2270
- } catch (error) {
2271
- logger.warn(
2272
- "Failed to load classroom registrations, continuing without classroom data:",
2273
- error
2274
- );
2275
- return [];
2276
- }
2277
- }
2278
- async scheduleCardReview(review) {
2279
- return scheduleCardReviewLocal(this.remoteDB, review);
2280
- }
2281
- async removeScheduledCardReview(reviewId) {
2282
- return removeScheduledCardReviewLocal(this.remoteDB, reviewId);
2283
- }
2284
- async registerForClassroom(_classId, _registerAs) {
2285
- return registerUserForClassroom(this._username, _classId, _registerAs);
2286
- }
2287
- async dropFromClassroom(classId) {
2288
- return dropUserFromClassroom(this._username, classId);
2289
- }
2290
- async getUserClassrooms() {
2291
- return getUserClassrooms(this._username);
2292
- }
2293
- async updateUserElo(courseId, elo) {
2294
- return updateUserElo(this._username, courseId, elo);
2295
- }
2296
- };
2297
- userCoursesDoc = "CourseRegistrations";
2298
- userClassroomsDoc = "ClassroomRegistrations";
2299
- }
2300
- });
2301
-
2302
- // src/impl/common/index.ts
2303
- var init_common = __esm({
2304
- "src/impl/common/index.ts"() {
2305
- "use strict";
2306
- init_SyncStrategy();
2307
- init_BaseUserDB();
2308
- init_userDBHelpers();
2309
- }
2310
- });
2311
-
2312
- // src/factory.ts
2313
- function getDataLayer() {
2314
- if (!dataLayerInstance) {
2315
- throw new Error("Data layer not initialized. Call initializeDataLayer first.");
2316
- }
2317
- return dataLayerInstance;
2318
- }
2319
- var ENV, dataLayerInstance;
2320
- var init_factory = __esm({
2321
- "src/factory.ts"() {
2322
- "use strict";
2323
- init_common();
2324
- init_logger();
2325
- ENV = {
2326
- COUCHDB_SERVER_PROTOCOL: "NOT_SET",
2327
- COUCHDB_SERVER_URL: "NOT_SET"
2328
- };
2329
- dataLayerInstance = null;
2330
- }
2331
- });
2332
-
2333
- // src/impl/couch/classroomDB.ts
2334
- function getClassroomDB(classID, version) {
2335
- const dbName = `classdb-${version}-${classID}`;
2336
- logger.info(`Retrieving classroom db: ${dbName}`);
2337
- return new pouchdb_setup_default(
2338
- ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + dbName,
2339
- pouchDBincludeCredentialsConfig
2340
- );
2341
- }
2342
- async function getClassroomConfig(classID) {
2343
- return await getClassroomDB(classID, "student").get(CLASSROOM_CONFIG);
2344
- }
2345
- var import_moment4, classroomLookupDBTitle, CLASSROOM_CONFIG, ClassroomDBBase, StudentClassroomDB, TeacherClassroomDB, ClassroomLookupDB;
2346
- var init_classroomDB = __esm({
2347
- "src/impl/couch/classroomDB.ts"() {
2348
- "use strict";
2349
- init_factory();
2350
- init_logger();
2351
- import_moment4 = __toESM(require("moment"));
2352
- init_pouchdb_setup();
2353
- init_couch();
2354
- init_courseDB();
2355
- classroomLookupDBTitle = "classdb-lookup";
2356
- CLASSROOM_CONFIG = "ClassroomConfig";
2357
- ClassroomDBBase = class {
2358
- _id;
2359
- _db;
2360
- _cfg;
2361
- _initComplete = false;
2362
- _content_prefix = "content";
2363
- get _content_searchkeys() {
2364
- return getStartAndEndKeys2(this._content_prefix);
2365
- }
2366
- async getAssignedContent() {
2367
- logger.info(`Getting assigned content...`);
2368
- const docRows = await this._db.allDocs({
2369
- startkey: this._content_prefix,
2370
- endkey: this._content_prefix + `\uFFF0`,
2371
- include_docs: true
2372
- });
2373
- const ret = docRows.rows.map((row) => {
2374
- return row.doc;
2375
- });
2376
- return ret;
2377
2298
  }
2378
- getContentId(content) {
2379
- if (content.type === "tag") {
2380
- return `${this._content_prefix}-${content.courseID}-${content.tagID}`;
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)}`);
2381
2308
  } else {
2382
- return `${this._content_prefix}-${content.courseID}`;
2309
+ logger.error(`Error setting config items: ${JSON.stringify(put)}`);
2383
2310
  }
2384
2311
  }
2385
- get ready() {
2386
- 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
+ }
2387
2345
  }
2388
- getConfig() {
2389
- return this._cfg;
2346
+ constructor(username, syncStrategy) {
2347
+ _BaseUser._initialized = false;
2348
+ this._username = username;
2349
+ this.syncStrategy = syncStrategy;
2350
+ this.setDBandQ();
2390
2351
  }
2391
- };
2392
- StudentClassroomDB = class _StudentClassroomDB extends ClassroomDBBase {
2393
- // private readonly _prefix: string = 'content';
2394
- userMessages;
2395
- _user;
2396
- constructor(classID, user) {
2397
- super();
2398
- this._id = classID;
2399
- 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);
2400
2357
  }
2401
2358
  async init() {
2402
- const dbName = `classdb-student-${this._id}`;
2403
- this._db = new pouchdb_setup_default(
2404
- ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + dbName,
2405
- pouchDBincludeCredentialsConfig
2406
- );
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) {
2407
2417
  try {
2408
- const cfg = await this._db.get(CLASSROOM_CONFIG);
2409
- this._cfg = cfg;
2410
- this.userMessages = this._db.changes({
2411
- since: "now",
2412
- live: true,
2413
- include_docs: true
2418
+ const existingDoc = await this.remoteDB.get(doc._id);
2419
+ await this.remoteDB.put({
2420
+ ...doc,
2421
+ _rev: existingDoc._rev
2414
2422
  });
2415
- this._initComplete = true;
2416
- return;
2417
2423
  } catch (e) {
2418
- 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;
2419
2429
  }
2420
2430
  }
2421
- static async factory(classID, user) {
2422
- const ret = new _StudentClassroomDB(classID, user);
2423
- await ret.init();
2424
- 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
+ }
2425
2483
  }
2426
- setChangeFcn(f) {
2427
- 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
+ }
2428
2524
  }
2429
- async getPendingReviews() {
2430
- const u = this._user;
2431
- return (await u.getPendingReviews()).filter((r) => r.scheduledFor === "classroom" && r.schedulingAgentId === this._id).map((r) => {
2432
- return {
2433
- ...r,
2434
- qualifiedID: `${r.courseId}-${r.cardId}`,
2435
- courseID: r.courseId,
2436
- cardID: r.cardId,
2437
- contentSourceType: "classroom",
2438
- contentSourceID: this._id,
2439
- reviewID: r._id,
2440
- status: "review"
2441
- };
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
+ }
2442
2544
  });
2545
+ return ret;
2443
2546
  }
2444
- async getNewCards() {
2445
- const activeCards = await this._user.getActiveCards();
2446
- const now = import_moment4.default.utc();
2447
- const assigned = await this.getAssignedContent();
2448
- const due = assigned.filter((c) => now.isAfter(import_moment4.default.utc(c.activeOn, REVIEW_TIME_FORMAT2)));
2449
- logger.info(`Due content: ${JSON.stringify(due)}`);
2450
- let ret = [];
2451
- for (let i = 0; i < due.length; i++) {
2452
- const content = due[i];
2453
- if (content.type === "course") {
2454
- const db = new CourseDB(content.courseID, async () => this._user);
2455
- ret = ret.concat(await db.getNewCards());
2456
- } else if (content.type === "tag") {
2457
- const tagDoc = await getTag(content.courseID, content.tagID);
2458
- ret = ret.concat(
2459
- tagDoc.taggedCards.map((c) => {
2460
- return {
2461
- courseID: content.courseID,
2462
- cardID: c,
2463
- qualifiedID: `${content.courseID}-${c}`,
2464
- contentSourceType: "classroom",
2465
- contentSourceID: this._id,
2466
- status: "new"
2467
- };
2468
- })
2469
- );
2470
- } else if (content.type === "card") {
2471
- 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
2472
2558
  }
2473
- }
2474
- logger.info(`New Cards from classroom ${this._cfg.name}: ${ret.map((c) => c.qualifiedID)}`);
2475
- return ret.filter((c) => {
2476
- if (activeCards.some((ac) => c.qualifiedID.includes(ac))) {
2477
- return false;
2478
- } else {
2479
- 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
+ });
2480
2572
  }
2573
+ return this.localDB.put(doc);
2481
2574
  });
2482
2575
  }
2483
- };
2484
- TeacherClassroomDB = class _TeacherClassroomDB extends ClassroomDBBase {
2485
- _stuDb;
2486
- constructor(classID) {
2487
- super();
2488
- 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
+ }
2489
2585
  }
2490
- async init() {
2491
- const dbName = `classdb-teacher-${this._id}`;
2492
- const stuDbName = `classdb-student-${this._id}`;
2493
- this._db = new pouchdb_setup_default(
2494
- ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + dbName,
2495
- pouchDBincludeCredentialsConfig
2496
- );
2497
- this._stuDb = new pouchdb_setup_default(
2498
- ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + stuDbName,
2499
- pouchDBincludeCredentialsConfig
2500
- );
2586
+ async getOrCreateClassroomRegistrationsDoc() {
2587
+ let ret;
2501
2588
  try {
2502
- return this._db.get(CLASSROOM_CONFIG).then((cfg) => {
2503
- this._cfg = cfg;
2504
- this._initComplete = true;
2505
- }).then(() => {
2506
- return;
2507
- });
2589
+ ret = await this.remoteDB.get(
2590
+ _BaseUser.DOC_IDS.CLASSROOM_REGISTRATIONS
2591
+ );
2508
2592
  } catch (e) {
2509
- 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
+ }
2510
2616
  }
2511
- }
2512
- static async factory(classID) {
2513
- const ret = new _TeacherClassroomDB(classID);
2514
- await ret.init();
2617
+ logger.debug(`Returning classroom registrations doc: ${JSON.stringify(ret)}`);
2515
2618
  return ret;
2516
2619
  }
2517
- async removeContent(content) {
2518
- 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() {
2519
2632
  try {
2520
- const doc = await this._db.get(contentID);
2521
- await this._db.remove(doc);
2522
- void this._db.replicate.to(this._stuDb, {
2523
- doc_ids: [contentID]
2524
- });
2633
+ return (await this.getOrCreateClassroomRegistrationsDoc()).registrations.filter((c) => c.registeredAs === "student").map((c) => c.classID);
2525
2634
  } catch (error) {
2526
- 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 [];
2527
2640
  }
2528
2641
  }
2529
- async assignContent(content) {
2530
- let put;
2531
- const id = this.getContentId(content);
2532
- if (content.type === "tag") {
2533
- put = await this._db.put({
2534
- courseID: content.courseID,
2535
- tagID: content.tagID,
2536
- type: "tag",
2537
- _id: id,
2538
- assignedBy: content.assignedBy,
2539
- assignedOn: import_moment4.default.utc(),
2540
- activeOn: content.activeOn || import_moment4.default.utc()
2541
- });
2542
- } else {
2543
- put = await this._db.put({
2544
- courseID: content.courseID,
2545
- type: "course",
2546
- _id: id,
2547
- assignedBy: content.assignedBy,
2548
- assignedOn: import_moment4.default.utc(),
2549
- activeOn: content.activeOn || import_moment4.default.utc()
2550
- });
2551
- }
2552
- if (put.ok) {
2553
- void this._db.replicate.to(this._stuDb, {
2554
- doc_ids: [id]
2555
- });
2556
- return true;
2557
- } else {
2558
- return false;
2559
- }
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);
2560
2659
  }
2561
2660
  };
2562
- ClassroomLookupDB = () => new pouchdb_setup_default(ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + classroomLookupDBTitle, {
2563
- skip_setup: true
2564
- });
2661
+ userCoursesDoc = "CourseRegistrations";
2662
+ userClassroomsDoc = "ClassroomRegistrations";
2565
2663
  }
2566
2664
  });
2567
2665
 
2568
- // src/core/interfaces/contentSource.ts
2569
- function isReview(item) {
2570
- const ret = item.status === "review" || item.status === "failed-review" || "reviewID" in item;
2571
- return ret;
2572
- }
2573
- async function getStudySource(source, user) {
2574
- if (source.type === "classroom") {
2575
- return await StudentClassroomDB.factory(source.id, user);
2576
- } else {
2577
- 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();
2673
+ }
2674
+ });
2675
+
2676
+ // src/factory.ts
2677
+ function getDataLayer() {
2678
+ if (!dataLayerInstance) {
2679
+ throw new Error("Data layer not initialized. Call initializeDataLayer first.");
2578
2680
  }
2681
+ return dataLayerInstance;
2579
2682
  }
2580
- var init_contentSource = __esm({
2581
- "src/core/interfaces/contentSource.ts"() {
2683
+ var ENV, dataLayerInstance;
2684
+ var init_factory = __esm({
2685
+ "src/factory.ts"() {
2582
2686
  "use strict";
2583
- init_factory();
2584
- 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;
2585
2694
  }
2586
2695
  });
2587
2696
 
2588
2697
  // src/impl/couch/adminDB.ts
2589
2698
  var AdminDB;
2590
- var init_adminDB = __esm({
2699
+ var init_adminDB2 = __esm({
2591
2700
  "src/impl/couch/adminDB.ts"() {
2592
2701
  "use strict";
2593
2702
  init_pouchdb_setup();
2594
2703
  init_factory();
2595
2704
  init_couch();
2596
- init_classroomDB();
2705
+ init_classroomDB2();
2597
2706
  init_courseLookupDB();
2598
2707
  init_logger();
2599
2708
  AdminDB = class {
@@ -2607,7 +2716,7 @@ var init_adminDB = __esm({
2607
2716
  async getUsers() {
2608
2717
  return (await this.usersDB.allDocs({
2609
2718
  include_docs: true,
2610
- ...getStartAndEndKeys2("org.couchdb.user:")
2719
+ ...getStartAndEndKeys("org.couchdb.user:")
2611
2720
  })).rows.map((r) => r.doc);
2612
2721
  }
2613
2722
  async getCourses() {
@@ -2700,14 +2809,14 @@ var init_auth = __esm({
2700
2809
  });
2701
2810
 
2702
2811
  // src/impl/couch/CouchDBSyncStrategy.ts
2703
- var import_common8, log4, CouchDBSyncStrategy;
2812
+ var import_common9, log4, CouchDBSyncStrategy;
2704
2813
  var init_CouchDBSyncStrategy = __esm({
2705
2814
  "src/impl/couch/CouchDBSyncStrategy.ts"() {
2706
2815
  "use strict";
2707
2816
  init_factory();
2708
2817
  init_types_legacy();
2709
2818
  init_logger();
2710
- import_common8 = require("@vue-skuilder/common");
2819
+ import_common9 = require("@vue-skuilder/common");
2711
2820
  init_common();
2712
2821
  init_pouchdb_setup();
2713
2822
  init_couch();
@@ -2725,6 +2834,13 @@ var init_CouchDBSyncStrategy = __esm({
2725
2834
  return this.getUserDB(username);
2726
2835
  }
2727
2836
  }
2837
+ getWriteDB(username) {
2838
+ if (username === GuestUsername || username.startsWith(GuestUsername)) {
2839
+ return getLocalUserDB(username);
2840
+ } else {
2841
+ return this.getUserDB(username);
2842
+ }
2843
+ }
2728
2844
  startSync(localDB, remoteDB) {
2729
2845
  if (localDB !== remoteDB) {
2730
2846
  this.syncHandle = pouchdb_setup_default.sync(localDB, remoteDB, {
@@ -2759,32 +2875,32 @@ var init_CouchDBSyncStrategy = __esm({
2759
2875
  log4(`CREATEACCOUNT: logged in as new user: ${loginResult.ok}`);
2760
2876
  if (loginResult.ok) {
2761
2877
  return {
2762
- status: import_common8.Status.ok,
2878
+ status: import_common9.Status.ok,
2763
2879
  error: void 0
2764
2880
  };
2765
2881
  } else {
2766
2882
  return {
2767
- status: import_common8.Status.error,
2883
+ status: import_common9.Status.error,
2768
2884
  error: "Failed to log in after account creation"
2769
2885
  };
2770
2886
  }
2771
2887
  } else {
2772
2888
  logger.warn(`Signup not OK: ${JSON.stringify(signupRequest)}`);
2773
2889
  return {
2774
- status: import_common8.Status.error,
2890
+ status: import_common9.Status.error,
2775
2891
  error: "Account creation failed"
2776
2892
  };
2777
2893
  }
2778
2894
  } catch (e) {
2779
2895
  if (e.reason === "Document update conflict.") {
2780
2896
  return {
2781
- status: import_common8.Status.error,
2897
+ status: import_common9.Status.error,
2782
2898
  error: "This username is taken!"
2783
2899
  };
2784
2900
  }
2785
2901
  logger.error(`Error on signup: ${JSON.stringify(e)}`);
2786
2902
  return {
2787
- status: import_common8.Status.error,
2903
+ status: import_common9.Status.error,
2788
2904
  error: e.message || "Unknown error during account creation"
2789
2905
  };
2790
2906
  }
@@ -2878,15 +2994,14 @@ __export(couch_exports, {
2878
2994
  CouchDBSyncStrategy: () => CouchDBSyncStrategy,
2879
2995
  CourseDB: () => CourseDB,
2880
2996
  CoursesDB: () => CoursesDB,
2881
- REVIEW_PREFIX: () => REVIEW_PREFIX2,
2882
- REVIEW_TIME_FORMAT: () => REVIEW_TIME_FORMAT2,
2997
+ REVIEW_TIME_FORMAT: () => REVIEW_TIME_FORMAT,
2883
2998
  StudentClassroomDB: () => StudentClassroomDB,
2884
2999
  TeacherClassroomDB: () => TeacherClassroomDB,
2885
3000
  addNote55: () => addNote55,
2886
3001
  addTagToCard: () => addTagToCard,
2887
3002
  createTag: () => createTag,
2888
3003
  deleteTag: () => deleteTag,
2889
- filterAllDocsByPrefix: () => filterAllDocsByPrefix2,
3004
+ filterAllDocsByPrefix: () => filterAllDocsByPrefix,
2890
3005
  getAncestorTagIDs: () => getAncestorTagIDs,
2891
3006
  getAppliedTags: () => getAppliedTags,
2892
3007
  getChildTagStubs: () => getChildTagStubs,
@@ -2903,7 +3018,7 @@ __export(couch_exports, {
2903
3018
  getCredentialledDataShapes: () => getCredentialledDataShapes,
2904
3019
  getLatestVersion: () => getLatestVersion,
2905
3020
  getRandomCards: () => getRandomCards,
2906
- getStartAndEndKeys: () => getStartAndEndKeys2,
3021
+ getStartAndEndKeys: () => getStartAndEndKeys,
2907
3022
  getStudySource: () => getStudySource,
2908
3023
  getTag: () => getTag,
2909
3024
  getTagID: () => getTagID,
@@ -3031,16 +3146,16 @@ function scheduleCardReview(review) {
3031
3146
  const now = import_moment5.default.utc();
3032
3147
  logger.info(`Scheduling for review in: ${review.time.diff(now, "h") / 24} days`);
3033
3148
  void getCouchUserDB(review.user).put({
3034
- _id: REVIEW_PREFIX2 + review.time.format(REVIEW_TIME_FORMAT2),
3149
+ _id: DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */] + review.time.format(REVIEW_TIME_FORMAT),
3035
3150
  cardId: review.card_id,
3036
- reviewTime: review.time,
3151
+ reviewTime: review.time.toISOString(),
3037
3152
  courseId: review.course_id,
3038
- scheduledAt: now,
3153
+ scheduledAt: now.toISOString(),
3039
3154
  scheduledFor: review.scheduledFor,
3040
3155
  schedulingAgentId: review.schedulingAgentId
3041
3156
  });
3042
3157
  }
3043
- function filterAllDocsByPrefix2(db, prefix, opts) {
3158
+ function filterAllDocsByPrefix(db, prefix, opts) {
3044
3159
  const options = {
3045
3160
  startkey: prefix,
3046
3161
  endkey: prefix + "\uFFF0",
@@ -3051,13 +3166,13 @@ function filterAllDocsByPrefix2(db, prefix, opts) {
3051
3166
  }
3052
3167
  return db.allDocs(options);
3053
3168
  }
3054
- function getStartAndEndKeys2(key) {
3169
+ function getStartAndEndKeys(key) {
3055
3170
  return {
3056
3171
  startkey: key,
3057
3172
  endkey: key + "\uFFF0"
3058
3173
  };
3059
3174
  }
3060
- 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;
3061
3176
  var init_couch = __esm({
3062
3177
  "src/impl/couch/index.ts"() {
3063
3178
  init_factory();
@@ -3067,8 +3182,8 @@ var init_couch = __esm({
3067
3182
  init_pouchdb_setup();
3068
3183
  import_process = __toESM(require("process"));
3069
3184
  init_contentSource();
3070
- init_adminDB();
3071
- init_classroomDB();
3185
+ init_adminDB2();
3186
+ init_classroomDB2();
3072
3187
  init_courseAPI();
3073
3188
  init_courseDB();
3074
3189
  init_CouchDBSyncStrategy();
@@ -3085,8 +3200,7 @@ var init_couch = __esm({
3085
3200
  return pouchdb_setup_default.fetch(url, opts);
3086
3201
  }
3087
3202
  };
3088
- REVIEW_PREFIX2 = "card_review_";
3089
- REVIEW_TIME_FORMAT2 = "YYYY-MM-DD--kk:mm:ss-SSS";
3203
+ REVIEW_TIME_FORMAT = "YYYY-MM-DD--kk:mm:ss-SSS";
3090
3204
  }
3091
3205
  });
3092
3206
  init_couch();
@@ -3098,7 +3212,6 @@ init_couch();
3098
3212
  CouchDBSyncStrategy,
3099
3213
  CourseDB,
3100
3214
  CoursesDB,
3101
- REVIEW_PREFIX,
3102
3215
  REVIEW_TIME_FORMAT,
3103
3216
  StudentClassroomDB,
3104
3217
  TeacherClassroomDB,