@vue-skuilder/db 0.1.6 → 0.1.8-0

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