@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
@@ -20,6 +20,20 @@ var init_SyncStrategy = __esm({
20
20
  }
21
21
  });
22
22
 
23
+ // src/core/interfaces/adminDB.ts
24
+ var init_adminDB = __esm({
25
+ "src/core/interfaces/adminDB.ts"() {
26
+ "use strict";
27
+ }
28
+ });
29
+
30
+ // src/core/interfaces/classroomDB.ts
31
+ var init_classroomDB = __esm({
32
+ "src/core/interfaces/classroomDB.ts"() {
33
+ "use strict";
34
+ }
35
+ });
36
+
23
37
  // src/util/logger.ts
24
38
  var isDevelopment, logger;
25
39
  var init_logger = __esm({
@@ -65,31 +79,6 @@ var init_logger = __esm({
65
79
  }
66
80
  });
67
81
 
68
- // src/core/types/types-legacy.ts
69
- var GuestUsername, log, cardHistoryPrefix;
70
- var init_types_legacy = __esm({
71
- "src/core/types/types-legacy.ts"() {
72
- "use strict";
73
- init_logger();
74
- GuestUsername = "Guest";
75
- log = (message) => {
76
- logger.log(message);
77
- };
78
- cardHistoryPrefix = "cardH";
79
- }
80
- });
81
-
82
- // src/core/util/index.ts
83
- function getCardHistoryID(courseID, cardID) {
84
- return `${cardHistoryPrefix}-${courseID}-${cardID}`;
85
- }
86
- var init_util = __esm({
87
- "src/core/util/index.ts"() {
88
- "use strict";
89
- init_types_legacy();
90
- }
91
- });
92
-
93
82
  // src/impl/couch/pouchdb-setup.ts
94
83
  import PouchDB from "pouchdb";
95
84
  import PouchDBFind from "pouchdb-find";
@@ -109,122 +98,6 @@ var init_pouchdb_setup = __esm({
109
98
  }
110
99
  });
111
100
 
112
- // src/util/tuiLogger.ts
113
- var init_tuiLogger = __esm({
114
- "src/util/tuiLogger.ts"() {
115
- "use strict";
116
- init_dataDirectory();
117
- }
118
- });
119
-
120
- // src/util/dataDirectory.ts
121
- import * as path from "path";
122
- import * as os from "os";
123
- function getAppDataDirectory() {
124
- return path.join(os.homedir(), ".tuilder");
125
- }
126
- function getDbPath(dbName) {
127
- return path.join(getAppDataDirectory(), dbName);
128
- }
129
- var init_dataDirectory = __esm({
130
- "src/util/dataDirectory.ts"() {
131
- "use strict";
132
- init_tuiLogger();
133
- }
134
- });
135
-
136
- // src/impl/common/userDBHelpers.ts
137
- import moment from "moment";
138
- function hexEncode(str) {
139
- let hex;
140
- let returnStr = "";
141
- for (let i = 0; i < str.length; i++) {
142
- hex = str.charCodeAt(i).toString(16);
143
- returnStr += ("000" + hex).slice(3);
144
- }
145
- return returnStr;
146
- }
147
- function filterAllDocsByPrefix(db, prefix, opts) {
148
- const options = {
149
- startkey: prefix,
150
- endkey: prefix + "\uFFF0",
151
- include_docs: true
152
- };
153
- if (opts) {
154
- Object.assign(options, opts);
155
- }
156
- return db.allDocs(options);
157
- }
158
- function getStartAndEndKeys(key) {
159
- return {
160
- startkey: key,
161
- endkey: key + "\uFFF0"
162
- };
163
- }
164
- function updateGuestAccountExpirationDate(guestDB) {
165
- const currentTime = moment.utc();
166
- const expirationDate = currentTime.add(2, "months").toISOString();
167
- const expiryDocID2 = "GuestAccountExpirationDate";
168
- void guestDB.get(expiryDocID2).then((doc) => {
169
- return guestDB.put({
170
- _id: expiryDocID2,
171
- _rev: doc._rev,
172
- date: expirationDate
173
- });
174
- }).catch(() => {
175
- return guestDB.put({
176
- _id: expiryDocID2,
177
- date: expirationDate
178
- });
179
- });
180
- }
181
- function getLocalUserDB(username) {
182
- const dbName = `userdb-${username}`;
183
- if (typeof window === "undefined") {
184
- return new pouchdb_setup_default(getDbPath(dbName), {});
185
- } else {
186
- return new pouchdb_setup_default(dbName, {});
187
- }
188
- }
189
- function scheduleCardReviewLocal(userDB, review) {
190
- const now = moment.utc();
191
- logger.info(`Scheduling for review in: ${review.time.diff(now, "h") / 24} days`);
192
- void userDB.put({
193
- _id: REVIEW_PREFIX + review.time.format(REVIEW_TIME_FORMAT),
194
- cardId: review.card_id,
195
- reviewTime: review.time,
196
- courseId: review.course_id,
197
- scheduledAt: now,
198
- scheduledFor: review.scheduledFor,
199
- schedulingAgentId: review.schedulingAgentId
200
- });
201
- }
202
- async function removeScheduledCardReviewLocal(userDB, reviewDocID) {
203
- const reviewDoc = await userDB.get(reviewDocID);
204
- userDB.remove(reviewDoc).then((res) => {
205
- if (res.ok) {
206
- log2(`Removed Review Doc: ${reviewDocID}`);
207
- }
208
- }).catch((err) => {
209
- log2(`Failed to remove Review Doc: ${reviewDocID},
210
- ${JSON.stringify(err)}`);
211
- });
212
- }
213
- var REVIEW_PREFIX, REVIEW_TIME_FORMAT, log2;
214
- var init_userDBHelpers = __esm({
215
- "src/impl/common/userDBHelpers.ts"() {
216
- "use strict";
217
- init_logger();
218
- init_pouchdb_setup();
219
- init_dataDirectory();
220
- REVIEW_PREFIX = "card_review_";
221
- REVIEW_TIME_FORMAT = "YYYY-MM-DD--kk:mm:ss-SSS";
222
- log2 = (s) => {
223
- logger.info(s);
224
- };
225
- }
226
- });
227
-
228
101
  // src/util/Loggable.ts
229
102
  var Loggable;
230
103
  var init_Loggable = __esm({
@@ -252,7 +125,10 @@ var init_updateQueue = __esm({
252
125
  _className = "UpdateQueue";
253
126
  pendingUpdates = {};
254
127
  inprogressUpdates = {};
255
- db;
128
+ readDB;
129
+ // Database for read operations
130
+ writeDB;
131
+ // Database for write operations (local-first)
256
132
  update(id, update) {
257
133
  logger.debug(`Update requested on doc: ${id}`);
258
134
  if (this.pendingUpdates[id]) {
@@ -262,24 +138,25 @@ var init_updateQueue = __esm({
262
138
  }
263
139
  return this.applyUpdates(id);
264
140
  }
265
- constructor(db) {
141
+ constructor(readDB, writeDB) {
266
142
  super();
267
- this.db = db;
143
+ this.readDB = readDB;
144
+ this.writeDB = writeDB || readDB;
268
145
  logger.debug(`UpdateQ initialized...`);
269
- void this.db.info().then((i) => {
146
+ void this.readDB.info().then((i) => {
270
147
  logger.debug(`db info: ${JSON.stringify(i)}`);
271
148
  });
272
149
  }
273
150
  async applyUpdates(id) {
274
151
  logger.debug(`Applying updates on doc: ${id}`);
275
152
  if (this.inprogressUpdates[id]) {
276
- await this.db.info();
153
+ await this.readDB.info();
277
154
  return this.applyUpdates(id);
278
155
  } else {
279
156
  if (this.pendingUpdates[id] && this.pendingUpdates[id].length > 0) {
280
157
  this.inprogressUpdates[id] = true;
281
158
  try {
282
- let doc = await this.db.get(id);
159
+ let doc = await this.readDB.get(id);
283
160
  logger.debug(`Retrieved doc: ${id}`);
284
161
  while (this.pendingUpdates[id].length !== 0) {
285
162
  const update = this.pendingUpdates[id].splice(0, 1)[0];
@@ -292,7 +169,7 @@ var init_updateQueue = __esm({
292
169
  };
293
170
  }
294
171
  }
295
- await this.db.put(doc);
172
+ await this.writeDB.put(doc);
296
173
  logger.debug(`Put doc: ${id}`);
297
174
  if (this.pendingUpdates[id].length === 0) {
298
175
  this.inprogressUpdates[id] = false;
@@ -318,6 +195,32 @@ var init_updateQueue = __esm({
318
195
  }
319
196
  });
320
197
 
198
+ // src/core/types/types-legacy.ts
199
+ var GuestUsername, log, DocTypePrefixes;
200
+ var init_types_legacy = __esm({
201
+ "src/core/types/types-legacy.ts"() {
202
+ "use strict";
203
+ init_logger();
204
+ GuestUsername = "Guest";
205
+ log = (message) => {
206
+ logger.log(message);
207
+ };
208
+ DocTypePrefixes = {
209
+ ["CARD" /* CARD */]: "c",
210
+ ["DISPLAYABLE_DATA" /* DISPLAYABLE_DATA */]: "dd",
211
+ ["TAG" /* TAG */]: "TAG",
212
+ ["CARDRECORD" /* CARDRECORD */]: "cardH",
213
+ ["SCHEDULED_CARD" /* SCHEDULED_CARD */]: "card_review_",
214
+ // Add other doctypes here as they get prefixed IDs
215
+ ["DATASHAPE" /* DATASHAPE */]: "DATASHAPE",
216
+ ["QUESTION" /* QUESTIONTYPE */]: "QUESTION",
217
+ ["VIEW" /* VIEW */]: "VIEW",
218
+ ["PEDAGOGY" /* PEDAGOGY */]: "PEDAGOGY",
219
+ ["NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */]: "NAVIGATION_STRATEGY"
220
+ };
221
+ }
222
+ });
223
+
321
224
  // src/impl/couch/clientCache.ts
322
225
  async function GET_CACHED(k, f) {
323
226
  if (CLIENT_CACHE[k]) {
@@ -341,10 +244,12 @@ var init_clientCache = __esm({
341
244
  import { NameSpacer } from "@vue-skuilder/common";
342
245
  import { blankCourseElo, toCourseElo } from "@vue-skuilder/common";
343
246
  import { prepareNote55 } from "@vue-skuilder/common";
247
+ import { v4 as uuidv4 } from "uuid";
344
248
  async function addNote55(courseID, codeCourse, shape, data, author, tags, uploads, elo = blankCourseElo()) {
345
249
  const db = getCourseDB(courseID);
346
250
  const payload = prepareNote55(courseID, codeCourse, shape, data, author, tags, uploads);
347
- const result = await db.post(payload);
251
+ const _id = `${DocTypePrefixes["DISPLAYABLE_DATA" /* DISPLAYABLE_DATA */]}-${uuidv4()}`;
252
+ const result = await db.put({ ...payload, _id });
348
253
  const dataShapeId = NameSpacer.getDataShapeString({
349
254
  course: codeCourse,
350
255
  dataShape: shape.name
@@ -415,7 +320,9 @@ async function createCard(questionViewName, courseID, dsDescriptor, noteID, tags
415
320
  }
416
321
  async function addCard(courseID, course, id_displayable_data, id_view, elo, tags, author) {
417
322
  const db = getCourseDB(courseID);
418
- const card = await db.post({
323
+ const _id = `${DocTypePrefixes["CARD" /* CARD */]}-${uuidv4()}`;
324
+ const card = await db.put({
325
+ _id,
419
326
  course,
420
327
  id_displayable_data,
421
328
  id_view,
@@ -794,7 +701,7 @@ async function getCourseQuestionTypes(courseID) {
794
701
  }
795
702
  async function getCourseTagStubs(courseID) {
796
703
  logger.debug(`Getting tag stubs for course: ${courseID}`);
797
- const stubs = await filterAllDocsByPrefix2(
704
+ const stubs = await filterAllDocsByPrefix(
798
705
  getCourseDB2(courseID),
799
706
  "TAG" /* TAG */.valueOf() + "-"
800
707
  );
@@ -859,7 +766,7 @@ function getAncestorTagIDs(courseID, tagID) {
859
766
  }
860
767
  }
861
768
  async function getChildTagStubs(courseID, tagID) {
862
- return await filterAllDocsByPrefix2(getCourseDB2(courseID), tagID + ">");
769
+ return await filterAllDocsByPrefix(getCourseDB2(courseID), tagID + ">");
863
770
  }
864
771
  async function getAppliedTags(id_course, id_card) {
865
772
  const db = getCourseDB2(id_course);
@@ -1365,1217 +1272,1418 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
1365
1272
  }
1366
1273
  });
1367
1274
 
1368
- // src/impl/couch/user-course-relDB.ts
1369
- import moment2 from "moment";
1370
- var UsrCrsData;
1371
- var init_user_course_relDB = __esm({
1372
- "src/impl/couch/user-course-relDB.ts"() {
1275
+ // src/impl/couch/classroomDB.ts
1276
+ import moment from "moment";
1277
+ function getClassroomDB(classID, version) {
1278
+ const dbName = `classdb-${version}-${classID}`;
1279
+ logger.info(`Retrieving classroom db: ${dbName}`);
1280
+ return new pouchdb_setup_default(
1281
+ ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + dbName,
1282
+ pouchDBincludeCredentialsConfig
1283
+ );
1284
+ }
1285
+ async function getClassroomConfig(classID) {
1286
+ return await getClassroomDB(classID, "student").get(CLASSROOM_CONFIG);
1287
+ }
1288
+ var classroomLookupDBTitle, CLASSROOM_CONFIG, ClassroomDBBase, StudentClassroomDB, TeacherClassroomDB, ClassroomLookupDB;
1289
+ var init_classroomDB2 = __esm({
1290
+ "src/impl/couch/classroomDB.ts"() {
1373
1291
  "use strict";
1292
+ init_factory();
1293
+ init_logger();
1294
+ init_pouchdb_setup();
1374
1295
  init_couch();
1375
1296
  init_courseDB();
1376
- init_logger();
1377
- UsrCrsData = class {
1378
- user;
1379
- course;
1380
- _courseId;
1381
- constructor(user, courseId) {
1382
- this.user = user;
1383
- this.course = new CourseDB(courseId, async () => this.user);
1384
- this._courseId = courseId;
1385
- }
1386
- async getReviewsForcast(daysCount) {
1387
- const time = moment2.utc().add(daysCount, "days");
1388
- return this.getReviewstoDate(time);
1389
- }
1390
- async getPendingReviews() {
1391
- const now = moment2.utc();
1392
- return this.getReviewstoDate(now);
1297
+ classroomLookupDBTitle = "classdb-lookup";
1298
+ CLASSROOM_CONFIG = "ClassroomConfig";
1299
+ ClassroomDBBase = class {
1300
+ _id;
1301
+ _db;
1302
+ _cfg;
1303
+ _initComplete = false;
1304
+ _content_prefix = "content";
1305
+ get _content_searchkeys() {
1306
+ return getStartAndEndKeys(this._content_prefix);
1393
1307
  }
1394
- async getScheduledReviewCount() {
1395
- return (await this.getPendingReviews()).length;
1308
+ async getAssignedContent() {
1309
+ logger.info(`Getting assigned content...`);
1310
+ const docRows = await this._db.allDocs({
1311
+ startkey: this._content_prefix,
1312
+ endkey: this._content_prefix + `\uFFF0`,
1313
+ include_docs: true
1314
+ });
1315
+ const ret = docRows.rows.map((row) => {
1316
+ return row.doc;
1317
+ });
1318
+ return ret;
1396
1319
  }
1397
- async getCourseSettings() {
1398
- const regDoc = await this.user.getCourseRegistrationsDoc();
1399
- const crsDoc = regDoc.courses.find((c) => c.courseID === this._courseId);
1400
- if (crsDoc && crsDoc.settings) {
1401
- return crsDoc.settings;
1320
+ getContentId(content) {
1321
+ if (content.type === "tag") {
1322
+ return `${this._content_prefix}-${content.courseID}-${content.tagID}`;
1402
1323
  } else {
1403
- logger.warn(`no settings found during lookup on course ${this._courseId}`);
1404
- return {};
1324
+ return `${this._content_prefix}-${content.courseID}`;
1405
1325
  }
1406
1326
  }
1407
- updateCourseSettings(updates) {
1408
- void this.user.updateCourseSettings(this._courseId, updates);
1327
+ get ready() {
1328
+ return this._initComplete;
1409
1329
  }
1410
- async getReviewstoDate(targetDate) {
1411
- const keys = getStartAndEndKeys2(REVIEW_PREFIX2);
1412
- const reviews = await this.user.remote().allDocs({
1413
- startkey: keys.startkey,
1414
- endkey: keys.endkey,
1415
- include_docs: true
1416
- });
1417
- logger.debug(
1418
- `Fetching ${this.user.getUsername()}'s scheduled reviews for course ${this._courseId}.`
1419
- );
1420
- return reviews.rows.filter((r) => {
1421
- if (r.id.startsWith(REVIEW_PREFIX2)) {
1422
- const date = moment2.utc(r.id.substr(REVIEW_PREFIX2.length), REVIEW_TIME_FORMAT2);
1423
- if (targetDate.isAfter(date)) {
1424
- if (this._courseId === void 0 || r.doc.courseId === this._courseId) {
1425
- return true;
1426
- }
1427
- }
1428
- }
1429
- }).map((r) => r.doc);
1330
+ getConfig() {
1331
+ return this._cfg;
1430
1332
  }
1431
1333
  };
1432
- }
1433
- });
1434
-
1435
- // src/impl/common/BaseUserDB.ts
1436
- import { Status as Status2 } from "@vue-skuilder/common";
1437
- import moment3 from "moment";
1438
- async function getOrCreateClassroomRegistrationsDoc(user) {
1439
- let ret;
1440
- try {
1441
- ret = await getLocalUserDB(user).get(userClassroomsDoc);
1442
- } catch (e) {
1443
- const err = e;
1444
- if (err.status === 404) {
1445
- await getLocalUserDB(user).put({
1446
- _id: userClassroomsDoc,
1447
- registrations: []
1448
- });
1449
- ret = await getOrCreateClassroomRegistrationsDoc(user);
1450
- } else {
1451
- const errorDetails = {
1452
- name: err.name,
1453
- status: err.status,
1454
- message: err.message,
1455
- reason: err.reason,
1456
- error: err.error
1457
- };
1458
- logger.error(
1459
- "Database error in getOrCreateClassroomRegistrationsDoc (standalone function):",
1460
- errorDetails
1461
- );
1462
- throw new Error(
1463
- `Database error accessing classroom registrations: ${err.message || err.name || "Unknown error"} (status: ${err.status})`
1464
- );
1465
- }
1466
- }
1467
- return ret;
1468
- }
1469
- async function getOrCreateCourseRegistrationsDoc(user) {
1470
- let ret;
1471
- try {
1472
- ret = await getLocalUserDB(user).get(userCoursesDoc);
1473
- } catch (e) {
1474
- const err = e;
1475
- if (err.status === 404) {
1476
- await getLocalUserDB(user).put({
1477
- _id: userCoursesDoc,
1478
- courses: [],
1479
- studyWeight: {}
1480
- });
1481
- ret = await getOrCreateCourseRegistrationsDoc(user);
1482
- } else {
1483
- throw new Error(
1484
- `Unexpected error ${JSON.stringify(e)} in getOrCreateCourseRegistrationDoc...`
1485
- );
1486
- }
1487
- }
1488
- return ret;
1489
- }
1490
- async function updateUserElo(user, course_id, elo) {
1491
- const regDoc = await getOrCreateCourseRegistrationsDoc(user);
1492
- const course = regDoc.courses.find((c) => c.courseID === course_id);
1493
- course.elo = elo;
1494
- return getLocalUserDB(user).put(regDoc);
1495
- }
1496
- async function registerUserForClassroom(user, classID, registerAs) {
1497
- log3(`Registering user: ${user} in course: ${classID}`);
1498
- return getOrCreateClassroomRegistrationsDoc(user).then((doc) => {
1499
- const regItem = {
1500
- classID,
1501
- registeredAs: registerAs
1502
- };
1503
- if (doc.registrations.filter((reg) => {
1504
- return reg.classID === regItem.classID && reg.registeredAs === regItem.registeredAs;
1505
- }).length === 0) {
1506
- doc.registrations.push(regItem);
1507
- } else {
1508
- log3(`User ${user} is already registered for class ${classID}`);
1509
- }
1510
- return getLocalUserDB(user).put(doc);
1511
- });
1512
- }
1513
- async function dropUserFromClassroom(user, classID) {
1514
- return getOrCreateClassroomRegistrationsDoc(user).then((doc) => {
1515
- let index = -1;
1516
- for (let i = 0; i < doc.registrations.length; i++) {
1517
- if (doc.registrations[i].classID === classID) {
1518
- index = i;
1519
- }
1520
- }
1521
- if (index !== -1) {
1522
- doc.registrations.splice(index, 1);
1523
- }
1524
- return getLocalUserDB(user).put(doc);
1525
- });
1526
- }
1527
- async function getUserClassrooms(user) {
1528
- return getOrCreateClassroomRegistrationsDoc(user);
1529
- }
1530
- var log3, cardHistoryPrefix2, BaseUser, userCoursesDoc, userClassroomsDoc;
1531
- var init_BaseUserDB = __esm({
1532
- "src/impl/common/BaseUserDB.ts"() {
1533
- "use strict";
1534
- init_util();
1535
- init_types_legacy();
1536
- init_logger();
1537
- init_userDBHelpers();
1538
- init_updateQueue();
1539
- init_user_course_relDB();
1540
- init_couch();
1541
- log3 = (s) => {
1542
- logger.info(s);
1543
- };
1544
- cardHistoryPrefix2 = "cardH-";
1545
- BaseUser = class _BaseUser {
1546
- static _instance;
1547
- static _initialized = false;
1548
- static Dummy(syncStrategy) {
1549
- return new _BaseUser("Me", syncStrategy);
1334
+ StudentClassroomDB = class _StudentClassroomDB extends ClassroomDBBase {
1335
+ // private readonly _prefix: string = 'content';
1336
+ userMessages;
1337
+ _user;
1338
+ constructor(classID, user) {
1339
+ super();
1340
+ this._id = classID;
1341
+ this._user = user;
1550
1342
  }
1551
- static DOC_IDS = {
1552
- CONFIG: "CONFIG",
1553
- COURSE_REGISTRATIONS: "CourseRegistrations",
1554
- CLASSROOM_REGISTRATIONS: "ClassroomRegistrations"
1555
- };
1556
- // private email: string;
1557
- _username;
1558
- syncStrategy;
1559
- getUsername() {
1560
- return this._username;
1343
+ async init() {
1344
+ const dbName = `classdb-student-${this._id}`;
1345
+ this._db = new pouchdb_setup_default(
1346
+ ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + dbName,
1347
+ pouchDBincludeCredentialsConfig
1348
+ );
1349
+ try {
1350
+ const cfg = await this._db.get(CLASSROOM_CONFIG);
1351
+ this._cfg = cfg;
1352
+ this.userMessages = this._db.changes({
1353
+ since: "now",
1354
+ live: true,
1355
+ include_docs: true
1356
+ });
1357
+ this._initComplete = true;
1358
+ return;
1359
+ } catch (e) {
1360
+ throw new Error(`Error in StudentClassroomDB constructor: ${JSON.stringify(e)}`);
1361
+ }
1561
1362
  }
1562
- isLoggedIn() {
1563
- return !this._username.startsWith(GuestUsername);
1363
+ static async factory(classID, user) {
1364
+ const ret = new _StudentClassroomDB(classID, user);
1365
+ await ret.init();
1366
+ return ret;
1564
1367
  }
1565
- remoteDB;
1566
- remote() {
1567
- return this.remoteDB;
1368
+ setChangeFcn(f) {
1369
+ void this.userMessages.on("change", f);
1568
1370
  }
1569
- localDB;
1570
- updateQueue;
1571
- async createAccount(username, password) {
1572
- if (!this.syncStrategy.canCreateAccount()) {
1573
- throw new Error("Account creation not supported by current sync strategy");
1574
- }
1575
- if (!this._username.startsWith(GuestUsername)) {
1576
- throw new Error(
1577
- `Cannot create a new account while logged in:
1578
- Currently logged-in as ${this._username}.`
1579
- );
1580
- }
1581
- const result = await this.syncStrategy.createAccount(username, password);
1582
- if (result.status === Status2.ok) {
1583
- log3(`Account created successfully, updating username to ${username}`);
1584
- this._username = username;
1585
- try {
1586
- localStorage.removeItem("dbUUID");
1587
- } catch (e) {
1588
- logger.warn("localStorage not available (Node.js environment):", e);
1589
- }
1590
- await this.init();
1591
- }
1592
- return {
1593
- status: result.status,
1594
- error: result.error || ""
1595
- };
1371
+ async getPendingReviews() {
1372
+ const u = this._user;
1373
+ return (await u.getPendingReviews()).filter((r) => r.scheduledFor === "classroom" && r.schedulingAgentId === this._id).map((r) => {
1374
+ return {
1375
+ ...r,
1376
+ qualifiedID: `${r.courseId}-${r.cardId}`,
1377
+ courseID: r.courseId,
1378
+ cardID: r.cardId,
1379
+ contentSourceType: "classroom",
1380
+ contentSourceID: this._id,
1381
+ reviewID: r._id,
1382
+ status: "review"
1383
+ };
1384
+ });
1596
1385
  }
1597
- async login(username, password) {
1598
- if (!this.syncStrategy.canAuthenticate()) {
1599
- throw new Error("Authentication not supported by current sync strategy");
1600
- }
1601
- if (!this._username.startsWith(GuestUsername) && this._username != username) {
1602
- if (this._username != username) {
1603
- throw new Error(`Cannot change accounts while logged in.
1604
- Log out of account ${this.getUsername()} before logging in as ${username}.`);
1386
+ async getNewCards() {
1387
+ const activeCards = await this._user.getActiveCards();
1388
+ const now = moment.utc();
1389
+ const assigned = await this.getAssignedContent();
1390
+ const due = assigned.filter((c) => now.isAfter(moment.utc(c.activeOn, REVIEW_TIME_FORMAT)));
1391
+ logger.info(`Due content: ${JSON.stringify(due)}`);
1392
+ let ret = [];
1393
+ for (let i = 0; i < due.length; i++) {
1394
+ const content = due[i];
1395
+ if (content.type === "course") {
1396
+ const db = new CourseDB(content.courseID, async () => this._user);
1397
+ ret = ret.concat(await db.getNewCards());
1398
+ } else if (content.type === "tag") {
1399
+ const tagDoc = await getTag(content.courseID, content.tagID);
1400
+ ret = ret.concat(
1401
+ tagDoc.taggedCards.map((c) => {
1402
+ return {
1403
+ courseID: content.courseID,
1404
+ cardID: c,
1405
+ qualifiedID: `${content.courseID}-${c}`,
1406
+ contentSourceType: "classroom",
1407
+ contentSourceID: this._id,
1408
+ status: "new"
1409
+ };
1410
+ })
1411
+ );
1412
+ } else if (content.type === "card") {
1413
+ ret.push(await getCourseDB2(content.courseID).get(content.cardID));
1605
1414
  }
1606
- logger.warn(`User ${this._username} is already logged in, but executing login again.`);
1607
1415
  }
1608
- const loginResult = await this.syncStrategy.authenticate(username, password);
1609
- if (loginResult.ok) {
1610
- log3(`Logged in as ${username}`);
1611
- this._username = username;
1612
- try {
1613
- localStorage.removeItem("dbUUID");
1614
- } catch (e) {
1615
- logger.warn("localStorage not available (Node.js environment):", e);
1416
+ logger.info(`New Cards from classroom ${this._cfg.name}: ${ret.map((c) => c.qualifiedID)}`);
1417
+ return ret.filter((c) => {
1418
+ if (activeCards.some((ac) => c.qualifiedID.includes(ac))) {
1419
+ return false;
1420
+ } else {
1421
+ return true;
1616
1422
  }
1617
- await this.init();
1618
- }
1619
- return loginResult;
1423
+ });
1620
1424
  }
1621
- async resetUserData() {
1622
- if (this.syncStrategy.canAuthenticate()) {
1623
- return {
1624
- status: Status2.error,
1625
- error: "Reset user data is only available for local-only mode. Use logout instead for remote sync."
1626
- };
1627
- }
1628
- try {
1629
- const localDB = getLocalUserDB(this._username);
1630
- const allDocs = await localDB.allDocs({ include_docs: false });
1631
- const docsToDelete = allDocs.rows.filter((row) => {
1632
- const id = row.id;
1633
- return id.startsWith(cardHistoryPrefix2) || // Card interaction history
1634
- id.startsWith(REVIEW_PREFIX) || // Scheduled reviews
1635
- id === _BaseUser.DOC_IDS.COURSE_REGISTRATIONS || // Course registrations
1636
- id === _BaseUser.DOC_IDS.CLASSROOM_REGISTRATIONS || // Classroom registrations
1637
- id === _BaseUser.DOC_IDS.CONFIG;
1638
- }).map((row) => ({ _id: row.id, _rev: row.value.rev, _deleted: true }));
1639
- if (docsToDelete.length > 0) {
1640
- await localDB.bulkDocs(docsToDelete);
1641
- }
1642
- await this.init();
1643
- return { status: Status2.ok };
1644
- } catch (error) {
1645
- logger.error("Failed to reset user data:", error);
1646
- return {
1647
- status: Status2.error,
1648
- error: error instanceof Error ? error.message : "Unknown error during reset"
1649
- };
1650
- }
1651
- }
1652
- async logout() {
1653
- if (!this.syncStrategy.canAuthenticate()) {
1654
- this._username = await this.syncStrategy.getCurrentUsername();
1655
- await this.init();
1656
- return { ok: true };
1657
- }
1658
- const ret = await this.syncStrategy.logout();
1659
- this._username = await this.syncStrategy.getCurrentUsername();
1660
- await this.init();
1661
- return ret;
1662
- }
1663
- update(id, update) {
1664
- return this.updateQueue.update(id, update);
1425
+ };
1426
+ TeacherClassroomDB = class _TeacherClassroomDB extends ClassroomDBBase {
1427
+ _stuDb;
1428
+ constructor(classID) {
1429
+ super();
1430
+ this._id = classID;
1665
1431
  }
1666
- async getCourseRegistrationsDoc() {
1667
- logger.debug(`Fetching courseRegistrations for ${this.getUsername()}`);
1668
- let ret;
1432
+ async init() {
1433
+ const dbName = `classdb-teacher-${this._id}`;
1434
+ const stuDbName = `classdb-student-${this._id}`;
1435
+ this._db = new pouchdb_setup_default(
1436
+ ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + dbName,
1437
+ pouchDBincludeCredentialsConfig
1438
+ );
1439
+ this._stuDb = new pouchdb_setup_default(
1440
+ ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + stuDbName,
1441
+ pouchDBincludeCredentialsConfig
1442
+ );
1669
1443
  try {
1670
- const regDoc = await this.localDB.get(
1671
- _BaseUser.DOC_IDS.COURSE_REGISTRATIONS
1672
- );
1673
- return regDoc;
1444
+ return this._db.get(CLASSROOM_CONFIG).then((cfg) => {
1445
+ this._cfg = cfg;
1446
+ this._initComplete = true;
1447
+ }).then(() => {
1448
+ return;
1449
+ });
1674
1450
  } catch (e) {
1675
- const err = e;
1676
- if (err.status === 404) {
1677
- await this.localDB.put({
1678
- _id: _BaseUser.DOC_IDS.COURSE_REGISTRATIONS,
1679
- courses: [],
1680
- studyWeight: {}
1681
- });
1682
- ret = await this.getCourseRegistrationsDoc();
1683
- } else {
1684
- throw new Error(
1685
- `Unexpected error ${JSON.stringify(e)} in getOrCreateCourseRegistrationDoc...`
1686
- );
1687
- }
1451
+ throw new Error(`Error in TeacherClassroomDB constructor: ${JSON.stringify(e)}`);
1688
1452
  }
1689
- return ret;
1690
1453
  }
1691
- async getActiveCourses() {
1692
- const reg = await this.getCourseRegistrationsDoc();
1693
- return reg.courses.filter((c) => {
1694
- return c.status === void 0 || c.status === "active";
1695
- });
1696
- }
1697
- /**
1698
- * Returns a promise of the card IDs that the user has
1699
- * a scheduled review for.
1700
- *
1701
- */
1702
- async getActiveCards() {
1703
- const keys = getStartAndEndKeys(REVIEW_PREFIX);
1704
- const reviews = await this.remoteDB.allDocs({
1705
- startkey: keys.startkey,
1706
- endkey: keys.endkey,
1707
- include_docs: true
1708
- });
1709
- return reviews.rows.map((r) => `${r.doc.courseId}-${r.doc.cardId}`);
1454
+ static async factory(classID) {
1455
+ const ret = new _TeacherClassroomDB(classID);
1456
+ await ret.init();
1457
+ return ret;
1710
1458
  }
1711
- async getActivityRecords() {
1459
+ async removeContent(content) {
1460
+ const contentID = this.getContentId(content);
1712
1461
  try {
1713
- const hist = await this.getHistory();
1714
- const allRecords = [];
1715
- if (!Array.isArray(hist)) {
1716
- logger.error("getHistory did not return an array:", hist);
1717
- return allRecords;
1718
- }
1719
- let sampleCount = 0;
1720
- for (let i = 0; i < hist.length; i++) {
1721
- try {
1722
- if (hist[i] && Array.isArray(hist[i].records)) {
1723
- hist[i].records.forEach((record) => {
1724
- try {
1725
- if (!record.timeStamp) {
1726
- return;
1727
- }
1728
- let timeStamp;
1729
- if (typeof record.timeStamp === "object") {
1730
- if (typeof record.timeStamp.toDate === "function") {
1731
- timeStamp = record.timeStamp.toISOString();
1732
- } else if (record.timeStamp instanceof Date) {
1733
- timeStamp = record.timeStamp.toISOString();
1734
- } else {
1735
- if (sampleCount < 3) {
1736
- logger.warn("Unknown timestamp object type:", record.timeStamp);
1737
- sampleCount++;
1738
- }
1739
- return;
1740
- }
1741
- } else if (typeof record.timeStamp === "string") {
1742
- const date = new Date(record.timeStamp);
1743
- if (isNaN(date.getTime())) {
1744
- return;
1745
- }
1746
- timeStamp = record.timeStamp;
1747
- } else if (typeof record.timeStamp === "number") {
1748
- timeStamp = new Date(record.timeStamp).toISOString();
1749
- } else {
1750
- return;
1751
- }
1752
- allRecords.push({
1753
- timeStamp,
1754
- courseID: record.courseID || "unknown",
1755
- cardID: record.cardID || "unknown",
1756
- timeSpent: record.timeSpent || 0,
1757
- type: "card_view"
1758
- });
1759
- } catch (err) {
1760
- }
1761
- });
1762
- }
1763
- } catch (err) {
1764
- logger.error("Error processing history item:", err);
1765
- }
1766
- }
1767
- logger.debug(`Found ${allRecords.length} activity records`);
1768
- return allRecords;
1769
- } catch (err) {
1770
- logger.error("Error in getActivityRecords:", err);
1771
- return [];
1462
+ const doc = await this._db.get(contentID);
1463
+ await this._db.remove(doc);
1464
+ void this._db.replicate.to(this._stuDb, {
1465
+ doc_ids: [contentID]
1466
+ });
1467
+ } catch (error) {
1468
+ logger.error("Failed to remove content:", contentID, error);
1772
1469
  }
1773
1470
  }
1774
- async getReviewstoDate(targetDate, course_id) {
1775
- const keys = getStartAndEndKeys(REVIEW_PREFIX);
1776
- const reviews = await this.remoteDB.allDocs({
1777
- startkey: keys.startkey,
1778
- endkey: keys.endkey,
1779
- include_docs: true
1780
- });
1781
- log3(
1782
- `Fetching ${this._username}'s scheduled reviews${course_id ? ` for course ${course_id}` : ""}.`
1783
- );
1784
- return reviews.rows.filter((r) => {
1785
- if (r.id.startsWith(REVIEW_PREFIX)) {
1786
- const date = moment3.utc(r.id.substr(REVIEW_PREFIX.length), REVIEW_TIME_FORMAT);
1787
- if (targetDate.isAfter(date)) {
1788
- if (course_id === void 0 || r.doc.courseId === course_id) {
1789
- return true;
1790
- }
1791
- }
1792
- }
1793
- }).map((r) => r.doc);
1471
+ async assignContent(content) {
1472
+ let put;
1473
+ const id = this.getContentId(content);
1474
+ if (content.type === "tag") {
1475
+ put = await this._db.put({
1476
+ courseID: content.courseID,
1477
+ tagID: content.tagID,
1478
+ type: "tag",
1479
+ _id: id,
1480
+ assignedBy: content.assignedBy,
1481
+ assignedOn: moment.utc(),
1482
+ activeOn: content.activeOn || moment.utc()
1483
+ });
1484
+ } else {
1485
+ put = await this._db.put({
1486
+ courseID: content.courseID,
1487
+ type: "course",
1488
+ _id: id,
1489
+ assignedBy: content.assignedBy,
1490
+ assignedOn: moment.utc(),
1491
+ activeOn: content.activeOn || moment.utc()
1492
+ });
1493
+ }
1494
+ if (put.ok) {
1495
+ void this._db.replicate.to(this._stuDb, {
1496
+ doc_ids: [id]
1497
+ });
1498
+ return true;
1499
+ } else {
1500
+ return false;
1501
+ }
1502
+ }
1503
+ };
1504
+ ClassroomLookupDB = () => new pouchdb_setup_default(ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + classroomLookupDBTitle, {
1505
+ skip_setup: true
1506
+ });
1507
+ }
1508
+ });
1509
+
1510
+ // src/core/interfaces/contentSource.ts
1511
+ function isReview(item) {
1512
+ const ret = item.status === "review" || item.status === "failed-review" || "reviewID" in item;
1513
+ return ret;
1514
+ }
1515
+ async function getStudySource(source, user) {
1516
+ if (source.type === "classroom") {
1517
+ return await StudentClassroomDB.factory(source.id, user);
1518
+ } else {
1519
+ return getDataLayer().getCourseDB(source.id);
1520
+ }
1521
+ }
1522
+ var init_contentSource = __esm({
1523
+ "src/core/interfaces/contentSource.ts"() {
1524
+ "use strict";
1525
+ init_factory();
1526
+ init_classroomDB2();
1527
+ }
1528
+ });
1529
+
1530
+ // src/core/interfaces/courseDB.ts
1531
+ var init_courseDB2 = __esm({
1532
+ "src/core/interfaces/courseDB.ts"() {
1533
+ "use strict";
1534
+ }
1535
+ });
1536
+
1537
+ // src/core/interfaces/dataLayerProvider.ts
1538
+ var init_dataLayerProvider = __esm({
1539
+ "src/core/interfaces/dataLayerProvider.ts"() {
1540
+ "use strict";
1541
+ }
1542
+ });
1543
+
1544
+ // src/core/interfaces/userDB.ts
1545
+ var init_userDB = __esm({
1546
+ "src/core/interfaces/userDB.ts"() {
1547
+ "use strict";
1548
+ }
1549
+ });
1550
+
1551
+ // src/core/interfaces/index.ts
1552
+ var init_interfaces = __esm({
1553
+ "src/core/interfaces/index.ts"() {
1554
+ "use strict";
1555
+ init_adminDB();
1556
+ init_classroomDB();
1557
+ init_contentSource();
1558
+ init_courseDB2();
1559
+ init_dataLayerProvider();
1560
+ init_userDB();
1561
+ }
1562
+ });
1563
+
1564
+ // src/core/types/user.ts
1565
+ var init_user = __esm({
1566
+ "src/core/types/user.ts"() {
1567
+ "use strict";
1568
+ }
1569
+ });
1570
+
1571
+ // src/core/util/index.ts
1572
+ function getCardHistoryID(courseID, cardID) {
1573
+ return `${DocTypePrefixes["CARDRECORD" /* CARDRECORD */]}-${courseID}-${cardID}`;
1574
+ }
1575
+ var init_util = __esm({
1576
+ "src/core/util/index.ts"() {
1577
+ "use strict";
1578
+ init_types_legacy();
1579
+ }
1580
+ });
1581
+
1582
+ // src/core/bulkImport/cardProcessor.ts
1583
+ import { Status as Status2 } from "@vue-skuilder/common";
1584
+ var init_cardProcessor = __esm({
1585
+ "src/core/bulkImport/cardProcessor.ts"() {
1586
+ "use strict";
1587
+ init_logger();
1588
+ }
1589
+ });
1590
+
1591
+ // src/core/bulkImport/types.ts
1592
+ var init_types = __esm({
1593
+ "src/core/bulkImport/types.ts"() {
1594
+ "use strict";
1595
+ }
1596
+ });
1597
+
1598
+ // src/core/bulkImport/index.ts
1599
+ var init_bulkImport = __esm({
1600
+ "src/core/bulkImport/index.ts"() {
1601
+ "use strict";
1602
+ init_cardProcessor();
1603
+ init_types();
1604
+ }
1605
+ });
1606
+
1607
+ // src/core/index.ts
1608
+ var init_core = __esm({
1609
+ "src/core/index.ts"() {
1610
+ "use strict";
1611
+ init_interfaces();
1612
+ init_types_legacy();
1613
+ init_user();
1614
+ init_Loggable();
1615
+ init_util();
1616
+ init_navigators();
1617
+ init_bulkImport();
1618
+ }
1619
+ });
1620
+
1621
+ // src/util/tuiLogger.ts
1622
+ var init_tuiLogger = __esm({
1623
+ "src/util/tuiLogger.ts"() {
1624
+ "use strict";
1625
+ init_dataDirectory();
1626
+ }
1627
+ });
1628
+
1629
+ // src/util/dataDirectory.ts
1630
+ import * as path from "path";
1631
+ import * as os from "os";
1632
+ function getAppDataDirectory() {
1633
+ return path.join(os.homedir(), ".tuilder");
1634
+ }
1635
+ function getDbPath(dbName) {
1636
+ return path.join(getAppDataDirectory(), dbName);
1637
+ }
1638
+ var init_dataDirectory = __esm({
1639
+ "src/util/dataDirectory.ts"() {
1640
+ "use strict";
1641
+ init_tuiLogger();
1642
+ }
1643
+ });
1644
+
1645
+ // src/impl/common/userDBHelpers.ts
1646
+ import moment2 from "moment";
1647
+ function hexEncode(str) {
1648
+ let hex;
1649
+ let returnStr = "";
1650
+ for (let i = 0; i < str.length; i++) {
1651
+ hex = str.charCodeAt(i).toString(16);
1652
+ returnStr += ("000" + hex).slice(3);
1653
+ }
1654
+ return returnStr;
1655
+ }
1656
+ function filterAllDocsByPrefix2(db, prefix, opts) {
1657
+ const options = {
1658
+ startkey: prefix,
1659
+ endkey: prefix + "\uFFF0",
1660
+ include_docs: true
1661
+ };
1662
+ if (opts) {
1663
+ Object.assign(options, opts);
1664
+ }
1665
+ return db.allDocs(options);
1666
+ }
1667
+ function getStartAndEndKeys2(key) {
1668
+ return {
1669
+ startkey: key,
1670
+ endkey: key + "\uFFF0"
1671
+ };
1672
+ }
1673
+ function updateGuestAccountExpirationDate(guestDB) {
1674
+ const currentTime = moment2.utc();
1675
+ const expirationDate = currentTime.add(2, "months").toISOString();
1676
+ const expiryDocID2 = "GuestAccountExpirationDate";
1677
+ void guestDB.get(expiryDocID2).then((doc) => {
1678
+ return guestDB.put({
1679
+ _id: expiryDocID2,
1680
+ _rev: doc._rev,
1681
+ date: expirationDate
1682
+ });
1683
+ }).catch(() => {
1684
+ return guestDB.put({
1685
+ _id: expiryDocID2,
1686
+ date: expirationDate
1687
+ });
1688
+ });
1689
+ }
1690
+ function getLocalUserDB(username) {
1691
+ const dbName = `userdb-${username}`;
1692
+ if (typeof window === "undefined") {
1693
+ return new pouchdb_setup_default(getDbPath(dbName), {});
1694
+ } else {
1695
+ return new pouchdb_setup_default(dbName, {});
1696
+ }
1697
+ }
1698
+ function scheduleCardReviewLocal(userDB, review) {
1699
+ const now = moment2.utc();
1700
+ logger.info(`Scheduling for review in: ${review.time.diff(now, "h") / 24} days`);
1701
+ void userDB.put({
1702
+ _id: DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */] + review.time.format(REVIEW_TIME_FORMAT2),
1703
+ cardId: review.card_id,
1704
+ reviewTime: review.time.toISOString(),
1705
+ courseId: review.course_id,
1706
+ scheduledAt: now.toISOString(),
1707
+ scheduledFor: review.scheduledFor,
1708
+ schedulingAgentId: review.schedulingAgentId
1709
+ });
1710
+ }
1711
+ async function removeScheduledCardReviewLocal(userDB, reviewDocID) {
1712
+ const reviewDoc = await userDB.get(reviewDocID);
1713
+ userDB.remove(reviewDoc).then((res) => {
1714
+ if (res.ok) {
1715
+ log2(`Removed Review Doc: ${reviewDocID}`);
1716
+ }
1717
+ }).catch((err) => {
1718
+ log2(`Failed to remove Review Doc: ${reviewDocID},
1719
+ ${JSON.stringify(err)}`);
1720
+ });
1721
+ }
1722
+ var REVIEW_TIME_FORMAT2, log2;
1723
+ var init_userDBHelpers = __esm({
1724
+ "src/impl/common/userDBHelpers.ts"() {
1725
+ "use strict";
1726
+ init_core();
1727
+ init_logger();
1728
+ init_pouchdb_setup();
1729
+ init_dataDirectory();
1730
+ REVIEW_TIME_FORMAT2 = "YYYY-MM-DD--kk:mm:ss-SSS";
1731
+ log2 = (s) => {
1732
+ logger.info(s);
1733
+ };
1734
+ }
1735
+ });
1736
+
1737
+ // src/impl/couch/user-course-relDB.ts
1738
+ import moment3 from "moment";
1739
+ var UsrCrsData;
1740
+ var init_user_course_relDB = __esm({
1741
+ "src/impl/couch/user-course-relDB.ts"() {
1742
+ "use strict";
1743
+ init_logger();
1744
+ UsrCrsData = class {
1745
+ user;
1746
+ _courseId;
1747
+ constructor(user, courseId) {
1748
+ this.user = user;
1749
+ this._courseId = courseId;
1794
1750
  }
1795
1751
  async getReviewsForcast(daysCount) {
1796
1752
  const time = moment3.utc().add(daysCount, "days");
1797
1753
  return this.getReviewstoDate(time);
1798
1754
  }
1799
- async getPendingReviews(course_id) {
1755
+ async getPendingReviews() {
1800
1756
  const now = moment3.utc();
1801
- return this.getReviewstoDate(now, course_id);
1802
- }
1803
- async getScheduledReviewCount(course_id) {
1804
- return (await this.getPendingReviews(course_id)).length;
1757
+ return this.getReviewstoDate(now);
1805
1758
  }
1806
- async getRegisteredCourses() {
1807
- const regDoc = await this.getCourseRegistrationsDoc();
1808
- return regDoc.courses.filter((c) => {
1809
- return !c.status || c.status === "active" || c.status === "maintenance-mode";
1810
- });
1759
+ async getScheduledReviewCount() {
1760
+ return (await this.getPendingReviews()).length;
1811
1761
  }
1812
- async getCourseRegDoc(courseID) {
1813
- const regDocs = await this.getCourseRegistrationsDoc();
1814
- const ret = regDocs.courses.find((c) => c.courseID === courseID);
1815
- if (ret) {
1816
- return ret;
1762
+ async getCourseSettings() {
1763
+ const regDoc = await this.user.getCourseRegistrationsDoc();
1764
+ const crsDoc = regDoc.courses.find((c) => c.courseID === this._courseId);
1765
+ if (crsDoc && crsDoc.settings) {
1766
+ return crsDoc.settings;
1817
1767
  } else {
1818
- throw new Error(`Course registration not found for course ID: ${courseID}`);
1768
+ logger.warn(`no settings found during lookup on course ${this._courseId}`);
1769
+ return {};
1819
1770
  }
1820
1771
  }
1821
- async registerForCourse(course_id, previewMode = false) {
1822
- return this.getCourseRegistrationsDoc().then((doc) => {
1823
- const status = previewMode ? "preview" : "active";
1824
- logger.debug(`Registering for ${course_id} with status: ${status}`);
1825
- const regItem = {
1826
- status,
1827
- courseID: course_id,
1828
- user: true,
1829
- admin: false,
1830
- moderator: false,
1831
- elo: {
1832
- global: {
1833
- score: 1e3,
1834
- count: 0
1835
- },
1836
- tags: {},
1837
- misc: {}
1838
- }
1839
- };
1840
- if (doc.courses.filter((course) => {
1841
- return course.courseID === regItem.courseID;
1842
- }).length === 0) {
1843
- log3(`It's a new course registration!`);
1844
- doc.courses.push(regItem);
1845
- doc.studyWeight[course_id] = 1;
1846
- } else {
1847
- doc.courses.forEach((c) => {
1848
- log3(`Found the previously registered course!`);
1849
- if (c.courseID === course_id) {
1850
- c.status = status;
1851
- }
1852
- });
1853
- }
1854
- return this.localDB.put(doc);
1855
- }).catch((e) => {
1856
- log3(`Registration failed because of: ${JSON.stringify(e)}`);
1857
- throw e;
1772
+ updateCourseSettings(updates) {
1773
+ if ("updateCourseSettings" in this.user) {
1774
+ void this.user.updateCourseSettings(this._courseId, updates);
1775
+ }
1776
+ }
1777
+ async getReviewstoDate(targetDate) {
1778
+ const allReviews = await this.user.getPendingReviews(this._courseId);
1779
+ logger.debug(
1780
+ `Fetching ${this.user.getUsername()}'s scheduled reviews for course ${this._courseId}.`
1781
+ );
1782
+ return allReviews.filter((review) => {
1783
+ const reviewTime = moment3.utc(review.reviewTime);
1784
+ return targetDate.isAfter(reviewTime);
1858
1785
  });
1859
1786
  }
1860
- async dropCourse(course_id, dropStatus = "dropped") {
1861
- return this.getCourseRegistrationsDoc().then((doc) => {
1862
- let index = -1;
1863
- for (let i = 0; i < doc.courses.length; i++) {
1864
- if (doc.courses[i].courseID === course_id) {
1865
- index = i;
1866
- }
1867
- }
1868
- if (index !== -1) {
1869
- delete doc.studyWeight[course_id];
1870
- doc.courses[index].status = dropStatus;
1871
- } else {
1872
- throw new Error(
1873
- `User ${this.getUsername()} is not currently registered for course ${course_id}`
1874
- );
1875
- }
1876
- return this.localDB.put(doc);
1877
- });
1787
+ };
1788
+ }
1789
+ });
1790
+
1791
+ // src/impl/common/BaseUserDB.ts
1792
+ import { Status as Status3 } from "@vue-skuilder/common";
1793
+ import moment4 from "moment";
1794
+ async function getOrCreateClassroomRegistrationsDoc(user) {
1795
+ let ret;
1796
+ try {
1797
+ ret = await getLocalUserDB(user).get(userClassroomsDoc);
1798
+ } catch (e) {
1799
+ const err = e;
1800
+ if (err.status === 404) {
1801
+ await getLocalUserDB(user).put({
1802
+ _id: userClassroomsDoc,
1803
+ registrations: []
1804
+ });
1805
+ ret = await getOrCreateClassroomRegistrationsDoc(user);
1806
+ } else {
1807
+ const errorDetails = {
1808
+ name: err.name,
1809
+ status: err.status,
1810
+ message: err.message,
1811
+ reason: err.reason,
1812
+ error: err.error
1813
+ };
1814
+ logger.error(
1815
+ "Database error in getOrCreateClassroomRegistrationsDoc (standalone function):",
1816
+ errorDetails
1817
+ );
1818
+ throw new Error(
1819
+ `Database error accessing classroom registrations: ${err.message || err.name || "Unknown error"} (status: ${err.status})`
1820
+ );
1821
+ }
1822
+ }
1823
+ return ret;
1824
+ }
1825
+ async function getOrCreateCourseRegistrationsDoc(user) {
1826
+ let ret;
1827
+ try {
1828
+ ret = await getLocalUserDB(user).get(userCoursesDoc);
1829
+ } catch (e) {
1830
+ const err = e;
1831
+ if (err.status === 404) {
1832
+ await getLocalUserDB(user).put({
1833
+ _id: userCoursesDoc,
1834
+ courses: [],
1835
+ studyWeight: {}
1836
+ });
1837
+ ret = await getOrCreateCourseRegistrationsDoc(user);
1838
+ } else {
1839
+ throw new Error(
1840
+ `Unexpected error ${JSON.stringify(e)} in getOrCreateCourseRegistrationDoc...`
1841
+ );
1842
+ }
1843
+ }
1844
+ return ret;
1845
+ }
1846
+ async function updateUserElo(user, course_id, elo) {
1847
+ const regDoc = await getOrCreateCourseRegistrationsDoc(user);
1848
+ const course = regDoc.courses.find((c) => c.courseID === course_id);
1849
+ course.elo = elo;
1850
+ return getLocalUserDB(user).put(regDoc);
1851
+ }
1852
+ async function registerUserForClassroom(user, classID, registerAs) {
1853
+ log3(`Registering user: ${user} in course: ${classID}`);
1854
+ return getOrCreateClassroomRegistrationsDoc(user).then((doc) => {
1855
+ const regItem = {
1856
+ classID,
1857
+ registeredAs: registerAs
1858
+ };
1859
+ if (doc.registrations.filter((reg) => {
1860
+ return reg.classID === regItem.classID && reg.registeredAs === regItem.registeredAs;
1861
+ }).length === 0) {
1862
+ doc.registrations.push(regItem);
1863
+ } else {
1864
+ log3(`User ${user} is already registered for class ${classID}`);
1865
+ }
1866
+ return getLocalUserDB(user).put(doc);
1867
+ });
1868
+ }
1869
+ async function dropUserFromClassroom(user, classID) {
1870
+ return getOrCreateClassroomRegistrationsDoc(user).then((doc) => {
1871
+ let index = -1;
1872
+ for (let i = 0; i < doc.registrations.length; i++) {
1873
+ if (doc.registrations[i].classID === classID) {
1874
+ index = i;
1875
+ }
1876
+ }
1877
+ if (index !== -1) {
1878
+ doc.registrations.splice(index, 1);
1879
+ }
1880
+ return getLocalUserDB(user).put(doc);
1881
+ });
1882
+ }
1883
+ async function getUserClassrooms(user) {
1884
+ return getOrCreateClassroomRegistrationsDoc(user);
1885
+ }
1886
+ var log3, BaseUser, userCoursesDoc, userClassroomsDoc;
1887
+ var init_BaseUserDB = __esm({
1888
+ "src/impl/common/BaseUserDB.ts"() {
1889
+ "use strict";
1890
+ init_core();
1891
+ init_util();
1892
+ init_types_legacy();
1893
+ init_logger();
1894
+ init_userDBHelpers();
1895
+ init_updateQueue();
1896
+ init_user_course_relDB();
1897
+ init_couch();
1898
+ log3 = (s) => {
1899
+ logger.info(s);
1900
+ };
1901
+ BaseUser = class _BaseUser {
1902
+ static _instance;
1903
+ static _initialized = false;
1904
+ static Dummy(syncStrategy) {
1905
+ return new _BaseUser("Me", syncStrategy);
1906
+ }
1907
+ static DOC_IDS = {
1908
+ CONFIG: "CONFIG",
1909
+ COURSE_REGISTRATIONS: "CourseRegistrations",
1910
+ CLASSROOM_REGISTRATIONS: "ClassroomRegistrations"
1911
+ };
1912
+ // private email: string;
1913
+ _username;
1914
+ syncStrategy;
1915
+ getUsername() {
1916
+ return this._username;
1878
1917
  }
1879
- async getCourseInterface(courseId) {
1880
- return new UsrCrsData(this, courseId);
1918
+ isLoggedIn() {
1919
+ return !this._username.startsWith(GuestUsername);
1881
1920
  }
1882
- async getUserEditableCourses() {
1883
- let courseIDs = [];
1884
- const registeredCourses = await this.getCourseRegistrationsDoc();
1885
- courseIDs = courseIDs.concat(
1886
- registeredCourses.courses.map((course) => {
1887
- return course.courseID;
1888
- })
1889
- );
1890
- const cfgs = await Promise.all(
1891
- courseIDs.map(async (id) => {
1892
- return await getCredentialledCourseConfig(id);
1893
- })
1894
- );
1895
- return cfgs;
1921
+ remote() {
1922
+ return this.remoteDB;
1896
1923
  }
1897
- async getConfig() {
1898
- const defaultConfig = {
1899
- _id: _BaseUser.DOC_IDS.CONFIG,
1900
- darkMode: false,
1901
- likesConfetti: false
1902
- };
1903
- try {
1904
- const cfg = await this.localDB.get(_BaseUser.DOC_IDS.CONFIG);
1905
- logger.debug("Raw config from DB:", cfg);
1906
- return cfg;
1907
- } catch (e) {
1908
- const err = e;
1909
- if (err.name && err.name === "not_found") {
1910
- await this.localDB.put(defaultConfig);
1911
- return this.getConfig();
1912
- } else {
1913
- logger.error(`Error setting user default config:`, e);
1914
- throw new Error(`Error returning the user's configuration: ${JSON.stringify(e)}`);
1915
- }
1924
+ localDB;
1925
+ remoteDB;
1926
+ writeDB;
1927
+ // Database to use for write operations (local-first approach)
1928
+ updateQueue;
1929
+ async createAccount(username, password) {
1930
+ if (!this.syncStrategy.canCreateAccount()) {
1931
+ throw new Error("Account creation not supported by current sync strategy");
1916
1932
  }
1917
- }
1918
- async setConfig(items) {
1919
- logger.debug(`Setting Config items ${JSON.stringify(items)}`);
1920
- const c = await this.getConfig();
1921
- const put = await this.localDB.put({
1922
- ...c,
1923
- ...items
1924
- });
1925
- if (put.ok) {
1926
- logger.debug(`Config items set: ${JSON.stringify(items)}`);
1927
- } else {
1928
- logger.error(`Error setting config items: ${JSON.stringify(put)}`);
1933
+ if (!this._username.startsWith(GuestUsername)) {
1934
+ throw new Error(
1935
+ `Cannot create a new account while logged in:
1936
+ Currently logged-in as ${this._username}.`
1937
+ );
1929
1938
  }
1930
- }
1931
- /**
1932
- *
1933
- * This function should be called *only* by the pouchdb datalayer provider
1934
- * auth store.
1935
- *
1936
- *
1937
- * Anyone else seeking the current user should use the auth store's
1938
- * exported `getCurrentUser` method.
1939
- *
1940
- */
1941
- static async instance(syncStrategy, username) {
1942
- if (username) {
1943
- _BaseUser._instance = new _BaseUser(username, syncStrategy);
1944
- await _BaseUser._instance.init();
1945
- return _BaseUser._instance;
1946
- } else if (_BaseUser._instance && _BaseUser._initialized) {
1947
- return _BaseUser._instance;
1948
- } else if (_BaseUser._instance) {
1949
- return new Promise((resolve) => {
1950
- (function waitForUser() {
1951
- if (_BaseUser._initialized) {
1952
- return resolve(_BaseUser._instance);
1953
- } else {
1954
- setTimeout(waitForUser, 50);
1955
- }
1956
- })();
1957
- });
1958
- } else {
1959
- const guestUsername = await syncStrategy.getCurrentUsername();
1960
- _BaseUser._instance = new _BaseUser(guestUsername, syncStrategy);
1961
- await _BaseUser._instance.init();
1962
- return _BaseUser._instance;
1939
+ const result = await this.syncStrategy.createAccount(username, password);
1940
+ if (result.status === Status3.ok) {
1941
+ log3(`Account created successfully, updating username to ${username}`);
1942
+ this._username = username;
1943
+ try {
1944
+ localStorage.removeItem("dbUUID");
1945
+ } catch (e) {
1946
+ logger.warn("localStorage not available (Node.js environment):", e);
1947
+ }
1948
+ await this.init();
1963
1949
  }
1950
+ return {
1951
+ status: result.status,
1952
+ error: result.error || ""
1953
+ };
1964
1954
  }
1965
- constructor(username, syncStrategy) {
1966
- _BaseUser._initialized = false;
1967
- this._username = username;
1968
- this.syncStrategy = syncStrategy;
1969
- this.setDBandQ();
1970
- }
1971
- setDBandQ() {
1972
- this.localDB = getLocalUserDB(this._username);
1973
- this.remoteDB = this.syncStrategy.setupRemoteDB(this._username);
1974
- this.updateQueue = new UpdateQueue(this.localDB);
1975
- }
1976
- async init() {
1977
- _BaseUser._initialized = false;
1978
- if (this._username === "admin") {
1979
- _BaseUser._initialized = true;
1980
- return;
1955
+ async login(username, password) {
1956
+ if (!this.syncStrategy.canAuthenticate()) {
1957
+ throw new Error("Authentication not supported by current sync strategy");
1981
1958
  }
1982
- this.setDBandQ();
1983
- this.syncStrategy.startSync(this.localDB, this.remoteDB);
1984
- void this.applyDesignDocs();
1985
- void this.deduplicateReviews();
1986
- _BaseUser._initialized = true;
1987
- }
1988
- static designDocs = [
1989
- {
1990
- _id: "_design/reviewCards",
1991
- views: {
1992
- reviewCards: {
1993
- map: `function (doc) {
1994
- if (doc._id && doc._id.indexOf('card_review') === 0 && doc.courseId && doc.cardId) {
1995
- emit(doc._id, doc.courseId + '-' + doc.cardId);
1996
- }
1997
- }`
1998
- }
1959
+ if (!this._username.startsWith(GuestUsername) && this._username != username) {
1960
+ if (this._username != username) {
1961
+ throw new Error(`Cannot change accounts while logged in.
1962
+ Log out of account ${this.getUsername()} before logging in as ${username}.`);
1999
1963
  }
1964
+ logger.warn(`User ${this._username} is already logged in, but executing login again.`);
2000
1965
  }
2001
- ];
2002
- async applyDesignDocs() {
2003
- if (this._username === "admin") {
2004
- return;
2005
- }
2006
- for (const doc of _BaseUser.designDocs) {
1966
+ const loginResult = await this.syncStrategy.authenticate(username, password);
1967
+ if (loginResult.ok) {
1968
+ log3(`Logged in as ${username}`);
1969
+ this._username = username;
2007
1970
  try {
2008
- try {
2009
- const existingDoc = await this.remoteDB.get(doc._id);
2010
- await this.remoteDB.put({
2011
- ...doc,
2012
- _rev: existingDoc._rev
2013
- });
2014
- } catch (e) {
2015
- if (e instanceof Error && e.name === "not_found") {
2016
- await this.remoteDB.put(doc);
2017
- } else {
2018
- throw e;
2019
- }
2020
- }
2021
- } catch (error) {
2022
- if (error.name && error.name === "conflict") {
2023
- logger.warn(`Design doc ${doc._id} update conflict - will retry`);
2024
- await new Promise((resolve) => setTimeout(resolve, 1e3));
2025
- await this.applyDesignDoc(doc);
2026
- } else {
2027
- logger.error(`Failed to apply design doc ${doc._id}:`, error);
2028
- throw error;
2029
- }
1971
+ localStorage.removeItem("dbUUID");
1972
+ } catch (e) {
1973
+ logger.warn("localStorage not available (Node.js environment):", e);
1974
+ }
1975
+ await this.init();
1976
+ }
1977
+ return loginResult;
1978
+ }
1979
+ async resetUserData() {
1980
+ if (this.syncStrategy.canAuthenticate()) {
1981
+ return {
1982
+ status: Status3.error,
1983
+ error: "Reset user data is only available for local-only mode. Use logout instead for remote sync."
1984
+ };
1985
+ }
1986
+ try {
1987
+ const localDB = getLocalUserDB(this._username);
1988
+ const allDocs = await localDB.allDocs({ include_docs: false });
1989
+ const docsToDelete = allDocs.rows.filter((row) => {
1990
+ const id = row.id;
1991
+ return id.startsWith(DocTypePrefixes["CARDRECORD" /* CARDRECORD */]) || // Card interaction history
1992
+ id.startsWith(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */]) || // Scheduled reviews
1993
+ id === _BaseUser.DOC_IDS.COURSE_REGISTRATIONS || // Course registrations
1994
+ id === _BaseUser.DOC_IDS.CLASSROOM_REGISTRATIONS || // Classroom registrations
1995
+ id === _BaseUser.DOC_IDS.CONFIG;
1996
+ }).map((row) => ({ _id: row.id, _rev: row.value.rev, _deleted: true }));
1997
+ if (docsToDelete.length > 0) {
1998
+ await localDB.bulkDocs(docsToDelete);
2030
1999
  }
2000
+ await this.init();
2001
+ return { status: Status3.ok };
2002
+ } catch (error) {
2003
+ logger.error("Failed to reset user data:", error);
2004
+ return {
2005
+ status: Status3.error,
2006
+ error: error instanceof Error ? error.message : "Unknown error during reset"
2007
+ };
2008
+ }
2009
+ }
2010
+ async logout() {
2011
+ if (!this.syncStrategy.canAuthenticate()) {
2012
+ this._username = await this.syncStrategy.getCurrentUsername();
2013
+ await this.init();
2014
+ return { ok: true };
2031
2015
  }
2016
+ const ret = await this.syncStrategy.logout();
2017
+ this._username = await this.syncStrategy.getCurrentUsername();
2018
+ await this.init();
2019
+ return ret;
2032
2020
  }
2033
- // Helper method for single doc update with retry
2034
- async applyDesignDoc(doc, retries = 3) {
2021
+ update(id, update) {
2022
+ return this.updateQueue.update(id, update);
2023
+ }
2024
+ async getCourseRegistrationsDoc() {
2025
+ logger.debug(`Fetching courseRegistrations for ${this.getUsername()}`);
2026
+ let ret;
2035
2027
  try {
2036
- const existingDoc = await this.remoteDB.get(doc._id);
2037
- await this.remoteDB.put({
2038
- ...doc,
2039
- _rev: existingDoc._rev
2040
- });
2028
+ const regDoc = await this.localDB.get(
2029
+ _BaseUser.DOC_IDS.COURSE_REGISTRATIONS
2030
+ );
2031
+ return regDoc;
2041
2032
  } catch (e) {
2042
- if (e instanceof Error && e.name === "conflict" && retries > 0) {
2043
- await new Promise((resolve) => setTimeout(resolve, 1e3));
2044
- return this.applyDesignDoc(doc, retries - 1);
2033
+ const err = e;
2034
+ if (err.status === 404) {
2035
+ await this.localDB.put({
2036
+ _id: _BaseUser.DOC_IDS.COURSE_REGISTRATIONS,
2037
+ courses: [],
2038
+ studyWeight: {}
2039
+ });
2040
+ ret = await this.getCourseRegistrationsDoc();
2041
+ } else {
2042
+ throw new Error(
2043
+ `Unexpected error ${JSON.stringify(e)} in getOrCreateCourseRegistrationDoc...`
2044
+ );
2045
2045
  }
2046
- throw e;
2047
2046
  }
2047
+ return ret;
2048
+ }
2049
+ async getActiveCourses() {
2050
+ const reg = await this.getCourseRegistrationsDoc();
2051
+ return reg.courses.filter((c) => {
2052
+ return c.status === void 0 || c.status === "active";
2053
+ });
2048
2054
  }
2049
2055
  /**
2050
- * Logs a record of the user's interaction with the card and returns the card's
2051
- * up-to-date history
2052
- *
2053
- * // [ ] #db-refactor extract to a smaller scope - eg, UserStudySession
2056
+ * Returns a promise of the card IDs that the user has
2057
+ * a scheduled review for.
2054
2058
  *
2055
- * @param record the recent recorded interaction between user and card
2056
- * @returns The updated state of the card's CardHistory data
2057
2059
  */
2058
- async putCardRecord(record) {
2059
- const cardHistoryID = getCardHistoryID(record.courseID, record.cardID);
2060
- record.timeStamp = moment3.utc(record.timeStamp).toString();
2060
+ async getActiveCards() {
2061
+ const keys = getStartAndEndKeys2(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */]);
2062
+ const reviews = await this.remoteDB.allDocs({
2063
+ startkey: keys.startkey,
2064
+ endkey: keys.endkey,
2065
+ include_docs: true
2066
+ });
2067
+ return reviews.rows.map((r) => `${r.doc.courseId}-${r.doc.cardId}`);
2068
+ }
2069
+ async getActivityRecords() {
2061
2070
  try {
2062
- const cardHistory = await this.update(
2063
- cardHistoryID,
2064
- function(h) {
2065
- h.records.push(record);
2066
- h.bestInterval = h.bestInterval || 0;
2067
- h.lapses = h.lapses || 0;
2068
- h.streak = h.streak || 0;
2069
- return h;
2071
+ const hist = await this.getHistory();
2072
+ const allRecords = [];
2073
+ if (!Array.isArray(hist)) {
2074
+ logger.error("getHistory did not return an array:", hist);
2075
+ return allRecords;
2076
+ }
2077
+ let sampleCount = 0;
2078
+ for (let i = 0; i < hist.length; i++) {
2079
+ try {
2080
+ if (hist[i] && Array.isArray(hist[i].records)) {
2081
+ hist[i].records.forEach((record) => {
2082
+ try {
2083
+ if (!record.timeStamp) {
2084
+ return;
2085
+ }
2086
+ let timeStamp;
2087
+ if (typeof record.timeStamp === "object") {
2088
+ if (typeof record.timeStamp.toDate === "function") {
2089
+ timeStamp = record.timeStamp.toISOString();
2090
+ } else if (record.timeStamp instanceof Date) {
2091
+ timeStamp = record.timeStamp.toISOString();
2092
+ } else {
2093
+ if (sampleCount < 3) {
2094
+ logger.warn("Unknown timestamp object type:", record.timeStamp);
2095
+ sampleCount++;
2096
+ }
2097
+ return;
2098
+ }
2099
+ } else if (typeof record.timeStamp === "string") {
2100
+ const date = new Date(record.timeStamp);
2101
+ if (isNaN(date.getTime())) {
2102
+ return;
2103
+ }
2104
+ timeStamp = record.timeStamp;
2105
+ } else if (typeof record.timeStamp === "number") {
2106
+ timeStamp = new Date(record.timeStamp).toISOString();
2107
+ } else {
2108
+ return;
2109
+ }
2110
+ allRecords.push({
2111
+ timeStamp,
2112
+ courseID: record.courseID || "unknown",
2113
+ cardID: record.cardID || "unknown",
2114
+ timeSpent: record.timeSpent || 0,
2115
+ type: "card_view"
2116
+ });
2117
+ } catch (err) {
2118
+ }
2119
+ });
2120
+ }
2121
+ } catch (err) {
2122
+ logger.error("Error processing history item:", err);
2070
2123
  }
2071
- );
2072
- cardHistory.records = cardHistory.records.map((record2) => {
2073
- const ret = {
2074
- ...record2
2075
- };
2076
- ret.timeStamp = moment3.utc(record2.timeStamp);
2077
- return ret;
2078
- });
2079
- return cardHistory;
2080
- } catch (e) {
2081
- const reason = e;
2082
- if (reason.status === 404) {
2083
- const initCardHistory = {
2084
- _id: cardHistoryID,
2085
- cardID: record.cardID,
2086
- courseID: record.courseID,
2087
- records: [record],
2088
- lapses: 0,
2089
- streak: 0,
2090
- bestInterval: 0
2091
- };
2092
- void this.remoteDB.put(initCardHistory);
2093
- return initCardHistory;
2094
- } else {
2095
- throw new Error(`putCardRecord failed because of:
2096
- name:${reason.name}
2097
- error: ${reason.error}
2098
- message: ${reason.message}`);
2099
2124
  }
2125
+ logger.debug(`Found ${allRecords.length} activity records`);
2126
+ return allRecords;
2127
+ } catch (err) {
2128
+ logger.error("Error in getActivityRecords:", err);
2129
+ return [];
2100
2130
  }
2101
2131
  }
2102
- async deduplicateReviews() {
2103
- try {
2104
- log3("Starting deduplication of scheduled reviews...");
2105
- const reviewsMap = {};
2106
- const duplicateDocIds = [];
2107
- const scheduledReviews = await this.remoteDB.query("reviewCards/reviewCards");
2108
- log3(`Found ${scheduledReviews.rows.length} scheduled reviews to process`);
2109
- scheduledReviews.rows.forEach((r) => {
2110
- const qualifiedCardId = r.value;
2111
- const docId = r.key;
2112
- if (reviewsMap[qualifiedCardId]) {
2113
- log3(`Found duplicate scheduled review for card: ${qualifiedCardId}`);
2114
- log3(
2115
- `Marking earlier review ${reviewsMap[qualifiedCardId]} for deletion, keeping ${docId}`
2116
- );
2117
- duplicateDocIds.push(reviewsMap[qualifiedCardId]);
2118
- reviewsMap[qualifiedCardId] = docId;
2119
- } else {
2120
- reviewsMap[qualifiedCardId] = docId;
2121
- }
2122
- });
2123
- if (duplicateDocIds.length > 0) {
2124
- log3(`Removing ${duplicateDocIds.length} duplicate reviews...`);
2125
- const deletePromises = duplicateDocIds.map(async (docId) => {
2126
- try {
2127
- const doc = await this.remoteDB.get(docId);
2128
- await this.remoteDB.remove(doc);
2129
- log3(`Successfully removed duplicate review: ${docId}`);
2130
- } catch (error) {
2131
- log3(`Failed to remove duplicate review ${docId}: ${error}`);
2132
+ async getReviewstoDate(targetDate, course_id) {
2133
+ const keys = getStartAndEndKeys2(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */]);
2134
+ const reviews = await this.remoteDB.allDocs({
2135
+ startkey: keys.startkey,
2136
+ endkey: keys.endkey,
2137
+ include_docs: true
2138
+ });
2139
+ log3(
2140
+ `Fetching ${this._username}'s scheduled reviews${course_id ? ` for course ${course_id}` : ""}.`
2141
+ );
2142
+ return reviews.rows.filter((r) => {
2143
+ if (r.id.startsWith(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */])) {
2144
+ const date = moment4.utc(
2145
+ r.id.substr(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */].length),
2146
+ REVIEW_TIME_FORMAT2
2147
+ );
2148
+ if (targetDate.isAfter(date)) {
2149
+ if (course_id === void 0 || r.doc.courseId === course_id) {
2150
+ return true;
2132
2151
  }
2133
- });
2134
- await Promise.all(deletePromises);
2135
- log3(`Deduplication complete. Processed ${duplicateDocIds.length} duplicates`);
2136
- } else {
2137
- log3("No duplicate reviews found");
2152
+ }
2138
2153
  }
2139
- } catch (error) {
2140
- log3(`Error during review deduplication: ${error}`);
2141
- }
2154
+ }).map((r) => r.doc);
2142
2155
  }
2143
- /**
2144
- * Returns a promise of the card IDs that the user has
2145
- * encountered in the past.
2146
- *
2147
- * @param course_id optional specification of individual course
2148
- */
2149
- async getSeenCards(course_id) {
2150
- let prefix = cardHistoryPrefix2;
2151
- if (course_id) {
2152
- prefix += course_id;
2153
- }
2154
- const docs = await filterAllDocsByPrefix(this.localDB, prefix, {
2155
- include_docs: false
2156
+ async getReviewsForcast(daysCount) {
2157
+ const time = moment4.utc().add(daysCount, "days");
2158
+ return this.getReviewstoDate(time);
2159
+ }
2160
+ async getPendingReviews(course_id) {
2161
+ const now = moment4.utc();
2162
+ return this.getReviewstoDate(now, course_id);
2163
+ }
2164
+ async getScheduledReviewCount(course_id) {
2165
+ return (await this.getPendingReviews(course_id)).length;
2166
+ }
2167
+ async getRegisteredCourses() {
2168
+ const regDoc = await this.getCourseRegistrationsDoc();
2169
+ return regDoc.courses.filter((c) => {
2170
+ return !c.status || c.status === "active" || c.status === "maintenance-mode";
2156
2171
  });
2157
- const ret = [];
2158
- docs.rows.forEach((row) => {
2159
- if (row.id.startsWith(cardHistoryPrefix2)) {
2160
- ret.push(row.id.substr(cardHistoryPrefix2.length));
2172
+ }
2173
+ async getCourseRegDoc(courseID) {
2174
+ const regDocs = await this.getCourseRegistrationsDoc();
2175
+ const ret = regDocs.courses.find((c) => c.courseID === courseID);
2176
+ if (ret) {
2177
+ return ret;
2178
+ } else {
2179
+ throw new Error(`Course registration not found for course ID: ${courseID}`);
2180
+ }
2181
+ }
2182
+ async registerForCourse(course_id, previewMode = false) {
2183
+ return this.getCourseRegistrationsDoc().then((doc) => {
2184
+ const status = previewMode ? "preview" : "active";
2185
+ logger.debug(`Registering for ${course_id} with status: ${status}`);
2186
+ const regItem = {
2187
+ status,
2188
+ courseID: course_id,
2189
+ user: true,
2190
+ admin: false,
2191
+ moderator: false,
2192
+ elo: {
2193
+ global: {
2194
+ score: 1e3,
2195
+ count: 0
2196
+ },
2197
+ tags: {},
2198
+ misc: {}
2199
+ }
2200
+ };
2201
+ if (doc.courses.filter((course) => {
2202
+ return course.courseID === regItem.courseID;
2203
+ }).length === 0) {
2204
+ log3(`It's a new course registration!`);
2205
+ doc.courses.push(regItem);
2206
+ doc.studyWeight[course_id] = 1;
2207
+ } else {
2208
+ doc.courses.forEach((c) => {
2209
+ log3(`Found the previously registered course!`);
2210
+ if (c.courseID === course_id) {
2211
+ c.status = status;
2212
+ }
2213
+ });
2161
2214
  }
2215
+ return this.localDB.put(doc);
2216
+ }).catch((e) => {
2217
+ log3(`Registration failed because of: ${JSON.stringify(e)}`);
2218
+ throw e;
2162
2219
  });
2163
- return ret;
2164
- }
2165
- /**
2166
- *
2167
- * @returns A promise of the cards that the user has seen in the past.
2168
- */
2169
- async getHistory() {
2170
- const cards = await filterAllDocsByPrefix(
2171
- this.remoteDB,
2172
- cardHistoryPrefix2,
2173
- {
2174
- include_docs: true,
2175
- attachments: false
2176
- }
2177
- );
2178
- return cards.rows.map((r) => r.doc);
2179
2220
  }
2180
- async updateCourseSettings(course_id, settings) {
2181
- void this.getCourseRegistrationsDoc().then((doc) => {
2182
- const crs = doc.courses.find((c) => c.courseID === course_id);
2183
- if (crs) {
2184
- if (crs.settings === null || crs.settings === void 0) {
2185
- crs.settings = {};
2221
+ async dropCourse(course_id, dropStatus = "dropped") {
2222
+ return this.getCourseRegistrationsDoc().then((doc) => {
2223
+ let index = -1;
2224
+ for (let i = 0; i < doc.courses.length; i++) {
2225
+ if (doc.courses[i].courseID === course_id) {
2226
+ index = i;
2186
2227
  }
2187
- settings.forEach((setting) => {
2188
- crs.settings[setting.key] = setting.value;
2189
- });
2228
+ }
2229
+ if (index !== -1) {
2230
+ delete doc.studyWeight[course_id];
2231
+ doc.courses[index].status = dropStatus;
2232
+ } else {
2233
+ throw new Error(
2234
+ `User ${this.getUsername()} is not currently registered for course ${course_id}`
2235
+ );
2190
2236
  }
2191
2237
  return this.localDB.put(doc);
2192
2238
  });
2193
2239
  }
2194
- async getCourseSettings(course_id) {
2195
- const regDoc = await this.getCourseRegistrationsDoc();
2196
- const crsDoc = regDoc.courses.find((c) => c.courseID === course_id);
2197
- if (crsDoc) {
2198
- return crsDoc.settings;
2199
- } else {
2200
- throw new Error(`getCourseSettings Failed:
2201
- User is not registered for course ${course_id}`);
2202
- }
2240
+ async getCourseInterface(courseId) {
2241
+ return new UsrCrsData(this, courseId);
2203
2242
  }
2204
- async getOrCreateClassroomRegistrationsDoc() {
2205
- let ret;
2243
+ async getUserEditableCourses() {
2244
+ let courseIDs = [];
2245
+ const registeredCourses = await this.getCourseRegistrationsDoc();
2246
+ courseIDs = courseIDs.concat(
2247
+ registeredCourses.courses.map((course) => {
2248
+ return course.courseID;
2249
+ })
2250
+ );
2251
+ const cfgs = await Promise.all(
2252
+ courseIDs.map(async (id) => {
2253
+ return await getCredentialledCourseConfig(id);
2254
+ })
2255
+ );
2256
+ return cfgs;
2257
+ }
2258
+ async getConfig() {
2259
+ const defaultConfig = {
2260
+ _id: _BaseUser.DOC_IDS.CONFIG,
2261
+ darkMode: false,
2262
+ likesConfetti: false,
2263
+ sessionTimeLimit: 5
2264
+ };
2206
2265
  try {
2207
- ret = await this.remoteDB.get(
2208
- _BaseUser.DOC_IDS.CLASSROOM_REGISTRATIONS
2209
- );
2266
+ const cfg = await this.localDB.get(_BaseUser.DOC_IDS.CONFIG);
2267
+ logger.debug("Raw config from DB:", cfg);
2268
+ return cfg;
2210
2269
  } catch (e) {
2211
2270
  const err = e;
2212
- if (err.status === 404) {
2213
- await this.remoteDB.put({
2214
- _id: _BaseUser.DOC_IDS.CLASSROOM_REGISTRATIONS,
2215
- registrations: []
2216
- });
2217
- ret = await this.getOrCreateClassroomRegistrationsDoc();
2271
+ if (err.name && err.name === "not_found") {
2272
+ await this.localDB.put(defaultConfig);
2273
+ return this.getConfig();
2218
2274
  } else {
2219
- const errorDetails = {
2220
- name: err.name,
2221
- status: err.status,
2222
- message: err.message,
2223
- reason: err.reason,
2224
- error: err.error
2225
- };
2226
- logger.error(
2227
- "Database error in getOrCreateClassroomRegistrationsDoc (private method):",
2228
- errorDetails
2229
- );
2230
- throw new Error(
2231
- `Database error accessing classroom registrations: ${err.message || err.name || "Unknown error"} (status: ${err.status})`
2232
- );
2275
+ logger.error(`Error setting user default config:`, e);
2276
+ throw new Error(`Error returning the user's configuration: ${JSON.stringify(e)}`);
2233
2277
  }
2234
2278
  }
2235
- logger.debug(`Returning classroom registrations doc: ${JSON.stringify(ret)}`);
2236
- return ret;
2237
- }
2238
- /**
2239
- * Retrieves the list of active classroom IDs where the user is registered as a student.
2240
- *
2241
- * @returns Promise<string[]> - Array of classroom IDs, or empty array if classroom
2242
- * registration document is unavailable due to database errors
2243
- *
2244
- * @description This method gracefully handles database connectivity issues by returning
2245
- * an empty array when the classroom registrations document cannot be accessed.
2246
- * This ensures that users can still access other application features even
2247
- * when classroom functionality is temporarily unavailable.
2248
- */
2249
- async getActiveClasses() {
2250
- try {
2251
- return (await this.getOrCreateClassroomRegistrationsDoc()).registrations.filter((c) => c.registeredAs === "student").map((c) => c.classID);
2252
- } catch (error) {
2253
- logger.warn(
2254
- "Failed to load classroom registrations, continuing without classroom data:",
2255
- error
2256
- );
2257
- return [];
2258
- }
2259
- }
2260
- async scheduleCardReview(review) {
2261
- return scheduleCardReviewLocal(this.remoteDB, review);
2262
- }
2263
- async removeScheduledCardReview(reviewId) {
2264
- return removeScheduledCardReviewLocal(this.remoteDB, reviewId);
2265
- }
2266
- async registerForClassroom(_classId, _registerAs) {
2267
- return registerUserForClassroom(this._username, _classId, _registerAs);
2268
- }
2269
- async dropFromClassroom(classId) {
2270
- return dropUserFromClassroom(this._username, classId);
2271
- }
2272
- async getUserClassrooms() {
2273
- return getUserClassrooms(this._username);
2274
- }
2275
- async updateUserElo(courseId, elo) {
2276
- return updateUserElo(this._username, courseId, elo);
2277
- }
2278
- };
2279
- userCoursesDoc = "CourseRegistrations";
2280
- userClassroomsDoc = "ClassroomRegistrations";
2281
- }
2282
- });
2283
-
2284
- // src/impl/common/index.ts
2285
- var init_common = __esm({
2286
- "src/impl/common/index.ts"() {
2287
- "use strict";
2288
- init_SyncStrategy();
2289
- init_BaseUserDB();
2290
- init_userDBHelpers();
2291
- }
2292
- });
2293
-
2294
- // src/factory.ts
2295
- function getDataLayer() {
2296
- if (!dataLayerInstance) {
2297
- throw new Error("Data layer not initialized. Call initializeDataLayer first.");
2298
- }
2299
- return dataLayerInstance;
2300
- }
2301
- var ENV, dataLayerInstance;
2302
- var init_factory = __esm({
2303
- "src/factory.ts"() {
2304
- "use strict";
2305
- init_common();
2306
- init_logger();
2307
- ENV = {
2308
- COUCHDB_SERVER_PROTOCOL: "NOT_SET",
2309
- COUCHDB_SERVER_URL: "NOT_SET"
2310
- };
2311
- dataLayerInstance = null;
2312
- }
2313
- });
2314
-
2315
- // src/impl/couch/classroomDB.ts
2316
- import moment4 from "moment";
2317
- function getClassroomDB(classID, version) {
2318
- const dbName = `classdb-${version}-${classID}`;
2319
- logger.info(`Retrieving classroom db: ${dbName}`);
2320
- return new pouchdb_setup_default(
2321
- ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + dbName,
2322
- pouchDBincludeCredentialsConfig
2323
- );
2324
- }
2325
- async function getClassroomConfig(classID) {
2326
- return await getClassroomDB(classID, "student").get(CLASSROOM_CONFIG);
2327
- }
2328
- var classroomLookupDBTitle, CLASSROOM_CONFIG, ClassroomDBBase, StudentClassroomDB, TeacherClassroomDB, ClassroomLookupDB;
2329
- var init_classroomDB = __esm({
2330
- "src/impl/couch/classroomDB.ts"() {
2331
- "use strict";
2332
- init_factory();
2333
- init_logger();
2334
- init_pouchdb_setup();
2335
- init_couch();
2336
- init_courseDB();
2337
- classroomLookupDBTitle = "classdb-lookup";
2338
- CLASSROOM_CONFIG = "ClassroomConfig";
2339
- ClassroomDBBase = class {
2340
- _id;
2341
- _db;
2342
- _cfg;
2343
- _initComplete = false;
2344
- _content_prefix = "content";
2345
- get _content_searchkeys() {
2346
- return getStartAndEndKeys2(this._content_prefix);
2347
2279
  }
2348
- async getAssignedContent() {
2349
- logger.info(`Getting assigned content...`);
2350
- const docRows = await this._db.allDocs({
2351
- startkey: this._content_prefix,
2352
- endkey: this._content_prefix + `\uFFF0`,
2353
- include_docs: true
2354
- });
2355
- const ret = docRows.rows.map((row) => {
2356
- return row.doc;
2280
+ async setConfig(items) {
2281
+ logger.debug(`Setting Config items ${JSON.stringify(items)}`);
2282
+ const c = await this.getConfig();
2283
+ const put = await this.localDB.put({
2284
+ ...c,
2285
+ ...items
2357
2286
  });
2358
- return ret;
2359
- }
2360
- getContentId(content) {
2361
- if (content.type === "tag") {
2362
- return `${this._content_prefix}-${content.courseID}-${content.tagID}`;
2287
+ if (put.ok) {
2288
+ logger.debug(`Config items set: ${JSON.stringify(items)}`);
2363
2289
  } else {
2364
- return `${this._content_prefix}-${content.courseID}`;
2290
+ logger.error(`Error setting config items: ${JSON.stringify(put)}`);
2365
2291
  }
2366
2292
  }
2367
- get ready() {
2368
- return this._initComplete;
2293
+ /**
2294
+ *
2295
+ * This function should be called *only* by the pouchdb datalayer provider
2296
+ * auth store.
2297
+ *
2298
+ *
2299
+ * Anyone else seeking the current user should use the auth store's
2300
+ * exported `getCurrentUser` method.
2301
+ *
2302
+ */
2303
+ static async instance(syncStrategy, username) {
2304
+ if (username) {
2305
+ _BaseUser._instance = new _BaseUser(username, syncStrategy);
2306
+ await _BaseUser._instance.init();
2307
+ return _BaseUser._instance;
2308
+ } else if (_BaseUser._instance && _BaseUser._initialized) {
2309
+ return _BaseUser._instance;
2310
+ } else if (_BaseUser._instance) {
2311
+ return new Promise((resolve) => {
2312
+ (function waitForUser() {
2313
+ if (_BaseUser._initialized) {
2314
+ return resolve(_BaseUser._instance);
2315
+ } else {
2316
+ setTimeout(waitForUser, 50);
2317
+ }
2318
+ })();
2319
+ });
2320
+ } else {
2321
+ const guestUsername = await syncStrategy.getCurrentUsername();
2322
+ _BaseUser._instance = new _BaseUser(guestUsername, syncStrategy);
2323
+ await _BaseUser._instance.init();
2324
+ return _BaseUser._instance;
2325
+ }
2369
2326
  }
2370
- getConfig() {
2371
- return this._cfg;
2327
+ constructor(username, syncStrategy) {
2328
+ _BaseUser._initialized = false;
2329
+ this._username = username;
2330
+ this.syncStrategy = syncStrategy;
2331
+ this.setDBandQ();
2372
2332
  }
2373
- };
2374
- StudentClassroomDB = class _StudentClassroomDB extends ClassroomDBBase {
2375
- // private readonly _prefix: string = 'content';
2376
- userMessages;
2377
- _user;
2378
- constructor(classID, user) {
2379
- super();
2380
- this._id = classID;
2381
- this._user = user;
2333
+ setDBandQ() {
2334
+ this.localDB = getLocalUserDB(this._username);
2335
+ this.remoteDB = this.syncStrategy.setupRemoteDB(this._username);
2336
+ this.writeDB = this.syncStrategy.getWriteDB ? this.syncStrategy.getWriteDB(this._username) : this.localDB;
2337
+ this.updateQueue = new UpdateQueue(this.localDB, this.writeDB);
2382
2338
  }
2383
2339
  async init() {
2384
- const dbName = `classdb-student-${this._id}`;
2385
- this._db = new pouchdb_setup_default(
2386
- ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + dbName,
2387
- pouchDBincludeCredentialsConfig
2388
- );
2340
+ _BaseUser._initialized = false;
2341
+ if (this._username === "admin") {
2342
+ _BaseUser._initialized = true;
2343
+ return;
2344
+ }
2345
+ this.setDBandQ();
2346
+ this.syncStrategy.startSync(this.localDB, this.remoteDB);
2347
+ void this.applyDesignDocs();
2348
+ void this.deduplicateReviews();
2349
+ _BaseUser._initialized = true;
2350
+ }
2351
+ static designDocs = [
2352
+ {
2353
+ _id: "_design/reviewCards",
2354
+ views: {
2355
+ reviewCards: {
2356
+ map: `function (doc) {
2357
+ if (doc._id && doc._id.indexOf('card_review') === 0 && doc.courseId && doc.cardId) {
2358
+ emit(doc._id, doc.courseId + '-' + doc.cardId);
2359
+ }
2360
+ }`
2361
+ }
2362
+ }
2363
+ }
2364
+ ];
2365
+ async applyDesignDocs() {
2366
+ if (this._username === "admin") {
2367
+ return;
2368
+ }
2369
+ for (const doc of _BaseUser.designDocs) {
2370
+ try {
2371
+ try {
2372
+ const existingDoc = await this.remoteDB.get(doc._id);
2373
+ await this.remoteDB.put({
2374
+ ...doc,
2375
+ _rev: existingDoc._rev
2376
+ });
2377
+ } catch (e) {
2378
+ if (e instanceof Error && e.name === "not_found") {
2379
+ await this.remoteDB.put(doc);
2380
+ } else {
2381
+ throw e;
2382
+ }
2383
+ }
2384
+ } catch (error) {
2385
+ if (error.name && error.name === "conflict") {
2386
+ logger.warn(`Design doc ${doc._id} update conflict - will retry`);
2387
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
2388
+ await this.applyDesignDoc(doc);
2389
+ } else {
2390
+ logger.error(`Failed to apply design doc ${doc._id}:`, error);
2391
+ throw error;
2392
+ }
2393
+ }
2394
+ }
2395
+ }
2396
+ // Helper method for single doc update with retry
2397
+ async applyDesignDoc(doc, retries = 3) {
2389
2398
  try {
2390
- const cfg = await this._db.get(CLASSROOM_CONFIG);
2391
- this._cfg = cfg;
2392
- this.userMessages = this._db.changes({
2393
- since: "now",
2394
- live: true,
2395
- include_docs: true
2399
+ const existingDoc = await this.remoteDB.get(doc._id);
2400
+ await this.remoteDB.put({
2401
+ ...doc,
2402
+ _rev: existingDoc._rev
2396
2403
  });
2397
- this._initComplete = true;
2398
- return;
2399
2404
  } catch (e) {
2400
- throw new Error(`Error in StudentClassroomDB constructor: ${JSON.stringify(e)}`);
2405
+ if (e instanceof Error && e.name === "conflict" && retries > 0) {
2406
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
2407
+ return this.applyDesignDoc(doc, retries - 1);
2408
+ }
2409
+ throw e;
2401
2410
  }
2402
2411
  }
2403
- static async factory(classID, user) {
2404
- const ret = new _StudentClassroomDB(classID, user);
2405
- await ret.init();
2406
- return ret;
2412
+ /**
2413
+ * Logs a record of the user's interaction with the card and returns the card's
2414
+ * up-to-date history
2415
+ *
2416
+ * // [ ] #db-refactor extract to a smaller scope - eg, UserStudySession
2417
+ *
2418
+ * @param record the recent recorded interaction between user and card
2419
+ * @returns The updated state of the card's CardHistory data
2420
+ */
2421
+ async putCardRecord(record) {
2422
+ const cardHistoryID = getCardHistoryID(record.courseID, record.cardID);
2423
+ record.timeStamp = moment4.utc(record.timeStamp).toString();
2424
+ try {
2425
+ const cardHistory = await this.update(
2426
+ cardHistoryID,
2427
+ function(h) {
2428
+ h.records.push(record);
2429
+ h.bestInterval = h.bestInterval || 0;
2430
+ h.lapses = h.lapses || 0;
2431
+ h.streak = h.streak || 0;
2432
+ return h;
2433
+ }
2434
+ );
2435
+ cardHistory.records = cardHistory.records.map((record2) => {
2436
+ const ret = {
2437
+ ...record2
2438
+ };
2439
+ ret.timeStamp = moment4.utc(record2.timeStamp);
2440
+ return ret;
2441
+ });
2442
+ return cardHistory;
2443
+ } catch (e) {
2444
+ const reason = e;
2445
+ if (reason.status === 404) {
2446
+ const initCardHistory = {
2447
+ _id: cardHistoryID,
2448
+ cardID: record.cardID,
2449
+ courseID: record.courseID,
2450
+ records: [record],
2451
+ lapses: 0,
2452
+ streak: 0,
2453
+ bestInterval: 0
2454
+ };
2455
+ const putResult = await this.writeDB.put(initCardHistory);
2456
+ return { ...initCardHistory, _rev: putResult.rev };
2457
+ } else {
2458
+ throw new Error(`putCardRecord failed because of:
2459
+ name:${reason.name}
2460
+ error: ${reason.error}
2461
+ message: ${reason.message}`);
2462
+ }
2463
+ }
2407
2464
  }
2408
- setChangeFcn(f) {
2409
- void this.userMessages.on("change", f);
2465
+ async deduplicateReviews() {
2466
+ try {
2467
+ log3("Starting deduplication of scheduled reviews...");
2468
+ const reviewsMap = {};
2469
+ const duplicateDocIds = [];
2470
+ const scheduledReviews = await this.remoteDB.query("reviewCards/reviewCards");
2471
+ log3(`Found ${scheduledReviews.rows.length} scheduled reviews to process`);
2472
+ scheduledReviews.rows.forEach((r) => {
2473
+ const qualifiedCardId = r.value;
2474
+ const docId = r.key;
2475
+ if (reviewsMap[qualifiedCardId]) {
2476
+ log3(`Found duplicate scheduled review for card: ${qualifiedCardId}`);
2477
+ log3(
2478
+ `Marking earlier review ${reviewsMap[qualifiedCardId]} for deletion, keeping ${docId}`
2479
+ );
2480
+ duplicateDocIds.push(reviewsMap[qualifiedCardId]);
2481
+ reviewsMap[qualifiedCardId] = docId;
2482
+ } else {
2483
+ reviewsMap[qualifiedCardId] = docId;
2484
+ }
2485
+ });
2486
+ if (duplicateDocIds.length > 0) {
2487
+ log3(`Removing ${duplicateDocIds.length} duplicate reviews...`);
2488
+ const deletePromises = duplicateDocIds.map(async (docId) => {
2489
+ try {
2490
+ const doc = await this.remoteDB.get(docId);
2491
+ await this.writeDB.remove(doc);
2492
+ log3(`Successfully removed duplicate review: ${docId}`);
2493
+ } catch (error) {
2494
+ log3(`Failed to remove duplicate review ${docId}: ${error}`);
2495
+ }
2496
+ });
2497
+ await Promise.all(deletePromises);
2498
+ log3(`Deduplication complete. Processed ${duplicateDocIds.length} duplicates`);
2499
+ } else {
2500
+ log3("No duplicate reviews found");
2501
+ }
2502
+ } catch (error) {
2503
+ log3(`Error during review deduplication: ${error}`);
2504
+ }
2410
2505
  }
2411
- async getPendingReviews() {
2412
- const u = this._user;
2413
- return (await u.getPendingReviews()).filter((r) => r.scheduledFor === "classroom" && r.schedulingAgentId === this._id).map((r) => {
2414
- return {
2415
- ...r,
2416
- qualifiedID: `${r.courseId}-${r.cardId}`,
2417
- courseID: r.courseId,
2418
- cardID: r.cardId,
2419
- contentSourceType: "classroom",
2420
- contentSourceID: this._id,
2421
- reviewID: r._id,
2422
- status: "review"
2423
- };
2506
+ /**
2507
+ * Returns a promise of the card IDs that the user has
2508
+ * encountered in the past.
2509
+ *
2510
+ * @param course_id optional specification of individual course
2511
+ */
2512
+ async getSeenCards(course_id) {
2513
+ let prefix = DocTypePrefixes["CARDRECORD" /* CARDRECORD */];
2514
+ if (course_id) {
2515
+ prefix += course_id;
2516
+ }
2517
+ const docs = await filterAllDocsByPrefix2(this.localDB, prefix, {
2518
+ include_docs: false
2519
+ });
2520
+ const ret = [];
2521
+ docs.rows.forEach((row) => {
2522
+ if (row.id.startsWith(DocTypePrefixes["CARDRECORD" /* CARDRECORD */])) {
2523
+ ret.push(row.id.substr(DocTypePrefixes["CARDRECORD" /* CARDRECORD */].length));
2524
+ }
2424
2525
  });
2526
+ return ret;
2425
2527
  }
2426
- async getNewCards() {
2427
- const activeCards = await this._user.getActiveCards();
2428
- const now = moment4.utc();
2429
- const assigned = await this.getAssignedContent();
2430
- const due = assigned.filter((c) => now.isAfter(moment4.utc(c.activeOn, REVIEW_TIME_FORMAT2)));
2431
- logger.info(`Due content: ${JSON.stringify(due)}`);
2432
- let ret = [];
2433
- for (let i = 0; i < due.length; i++) {
2434
- const content = due[i];
2435
- if (content.type === "course") {
2436
- const db = new CourseDB(content.courseID, async () => this._user);
2437
- ret = ret.concat(await db.getNewCards());
2438
- } else if (content.type === "tag") {
2439
- const tagDoc = await getTag(content.courseID, content.tagID);
2440
- ret = ret.concat(
2441
- tagDoc.taggedCards.map((c) => {
2442
- return {
2443
- courseID: content.courseID,
2444
- cardID: c,
2445
- qualifiedID: `${content.courseID}-${c}`,
2446
- contentSourceType: "classroom",
2447
- contentSourceID: this._id,
2448
- status: "new"
2449
- };
2450
- })
2451
- );
2452
- } else if (content.type === "card") {
2453
- ret.push(await getCourseDB2(content.courseID).get(content.cardID));
2528
+ /**
2529
+ *
2530
+ * @returns A promise of the cards that the user has seen in the past.
2531
+ */
2532
+ async getHistory() {
2533
+ const cards = await filterAllDocsByPrefix2(
2534
+ this.remoteDB,
2535
+ DocTypePrefixes["CARDRECORD" /* CARDRECORD */],
2536
+ {
2537
+ include_docs: true,
2538
+ attachments: false
2454
2539
  }
2455
- }
2456
- logger.info(`New Cards from classroom ${this._cfg.name}: ${ret.map((c) => c.qualifiedID)}`);
2457
- return ret.filter((c) => {
2458
- if (activeCards.some((ac) => c.qualifiedID.includes(ac))) {
2459
- return false;
2460
- } else {
2461
- return true;
2540
+ );
2541
+ return cards.rows.map((r) => r.doc);
2542
+ }
2543
+ async updateCourseSettings(course_id, settings) {
2544
+ void this.getCourseRegistrationsDoc().then((doc) => {
2545
+ const crs = doc.courses.find((c) => c.courseID === course_id);
2546
+ if (crs) {
2547
+ if (crs.settings === null || crs.settings === void 0) {
2548
+ crs.settings = {};
2549
+ }
2550
+ settings.forEach((setting) => {
2551
+ crs.settings[setting.key] = setting.value;
2552
+ });
2462
2553
  }
2554
+ return this.localDB.put(doc);
2463
2555
  });
2464
2556
  }
2465
- };
2466
- TeacherClassroomDB = class _TeacherClassroomDB extends ClassroomDBBase {
2467
- _stuDb;
2468
- constructor(classID) {
2469
- super();
2470
- this._id = classID;
2557
+ async getCourseSettings(course_id) {
2558
+ const regDoc = await this.getCourseRegistrationsDoc();
2559
+ const crsDoc = regDoc.courses.find((c) => c.courseID === course_id);
2560
+ if (crsDoc) {
2561
+ return crsDoc.settings;
2562
+ } else {
2563
+ throw new Error(`getCourseSettings Failed:
2564
+ User is not registered for course ${course_id}`);
2565
+ }
2471
2566
  }
2472
- async init() {
2473
- const dbName = `classdb-teacher-${this._id}`;
2474
- const stuDbName = `classdb-student-${this._id}`;
2475
- this._db = new pouchdb_setup_default(
2476
- ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + dbName,
2477
- pouchDBincludeCredentialsConfig
2478
- );
2479
- this._stuDb = new pouchdb_setup_default(
2480
- ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + stuDbName,
2481
- pouchDBincludeCredentialsConfig
2482
- );
2567
+ async getOrCreateClassroomRegistrationsDoc() {
2568
+ let ret;
2483
2569
  try {
2484
- return this._db.get(CLASSROOM_CONFIG).then((cfg) => {
2485
- this._cfg = cfg;
2486
- this._initComplete = true;
2487
- }).then(() => {
2488
- return;
2489
- });
2570
+ ret = await this.remoteDB.get(
2571
+ _BaseUser.DOC_IDS.CLASSROOM_REGISTRATIONS
2572
+ );
2490
2573
  } catch (e) {
2491
- throw new Error(`Error in TeacherClassroomDB constructor: ${JSON.stringify(e)}`);
2574
+ const err = e;
2575
+ if (err.status === 404) {
2576
+ await this.writeDB.put({
2577
+ _id: _BaseUser.DOC_IDS.CLASSROOM_REGISTRATIONS,
2578
+ registrations: []
2579
+ });
2580
+ ret = await this.getOrCreateClassroomRegistrationsDoc();
2581
+ } else {
2582
+ const errorDetails = {
2583
+ name: err.name,
2584
+ status: err.status,
2585
+ message: err.message,
2586
+ reason: err.reason,
2587
+ error: err.error
2588
+ };
2589
+ logger.error(
2590
+ "Database error in getOrCreateClassroomRegistrationsDoc (private method):",
2591
+ errorDetails
2592
+ );
2593
+ throw new Error(
2594
+ `Database error accessing classroom registrations: ${err.message || err.name || "Unknown error"} (status: ${err.status})`
2595
+ );
2596
+ }
2492
2597
  }
2493
- }
2494
- static async factory(classID) {
2495
- const ret = new _TeacherClassroomDB(classID);
2496
- await ret.init();
2598
+ logger.debug(`Returning classroom registrations doc: ${JSON.stringify(ret)}`);
2497
2599
  return ret;
2498
2600
  }
2499
- async removeContent(content) {
2500
- const contentID = this.getContentId(content);
2601
+ /**
2602
+ * Retrieves the list of active classroom IDs where the user is registered as a student.
2603
+ *
2604
+ * @returns Promise<string[]> - Array of classroom IDs, or empty array if classroom
2605
+ * registration document is unavailable due to database errors
2606
+ *
2607
+ * @description This method gracefully handles database connectivity issues by returning
2608
+ * an empty array when the classroom registrations document cannot be accessed.
2609
+ * This ensures that users can still access other application features even
2610
+ * when classroom functionality is temporarily unavailable.
2611
+ */
2612
+ async getActiveClasses() {
2501
2613
  try {
2502
- const doc = await this._db.get(contentID);
2503
- await this._db.remove(doc);
2504
- void this._db.replicate.to(this._stuDb, {
2505
- doc_ids: [contentID]
2506
- });
2614
+ return (await this.getOrCreateClassroomRegistrationsDoc()).registrations.filter((c) => c.registeredAs === "student").map((c) => c.classID);
2507
2615
  } catch (error) {
2508
- logger.error("Failed to remove content:", contentID, error);
2616
+ logger.warn(
2617
+ "Failed to load classroom registrations, continuing without classroom data:",
2618
+ error
2619
+ );
2620
+ return [];
2509
2621
  }
2510
2622
  }
2511
- async assignContent(content) {
2512
- let put;
2513
- const id = this.getContentId(content);
2514
- if (content.type === "tag") {
2515
- put = await this._db.put({
2516
- courseID: content.courseID,
2517
- tagID: content.tagID,
2518
- type: "tag",
2519
- _id: id,
2520
- assignedBy: content.assignedBy,
2521
- assignedOn: moment4.utc(),
2522
- activeOn: content.activeOn || moment4.utc()
2523
- });
2524
- } else {
2525
- put = await this._db.put({
2526
- courseID: content.courseID,
2527
- type: "course",
2528
- _id: id,
2529
- assignedBy: content.assignedBy,
2530
- assignedOn: moment4.utc(),
2531
- activeOn: content.activeOn || moment4.utc()
2532
- });
2533
- }
2534
- if (put.ok) {
2535
- void this._db.replicate.to(this._stuDb, {
2536
- doc_ids: [id]
2537
- });
2538
- return true;
2539
- } else {
2540
- return false;
2541
- }
2623
+ async scheduleCardReview(review) {
2624
+ return scheduleCardReviewLocal(this.writeDB, review);
2625
+ }
2626
+ async removeScheduledCardReview(reviewId) {
2627
+ return removeScheduledCardReviewLocal(this.writeDB, reviewId);
2628
+ }
2629
+ async registerForClassroom(_classId, _registerAs) {
2630
+ return registerUserForClassroom(this._username, _classId, _registerAs);
2631
+ }
2632
+ async dropFromClassroom(classId) {
2633
+ return dropUserFromClassroom(this._username, classId);
2634
+ }
2635
+ async getUserClassrooms() {
2636
+ return getUserClassrooms(this._username);
2637
+ }
2638
+ async updateUserElo(courseId, elo) {
2639
+ return updateUserElo(this._username, courseId, elo);
2542
2640
  }
2543
2641
  };
2544
- ClassroomLookupDB = () => new pouchdb_setup_default(ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + classroomLookupDBTitle, {
2545
- skip_setup: true
2546
- });
2642
+ userCoursesDoc = "CourseRegistrations";
2643
+ userClassroomsDoc = "ClassroomRegistrations";
2547
2644
  }
2548
2645
  });
2549
2646
 
2550
- // src/core/interfaces/contentSource.ts
2551
- function isReview(item) {
2552
- const ret = item.status === "review" || item.status === "failed-review" || "reviewID" in item;
2553
- return ret;
2554
- }
2555
- async function getStudySource(source, user) {
2556
- if (source.type === "classroom") {
2557
- return await StudentClassroomDB.factory(source.id, user);
2558
- } else {
2559
- return getDataLayer().getCourseDB(source.id);
2647
+ // src/impl/common/index.ts
2648
+ var init_common = __esm({
2649
+ "src/impl/common/index.ts"() {
2650
+ "use strict";
2651
+ init_SyncStrategy();
2652
+ init_BaseUserDB();
2653
+ init_userDBHelpers();
2560
2654
  }
2655
+ });
2656
+
2657
+ // src/factory.ts
2658
+ function getDataLayer() {
2659
+ if (!dataLayerInstance) {
2660
+ throw new Error("Data layer not initialized. Call initializeDataLayer first.");
2661
+ }
2662
+ return dataLayerInstance;
2561
2663
  }
2562
- var init_contentSource = __esm({
2563
- "src/core/interfaces/contentSource.ts"() {
2664
+ var ENV, dataLayerInstance;
2665
+ var init_factory = __esm({
2666
+ "src/factory.ts"() {
2564
2667
  "use strict";
2565
- init_factory();
2566
- init_classroomDB();
2668
+ init_common();
2669
+ init_logger();
2670
+ ENV = {
2671
+ COUCHDB_SERVER_PROTOCOL: "NOT_SET",
2672
+ COUCHDB_SERVER_URL: "NOT_SET"
2673
+ };
2674
+ dataLayerInstance = null;
2567
2675
  }
2568
2676
  });
2569
2677
 
2570
2678
  // src/impl/couch/adminDB.ts
2571
2679
  var AdminDB;
2572
- var init_adminDB = __esm({
2680
+ var init_adminDB2 = __esm({
2573
2681
  "src/impl/couch/adminDB.ts"() {
2574
2682
  "use strict";
2575
2683
  init_pouchdb_setup();
2576
2684
  init_factory();
2577
2685
  init_couch();
2578
- init_classroomDB();
2686
+ init_classroomDB2();
2579
2687
  init_courseLookupDB();
2580
2688
  init_logger();
2581
2689
  AdminDB = class {
@@ -2589,7 +2697,7 @@ var init_adminDB = __esm({
2589
2697
  async getUsers() {
2590
2698
  return (await this.usersDB.allDocs({
2591
2699
  include_docs: true,
2592
- ...getStartAndEndKeys2("org.couchdb.user:")
2700
+ ...getStartAndEndKeys("org.couchdb.user:")
2593
2701
  })).rows.map((r) => r.doc);
2594
2702
  }
2595
2703
  async getCourses() {
@@ -2682,7 +2790,7 @@ var init_auth = __esm({
2682
2790
  });
2683
2791
 
2684
2792
  // src/impl/couch/CouchDBSyncStrategy.ts
2685
- import { Status as Status3 } from "@vue-skuilder/common";
2793
+ import { Status as Status4 } from "@vue-skuilder/common";
2686
2794
  var log4, CouchDBSyncStrategy;
2687
2795
  var init_CouchDBSyncStrategy = __esm({
2688
2796
  "src/impl/couch/CouchDBSyncStrategy.ts"() {
@@ -2707,6 +2815,13 @@ var init_CouchDBSyncStrategy = __esm({
2707
2815
  return this.getUserDB(username);
2708
2816
  }
2709
2817
  }
2818
+ getWriteDB(username) {
2819
+ if (username === GuestUsername || username.startsWith(GuestUsername)) {
2820
+ return getLocalUserDB(username);
2821
+ } else {
2822
+ return this.getUserDB(username);
2823
+ }
2824
+ }
2710
2825
  startSync(localDB, remoteDB) {
2711
2826
  if (localDB !== remoteDB) {
2712
2827
  this.syncHandle = pouchdb_setup_default.sync(localDB, remoteDB, {
@@ -2741,32 +2856,32 @@ var init_CouchDBSyncStrategy = __esm({
2741
2856
  log4(`CREATEACCOUNT: logged in as new user: ${loginResult.ok}`);
2742
2857
  if (loginResult.ok) {
2743
2858
  return {
2744
- status: Status3.ok,
2859
+ status: Status4.ok,
2745
2860
  error: void 0
2746
2861
  };
2747
2862
  } else {
2748
2863
  return {
2749
- status: Status3.error,
2864
+ status: Status4.error,
2750
2865
  error: "Failed to log in after account creation"
2751
2866
  };
2752
2867
  }
2753
2868
  } else {
2754
2869
  logger.warn(`Signup not OK: ${JSON.stringify(signupRequest)}`);
2755
2870
  return {
2756
- status: Status3.error,
2871
+ status: Status4.error,
2757
2872
  error: "Account creation failed"
2758
2873
  };
2759
2874
  }
2760
2875
  } catch (e) {
2761
2876
  if (e.reason === "Document update conflict.") {
2762
2877
  return {
2763
- status: Status3.error,
2878
+ status: Status4.error,
2764
2879
  error: "This username is taken!"
2765
2880
  };
2766
2881
  }
2767
2882
  logger.error(`Error on signup: ${JSON.stringify(e)}`);
2768
2883
  return {
2769
- status: Status3.error,
2884
+ status: Status4.error,
2770
2885
  error: e.message || "Unknown error during account creation"
2771
2886
  };
2772
2887
  }
@@ -2965,16 +3080,16 @@ function scheduleCardReview(review) {
2965
3080
  const now = moment5.utc();
2966
3081
  logger.info(`Scheduling for review in: ${review.time.diff(now, "h") / 24} days`);
2967
3082
  void getCouchUserDB(review.user).put({
2968
- _id: REVIEW_PREFIX2 + review.time.format(REVIEW_TIME_FORMAT2),
3083
+ _id: DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */] + review.time.format(REVIEW_TIME_FORMAT),
2969
3084
  cardId: review.card_id,
2970
- reviewTime: review.time,
3085
+ reviewTime: review.time.toISOString(),
2971
3086
  courseId: review.course_id,
2972
- scheduledAt: now,
3087
+ scheduledAt: now.toISOString(),
2973
3088
  scheduledFor: review.scheduledFor,
2974
3089
  schedulingAgentId: review.schedulingAgentId
2975
3090
  });
2976
3091
  }
2977
- function filterAllDocsByPrefix2(db, prefix, opts) {
3092
+ function filterAllDocsByPrefix(db, prefix, opts) {
2978
3093
  const options = {
2979
3094
  startkey: prefix,
2980
3095
  endkey: prefix + "\uFFF0",
@@ -2985,13 +3100,13 @@ function filterAllDocsByPrefix2(db, prefix, opts) {
2985
3100
  }
2986
3101
  return db.allDocs(options);
2987
3102
  }
2988
- function getStartAndEndKeys2(key) {
3103
+ function getStartAndEndKeys(key) {
2989
3104
  return {
2990
3105
  startkey: key,
2991
3106
  endkey: key + "\uFFF0"
2992
3107
  };
2993
3108
  }
2994
- var isBrowser, expiryDocID, GUEST_LOCAL_DB, localUserDB, pouchDBincludeCredentialsConfig, REVIEW_PREFIX2, REVIEW_TIME_FORMAT2;
3109
+ var isBrowser, expiryDocID, GUEST_LOCAL_DB, localUserDB, pouchDBincludeCredentialsConfig, REVIEW_TIME_FORMAT;
2995
3110
  var init_couch = __esm({
2996
3111
  "src/impl/couch/index.ts"() {
2997
3112
  init_factory();
@@ -2999,8 +3114,8 @@ var init_couch = __esm({
2999
3114
  init_logger();
3000
3115
  init_pouchdb_setup();
3001
3116
  init_contentSource();
3002
- init_adminDB();
3003
- init_classroomDB();
3117
+ init_adminDB2();
3118
+ init_classroomDB2();
3004
3119
  init_courseAPI();
3005
3120
  init_courseDB();
3006
3121
  init_CouchDBSyncStrategy();
@@ -3017,8 +3132,7 @@ var init_couch = __esm({
3017
3132
  return pouchdb_setup_default.fetch(url, opts);
3018
3133
  }
3019
3134
  };
3020
- REVIEW_PREFIX2 = "card_review_";
3021
- REVIEW_TIME_FORMAT2 = "YYYY-MM-DD--kk:mm:ss-SSS";
3135
+ REVIEW_TIME_FORMAT = "YYYY-MM-DD--kk:mm:ss-SSS";
3022
3136
  }
3023
3137
  });
3024
3138
  init_couch();
@@ -3029,15 +3143,14 @@ export {
3029
3143
  CouchDBSyncStrategy,
3030
3144
  CourseDB,
3031
3145
  CoursesDB,
3032
- REVIEW_PREFIX2 as REVIEW_PREFIX,
3033
- REVIEW_TIME_FORMAT2 as REVIEW_TIME_FORMAT,
3146
+ REVIEW_TIME_FORMAT,
3034
3147
  StudentClassroomDB,
3035
3148
  TeacherClassroomDB,
3036
3149
  addNote55,
3037
3150
  addTagToCard,
3038
3151
  createTag,
3039
3152
  deleteTag,
3040
- filterAllDocsByPrefix2 as filterAllDocsByPrefix,
3153
+ filterAllDocsByPrefix,
3041
3154
  getAncestorTagIDs,
3042
3155
  getAppliedTags,
3043
3156
  getChildTagStubs,
@@ -3054,7 +3167,7 @@ export {
3054
3167
  getCredentialledDataShapes,
3055
3168
  getLatestVersion,
3056
3169
  getRandomCards,
3057
- getStartAndEndKeys2 as getStartAndEndKeys,
3170
+ getStartAndEndKeys,
3058
3171
  getStudySource,
3059
3172
  getTag,
3060
3173
  getTagID,