@vue-skuilder/db 0.1.6 → 0.1.8-0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/dist/{SyncStrategy-DnJRj-Xp.d.mts → SyncStrategy-CyATpyLQ.d.mts} +6 -0
  2. package/dist/{SyncStrategy-DnJRj-Xp.d.ts → SyncStrategy-CyATpyLQ.d.ts} +6 -0
  3. package/dist/core/index.d.mts +5 -5
  4. package/dist/core/index.d.ts +5 -5
  5. package/dist/core/index.js +825 -762
  6. package/dist/core/index.js.map +1 -1
  7. package/dist/core/index.mjs +812 -750
  8. package/dist/core/index.mjs.map +1 -1
  9. package/dist/{dataLayerProvider-BZmLyBVw.d.mts → dataLayerProvider-BInqI_RF.d.mts} +1 -1
  10. package/dist/{dataLayerProvider-BuntXkCs.d.ts → dataLayerProvider-DqtNroSh.d.ts} +1 -1
  11. package/dist/impl/couch/index.d.mts +6 -6
  12. package/dist/impl/couch/index.d.ts +6 -6
  13. package/dist/impl/couch/index.js +2261 -2081
  14. package/dist/impl/couch/index.js.map +1 -1
  15. package/dist/impl/couch/index.mjs +2274 -2095
  16. package/dist/impl/couch/index.mjs.map +1 -1
  17. package/dist/impl/static/index.d.mts +8 -6
  18. package/dist/impl/static/index.d.ts +8 -6
  19. package/dist/impl/static/index.js +524 -1064
  20. package/dist/impl/static/index.js.map +1 -1
  21. package/dist/impl/static/index.mjs +515 -1058
  22. package/dist/impl/static/index.mjs.map +1 -1
  23. package/dist/index-CLL31bEy.d.ts +137 -0
  24. package/dist/index-CUNnL38E.d.mts +137 -0
  25. package/dist/index.d.mts +200 -9
  26. package/dist/index.d.ts +200 -9
  27. package/dist/index.js +4123 -2820
  28. package/dist/index.js.map +1 -1
  29. package/dist/index.mjs +4119 -2830
  30. package/dist/index.mjs.map +1 -1
  31. package/dist/{types-D6SnlHPm.d.ts → types-BefDGkKa.d.ts} +1 -1
  32. package/dist/{types-DPRvCrIk.d.mts → types-DC-ckZug.d.mts} +1 -1
  33. package/dist/{types-legacy-WPe8CtO-.d.mts → types-legacy-Birv-Jx6.d.mts} +2 -2
  34. package/dist/{types-legacy-WPe8CtO-.d.ts → types-legacy-Birv-Jx6.d.ts} +2 -2
  35. package/dist/{userDB-D9EuWTp1.d.ts → userDB-C33Hzjgn.d.mts} +11 -4
  36. package/dist/{userDB-31gsvxyd.d.mts → userDB-DusL7OXe.d.ts} +11 -4
  37. package/dist/util/packer/index.d.mts +3 -63
  38. package/dist/util/packer/index.d.ts +3 -63
  39. package/dist/util/packer/index.js +53 -1
  40. package/dist/util/packer/index.js.map +1 -1
  41. package/dist/util/packer/index.mjs +53 -1
  42. package/dist/util/packer/index.mjs.map +1 -1
  43. package/package.json +7 -4
  44. package/src/core/types/types-legacy.ts +13 -1
  45. package/src/core/types/user.ts +9 -2
  46. package/src/core/util/index.ts +5 -4
  47. package/src/factory.ts +25 -0
  48. package/src/impl/common/BaseUserDB.ts +62 -28
  49. package/src/impl/common/SyncStrategy.ts +7 -0
  50. package/src/impl/common/index.ts +0 -1
  51. package/src/impl/common/userDBHelpers.ts +15 -5
  52. package/src/impl/couch/CouchDBSyncStrategy.ts +10 -0
  53. package/src/impl/couch/courseAPI.ts +7 -6
  54. package/src/impl/couch/courseLookupDB.ts +24 -0
  55. package/src/impl/couch/index.ts +10 -5
  56. package/src/impl/couch/updateQueue.ts +12 -8
  57. package/src/impl/couch/user-course-relDB.ts +17 -27
  58. package/src/impl/static/NoOpSyncStrategy.ts +5 -0
  59. package/src/impl/static/StaticDataUnpacker.ts +18 -36
  60. package/src/impl/static/courseDB.ts +135 -17
  61. package/src/util/dataDirectory.test.ts +53 -0
  62. package/src/util/dataDirectory.ts +52 -0
  63. package/src/util/index.ts +3 -0
  64. package/src/util/migrator/FileSystemAdapter.ts +79 -0
  65. package/src/util/migrator/StaticToCouchDBMigrator.ts +713 -0
  66. package/src/util/migrator/index.ts +18 -0
  67. package/src/util/migrator/types.ts +84 -0
  68. package/src/util/migrator/validation.ts +517 -0
  69. package/src/util/packer/CouchDBToStaticPacker.ts +92 -2
  70. package/src/util/tuiLogger.ts +139 -0
@@ -5,10 +5,10 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __glob = (map) => (path) => {
9
- var fn = map[path];
8
+ var __glob = (map) => (path2) => {
9
+ var fn = map[path2];
10
10
  if (fn) return fn();
11
- throw new Error("Module not found in bundle: " + path);
11
+ throw new Error("Module not found in bundle: " + path2);
12
12
  };
13
13
  var __esm = (fn, res) => function __init() {
14
14
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
@@ -94,19 +94,47 @@ var init_classroomDB = __esm({
94
94
  }
95
95
  });
96
96
 
97
- // src/factory.ts
98
- var ENV;
99
- var init_factory = __esm({
100
- "src/factory.ts"() {
97
+ // src/impl/common/SyncStrategy.ts
98
+ var init_SyncStrategy = __esm({
99
+ "src/impl/common/SyncStrategy.ts"() {
100
+ "use strict";
101
+ }
102
+ });
103
+
104
+ // src/core/types/types-legacy.ts
105
+ var GuestUsername, DocTypePrefixes;
106
+ var init_types_legacy = __esm({
107
+ "src/core/types/types-legacy.ts"() {
101
108
  "use strict";
102
109
  init_logger();
103
- ENV = {
104
- COUCHDB_SERVER_PROTOCOL: "NOT_SET",
105
- COUCHDB_SERVER_URL: "NOT_SET"
110
+ GuestUsername = "Guest";
111
+ DocTypePrefixes = {
112
+ ["CARD" /* CARD */]: "c",
113
+ ["DISPLAYABLE_DATA" /* DISPLAYABLE_DATA */]: "dd",
114
+ ["TAG" /* TAG */]: "TAG",
115
+ ["CARDRECORD" /* CARDRECORD */]: "cardH",
116
+ ["SCHEDULED_CARD" /* SCHEDULED_CARD */]: "card_review_",
117
+ // Add other doctypes here as they get prefixed IDs
118
+ ["DATASHAPE" /* DATASHAPE */]: "DATASHAPE",
119
+ ["QUESTION" /* QUESTIONTYPE */]: "QUESTION",
120
+ ["VIEW" /* VIEW */]: "VIEW",
121
+ ["PEDAGOGY" /* PEDAGOGY */]: "PEDAGOGY",
122
+ ["NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */]: "NAVIGATION_STRATEGY"
106
123
  };
107
124
  }
108
125
  });
109
126
 
127
+ // src/core/util/index.ts
128
+ function getCardHistoryID(courseID, cardID) {
129
+ return `${DocTypePrefixes["CARDRECORD" /* CARDRECORD */]}-${courseID}-${cardID}`;
130
+ }
131
+ var init_util = __esm({
132
+ "src/core/util/index.ts"() {
133
+ "use strict";
134
+ init_types_legacy();
135
+ }
136
+ });
137
+
110
138
  // src/impl/couch/pouchdb-setup.ts
111
139
  var import_pouchdb, import_pouchdb_find, import_pouchdb_authentication, pouchdb_setup_default;
112
140
  var init_pouchdb_setup = __esm({
@@ -126,51 +154,94 @@ var init_pouchdb_setup = __esm({
126
154
  }
127
155
  });
128
156
 
129
- // src/core/types/types-legacy.ts
130
- var GuestUsername, DocType, cardHistoryPrefix;
131
- var init_types_legacy = __esm({
132
- "src/core/types/types-legacy.ts"() {
157
+ // src/util/tuiLogger.ts
158
+ var init_tuiLogger = __esm({
159
+ "src/util/tuiLogger.ts"() {
133
160
  "use strict";
134
- init_logger();
135
- GuestUsername = "Guest";
136
- DocType = /* @__PURE__ */ ((DocType2) => {
137
- DocType2["DISPLAYABLE_DATA"] = "DISPLAYABLE_DATA";
138
- DocType2["CARD"] = "CARD";
139
- DocType2["DATASHAPE"] = "DATASHAPE";
140
- DocType2["QUESTIONTYPE"] = "QUESTION";
141
- DocType2["VIEW"] = "VIEW";
142
- DocType2["PEDAGOGY"] = "PEDAGOGY";
143
- DocType2["CARDRECORD"] = "CARDRECORD";
144
- DocType2["SCHEDULED_CARD"] = "SCHEDULED_CARD";
145
- DocType2["TAG"] = "TAG";
146
- DocType2["NAVIGATION_STRATEGY"] = "NAVIGATION_STRATEGY";
147
- return DocType2;
148
- })(DocType || {});
149
- cardHistoryPrefix = "cardH";
161
+ init_dataDirectory();
150
162
  }
151
163
  });
152
164
 
153
- // src/impl/couch/courseLookupDB.ts
154
- var init_courseLookupDB = __esm({
155
- "src/impl/couch/courseLookupDB.ts"() {
165
+ // src/util/dataDirectory.ts
166
+ function getAppDataDirectory() {
167
+ return path.join(os.homedir(), ".tuilder");
168
+ }
169
+ function getDbPath(dbName) {
170
+ return path.join(getAppDataDirectory(), dbName);
171
+ }
172
+ var path, os;
173
+ var init_dataDirectory = __esm({
174
+ "src/util/dataDirectory.ts"() {
156
175
  "use strict";
157
- init_pouchdb_setup();
158
- init_factory();
159
- init_logger();
160
- logger.debug(`COURSELOOKUP FILE RUNNING`);
176
+ path = __toESM(require("path"));
177
+ os = __toESM(require("os"));
178
+ init_tuiLogger();
161
179
  }
162
180
  });
163
181
 
164
- // src/impl/couch/adminDB.ts
165
- var init_adminDB2 = __esm({
166
- "src/impl/couch/adminDB.ts"() {
182
+ // src/impl/common/userDBHelpers.ts
183
+ function filterAllDocsByPrefix(db, prefix, opts) {
184
+ const options = {
185
+ startkey: prefix,
186
+ endkey: prefix + "\uFFF0",
187
+ include_docs: true
188
+ };
189
+ if (opts) {
190
+ Object.assign(options, opts);
191
+ }
192
+ return db.allDocs(options);
193
+ }
194
+ function getStartAndEndKeys(key) {
195
+ return {
196
+ startkey: key,
197
+ endkey: key + "\uFFF0"
198
+ };
199
+ }
200
+ function getLocalUserDB(username) {
201
+ const dbName = `userdb-${username}`;
202
+ if (typeof window === "undefined") {
203
+ return new pouchdb_setup_default(getDbPath(dbName), {});
204
+ } else {
205
+ return new pouchdb_setup_default(dbName, {});
206
+ }
207
+ }
208
+ function scheduleCardReviewLocal(userDB, review) {
209
+ const now = import_moment.default.utc();
210
+ logger.info(`Scheduling for review in: ${review.time.diff(now, "h") / 24} days`);
211
+ void userDB.put({
212
+ _id: DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */] + review.time.format(REVIEW_TIME_FORMAT),
213
+ cardId: review.card_id,
214
+ reviewTime: review.time.toISOString(),
215
+ courseId: review.course_id,
216
+ scheduledAt: now.toISOString(),
217
+ scheduledFor: review.scheduledFor,
218
+ schedulingAgentId: review.schedulingAgentId
219
+ });
220
+ }
221
+ async function removeScheduledCardReviewLocal(userDB, reviewDocID) {
222
+ const reviewDoc = await userDB.get(reviewDocID);
223
+ userDB.remove(reviewDoc).then((res) => {
224
+ if (res.ok) {
225
+ log(`Removed Review Doc: ${reviewDocID}`);
226
+ }
227
+ }).catch((err) => {
228
+ log(`Failed to remove Review Doc: ${reviewDocID},
229
+ ${JSON.stringify(err)}`);
230
+ });
231
+ }
232
+ var import_moment, REVIEW_TIME_FORMAT, log;
233
+ var init_userDBHelpers = __esm({
234
+ "src/impl/common/userDBHelpers.ts"() {
167
235
  "use strict";
168
- init_pouchdb_setup();
169
- init_factory();
170
- init_couch();
171
- init_classroomDB2();
172
- init_courseLookupDB();
236
+ import_moment = __toESM(require("moment"));
237
+ init_core();
173
238
  init_logger();
239
+ init_pouchdb_setup();
240
+ init_dataDirectory();
241
+ REVIEW_TIME_FORMAT = "YYYY-MM-DD--kk:mm:ss-SSS";
242
+ log = (s) => {
243
+ logger.info(s);
244
+ };
174
245
  }
175
246
  });
176
247
 
@@ -201,7 +272,10 @@ var init_updateQueue = __esm({
201
272
  _className = "UpdateQueue";
202
273
  pendingUpdates = {};
203
274
  inprogressUpdates = {};
204
- db;
275
+ readDB;
276
+ // Database for read operations
277
+ writeDB;
278
+ // Database for write operations (local-first)
205
279
  update(id, update) {
206
280
  logger.debug(`Update requested on doc: ${id}`);
207
281
  if (this.pendingUpdates[id]) {
@@ -211,24 +285,25 @@ var init_updateQueue = __esm({
211
285
  }
212
286
  return this.applyUpdates(id);
213
287
  }
214
- constructor(db) {
288
+ constructor(readDB, writeDB) {
215
289
  super();
216
- this.db = db;
290
+ this.readDB = readDB;
291
+ this.writeDB = writeDB || readDB;
217
292
  logger.debug(`UpdateQ initialized...`);
218
- void this.db.info().then((i) => {
293
+ void this.readDB.info().then((i) => {
219
294
  logger.debug(`db info: ${JSON.stringify(i)}`);
220
295
  });
221
296
  }
222
297
  async applyUpdates(id) {
223
298
  logger.debug(`Applying updates on doc: ${id}`);
224
299
  if (this.inprogressUpdates[id]) {
225
- await this.db.info();
300
+ await this.readDB.info();
226
301
  return this.applyUpdates(id);
227
302
  } else {
228
303
  if (this.pendingUpdates[id] && this.pendingUpdates[id].length > 0) {
229
304
  this.inprogressUpdates[id] = true;
230
305
  try {
231
- let doc = await this.db.get(id);
306
+ let doc = await this.readDB.get(id);
232
307
  logger.debug(`Retrieved doc: ${id}`);
233
308
  while (this.pendingUpdates[id].length !== 0) {
234
309
  const update = this.pendingUpdates[id].splice(0, 1)[0];
@@ -241,7 +316,7 @@ var init_updateQueue = __esm({
241
316
  };
242
317
  }
243
318
  }
244
- await this.db.put(doc);
319
+ await this.writeDB.put(doc);
245
320
  logger.debug(`Put doc: ${id}`);
246
321
  if (this.pendingUpdates[id].length === 0) {
247
322
  this.inprogressUpdates[id] = false;
@@ -267,22 +342,113 @@ var init_updateQueue = __esm({
267
342
  }
268
343
  });
269
344
 
345
+ // src/impl/couch/user-course-relDB.ts
346
+ var import_moment2, UsrCrsData;
347
+ var init_user_course_relDB = __esm({
348
+ "src/impl/couch/user-course-relDB.ts"() {
349
+ "use strict";
350
+ import_moment2 = __toESM(require("moment"));
351
+ init_logger();
352
+ UsrCrsData = class {
353
+ user;
354
+ _courseId;
355
+ constructor(user, courseId) {
356
+ this.user = user;
357
+ this._courseId = courseId;
358
+ }
359
+ async getReviewsForcast(daysCount) {
360
+ const time = import_moment2.default.utc().add(daysCount, "days");
361
+ return this.getReviewstoDate(time);
362
+ }
363
+ async getPendingReviews() {
364
+ const now = import_moment2.default.utc();
365
+ return this.getReviewstoDate(now);
366
+ }
367
+ async getScheduledReviewCount() {
368
+ return (await this.getPendingReviews()).length;
369
+ }
370
+ async getCourseSettings() {
371
+ const regDoc = await this.user.getCourseRegistrationsDoc();
372
+ const crsDoc = regDoc.courses.find((c) => c.courseID === this._courseId);
373
+ if (crsDoc && crsDoc.settings) {
374
+ return crsDoc.settings;
375
+ } else {
376
+ logger.warn(`no settings found during lookup on course ${this._courseId}`);
377
+ return {};
378
+ }
379
+ }
380
+ updateCourseSettings(updates) {
381
+ if ("updateCourseSettings" in this.user) {
382
+ void this.user.updateCourseSettings(this._courseId, updates);
383
+ }
384
+ }
385
+ async getReviewstoDate(targetDate) {
386
+ const allReviews = await this.user.getPendingReviews(this._courseId);
387
+ logger.debug(
388
+ `Fetching ${this.user.getUsername()}'s scheduled reviews for course ${this._courseId}.`
389
+ );
390
+ return allReviews.filter((review) => {
391
+ const reviewTime = import_moment2.default.utc(review.reviewTime);
392
+ return targetDate.isAfter(reviewTime);
393
+ });
394
+ }
395
+ };
396
+ }
397
+ });
398
+
270
399
  // src/impl/couch/clientCache.ts
271
- async function GET_CACHED(k, f) {
272
- if (CLIENT_CACHE[k]) {
273
- return CLIENT_CACHE[k];
400
+ var init_clientCache = __esm({
401
+ "src/impl/couch/clientCache.ts"() {
402
+ "use strict";
403
+ }
404
+ });
405
+
406
+ // src/impl/couch/courseAPI.ts
407
+ async function getCredentialledCourseConfig(courseID) {
408
+ try {
409
+ const db = getCourseDB(courseID);
410
+ const ret = await db.get("CourseConfig");
411
+ ret.courseID = courseID;
412
+ logger.info(`Returning course config: ${JSON.stringify(ret)}`);
413
+ return ret;
414
+ } catch (e) {
415
+ logger.error(`Error fetching config for ${courseID}:`, e);
416
+ throw e;
274
417
  }
275
- CLIENT_CACHE[k] = f ? await f(k) : await GET_ITEM(k);
276
- return GET_CACHED(k);
277
418
  }
278
- async function GET_ITEM(k) {
279
- throw new Error(`No implementation found for GET_CACHED(${k})`);
419
+ function getCourseDB(courseID) {
420
+ const dbName = `coursedb-${courseID}`;
421
+ return new pouchdb_setup_default(
422
+ ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + dbName,
423
+ pouchDBincludeCredentialsConfig
424
+ );
280
425
  }
281
- var CLIENT_CACHE;
282
- var init_clientCache = __esm({
283
- "src/impl/couch/clientCache.ts"() {
426
+ var import_common, import_common2, import_common3, import_uuid;
427
+ var init_courseAPI = __esm({
428
+ "src/impl/couch/courseAPI.ts"() {
429
+ "use strict";
430
+ init_pouchdb_setup();
431
+ init_couch();
432
+ init_factory();
433
+ import_common = require("@vue-skuilder/common");
434
+ import_common2 = require("@vue-skuilder/common");
435
+ init_courseDB();
436
+ init_types_legacy();
437
+ import_common3 = require("@vue-skuilder/common");
438
+ init_common();
439
+ init_logger();
440
+ import_uuid = require("uuid");
441
+ }
442
+ });
443
+
444
+ // src/impl/couch/courseLookupDB.ts
445
+ var init_courseLookupDB = __esm({
446
+ "src/impl/couch/courseLookupDB.ts"() {
284
447
  "use strict";
285
- CLIENT_CACHE = {};
448
+ init_pouchdb_setup();
449
+ init_factory();
450
+ init_logger();
451
+ logger.debug(`COURSELOOKUP FILE RUNNING`);
286
452
  }
287
453
  });
288
454
 
@@ -406,86 +572,11 @@ var init_navigators = __esm({
406
572
  });
407
573
 
408
574
  // src/impl/couch/courseDB.ts
409
- function randIntWeightedTowardZero(n) {
410
- return Math.floor(Math.random() * Math.random() * Math.random() * n);
411
- }
412
- async function getCourseTagStubs(courseID) {
413
- logger.debug(`Getting tag stubs for course: ${courseID}`);
414
- const stubs = await filterAllDocsByPrefix(
415
- getCourseDB(courseID),
416
- "TAG" /* TAG */.valueOf() + "-"
417
- );
418
- stubs.rows.forEach((row) => {
419
- logger.debug(` Tag stub for doc: ${row.id}`);
420
- });
421
- return stubs;
422
- }
423
- async function createTag(courseID, tagName, author) {
424
- logger.debug(`Creating tag: ${tagName}...`);
425
- const tagID = getTagID(tagName);
426
- const courseDB = getCourseDB(courseID);
427
- const resp = await courseDB.put({
428
- course: courseID,
429
- docType: "TAG" /* TAG */,
430
- name: tagName,
431
- snippet: "",
432
- taggedCards: [],
433
- wiki: "",
434
- author,
435
- _id: tagID
436
- });
437
- return resp;
438
- }
439
- async function updateTag(tag) {
440
- const prior = await getTag(tag.course, tag.name);
441
- return await getCourseDB(tag.course).put({
442
- ...tag,
443
- _rev: prior._rev
444
- });
445
- }
446
- async function getTag(courseID, tagName) {
447
- const tagID = getTagID(tagName);
448
- const courseDB = getCourseDB(courseID);
449
- return courseDB.get(tagID);
450
- }
451
- async function removeTagFromCard(courseID, cardID, tagID) {
452
- tagID = getTagID(tagID);
453
- const courseDB = getCourseDB(courseID);
454
- const tag = await courseDB.get(tagID);
455
- tag.taggedCards = tag.taggedCards.filter((taggedID) => {
456
- return cardID !== taggedID;
457
- });
458
- return courseDB.put(tag);
459
- }
460
- async function getAppliedTags(id_course, id_card) {
461
- const db = getCourseDB(id_course);
462
- const result = await db.query("getTags", {
463
- startkey: id_card,
464
- endkey: id_card
465
- // include_docs: true
466
- });
467
- return result;
468
- }
469
- async function updateCredentialledCourseConfig(courseID, config) {
470
- logger.debug(`Updating course config:
471
-
472
- ${JSON.stringify(config)}
473
- `);
474
- const db = getCourseDB(courseID);
475
- const old = await getCredentialledCourseConfig(courseID);
476
- return await db.put({
477
- ...config,
478
- _rev: old._rev
479
- });
480
- }
481
- function isSuccessRow(row) {
482
- return "doc" in row && row.doc !== null && row.doc !== void 0;
483
- }
484
- var import_common, CourseDB;
575
+ var import_common5;
485
576
  var init_courseDB = __esm({
486
577
  "src/impl/couch/courseDB.ts"() {
487
578
  "use strict";
488
- import_common = require("@vue-skuilder/common");
579
+ import_common5 = require("@vue-skuilder/common");
489
580
  init_couch();
490
581
  init_updateQueue();
491
582
  init_types_legacy();
@@ -494,560 +585,89 @@ var init_courseDB = __esm({
494
585
  init_courseAPI();
495
586
  init_courseLookupDB();
496
587
  init_navigators();
497
- CourseDB = class {
498
- // private log(msg: string): void {
499
- // log(`CourseLog: ${this.id}\n ${msg}`);
500
- // }
501
- db;
502
- id;
503
- _getCurrentUser;
504
- updateQueue;
505
- constructor(id, userLookup) {
506
- this.id = id;
507
- this.db = getCourseDB(this.id);
508
- this._getCurrentUser = userLookup;
509
- this.updateQueue = new UpdateQueue(this.db);
510
- }
511
- getCourseID() {
512
- return this.id;
513
- }
514
- async getCourseInfo() {
515
- const cardCount = (await this.db.find({
516
- selector: {
517
- docType: "CARD" /* CARD */
518
- },
519
- limit: 1e3
520
- })).docs.length;
521
- return {
522
- cardCount,
523
- registeredUsers: 0
524
- };
525
- }
526
- async getInexperiencedCards(limit = 2) {
527
- return (await this.db.query("cardsByInexperience", {
528
- limit
529
- })).rows.map((r) => {
530
- const ret = {
531
- courseId: this.id,
532
- cardId: r.id,
533
- count: r.key,
534
- elo: r.value
535
- };
536
- return ret;
537
- });
538
- }
539
- async getCardsByEloLimits(options = {
540
- low: 0,
541
- high: Number.MIN_SAFE_INTEGER,
542
- limit: 25,
543
- page: 0
544
- }) {
545
- return (await this.db.query("elo", {
546
- startkey: options.low,
547
- endkey: options.high,
548
- limit: options.limit,
549
- skip: options.limit * options.page
550
- })).rows.map((r) => {
551
- return `${this.id}-${r.id}-${r.key}`;
552
- });
553
- }
554
- async getCardEloData(id) {
555
- const docs = await this.db.allDocs({
556
- keys: id,
557
- include_docs: true
558
- });
559
- const ret = [];
560
- docs.rows.forEach((r) => {
561
- if (isSuccessRow(r)) {
562
- if (r.doc && r.doc.elo) {
563
- ret.push((0, import_common.toCourseElo)(r.doc.elo));
564
- } else {
565
- logger.warn("no elo data for card: " + r.id);
566
- ret.push((0, import_common.blankCourseElo)());
567
- }
568
- } else {
569
- logger.warn("no elo data for card: " + JSON.stringify(r));
570
- ret.push((0, import_common.blankCourseElo)());
571
- }
572
- });
573
- return ret;
574
- }
575
- /**
576
- * Returns the lowest and highest `global` ELO ratings in the course
577
- */
578
- async getELOBounds() {
579
- const [low, high] = await Promise.all([
580
- (await this.db.query("elo", {
581
- startkey: 0,
582
- limit: 1,
583
- include_docs: false
584
- })).rows[0].key,
585
- (await this.db.query("elo", {
586
- limit: 1,
587
- descending: true,
588
- startkey: 1e5
589
- })).rows[0].key
590
- ]);
591
- return {
592
- low,
593
- high
594
- };
595
- }
596
- async removeCard(id) {
597
- const doc = await this.db.get(id);
598
- if (!doc.docType || !(doc.docType === "CARD" /* CARD */)) {
599
- throw new Error(`failed to remove ${id} from course ${this.id}. id does not point to a card`);
600
- }
601
- return this.db.remove(doc);
602
- }
603
- async getCardDisplayableDataIDs(id) {
604
- logger.debug(id.join(", "));
605
- const cards = await this.db.allDocs({
606
- keys: id,
607
- include_docs: true
608
- });
609
- const ret = {};
610
- cards.rows.forEach((r) => {
611
- if (isSuccessRow(r)) {
612
- ret[r.id] = r.doc.id_displayable_data;
613
- }
614
- });
615
- await Promise.all(
616
- cards.rows.map((r) => {
617
- return async () => {
618
- if (isSuccessRow(r)) {
619
- ret[r.id] = r.doc.id_displayable_data;
620
- }
621
- };
622
- })
623
- );
624
- return ret;
625
- }
626
- async getCardsByELO(elo, cardLimit) {
627
- elo = parseInt(elo);
628
- const limit = cardLimit ? cardLimit : 25;
629
- const below = await this.db.query("elo", {
630
- limit: Math.ceil(limit / 2),
631
- startkey: elo,
632
- descending: true
633
- });
634
- const aboveLimit = limit - below.rows.length;
635
- const above = await this.db.query("elo", {
636
- limit: aboveLimit,
637
- startkey: elo + 1
638
- });
639
- let cards = below.rows;
640
- cards = cards.concat(above.rows);
641
- const ret = cards.sort((a, b) => {
642
- const s = Math.abs(a.key - elo) - Math.abs(b.key - elo);
643
- if (s === 0) {
644
- return Math.random() - 0.5;
645
- } else {
646
- return s;
647
- }
648
- }).map((c) => `${this.id}-${c.id}-${c.key}`);
649
- const str = `below:
650
- ${below.rows.map((r) => ` ${r.id}-${r.key}
651
- `)}
652
-
653
- above:
654
- ${above.rows.map((r) => ` ${r.id}-${r.key}
655
- `)}`;
656
- logger.debug(`Getting ${limit} cards centered around elo: ${elo}:
588
+ }
589
+ });
657
590
 
658
- ` + str);
659
- return ret;
660
- }
661
- async getCourseConfig() {
662
- const ret = await getCredentialledCourseConfig(this.id);
663
- if (ret) {
664
- return ret;
665
- } else {
666
- throw new Error(`Course config not found for course ID: ${this.id}`);
667
- }
668
- }
669
- async updateCourseConfig(cfg) {
670
- logger.debug(`Updating: ${JSON.stringify(cfg)}`);
671
- try {
672
- return await updateCredentialledCourseConfig(this.id, cfg);
673
- } catch (error) {
674
- logger.error(`Error updating course config in course DB: ${error}`);
675
- throw error;
676
- }
677
- }
678
- async updateCardElo(cardId, elo) {
679
- if (!elo) {
680
- throw new Error(`Cannot update card elo with null or undefined value for card ID: ${cardId}`);
681
- }
682
- try {
683
- const result = await this.updateQueue.update(cardId, (card) => {
684
- logger.debug(`Replacing ${JSON.stringify(card.elo)} with ${JSON.stringify(elo)}`);
685
- card.elo = elo;
686
- return card;
687
- });
688
- return { ok: true, id: cardId, rev: result._rev };
689
- } catch (error) {
690
- logger.error(`Failed to update card elo for card ID: ${cardId}`, error);
691
- throw new Error(`Failed to update card elo for card ID: ${cardId}`);
692
- }
693
- }
694
- async getAppliedTags(cardId) {
695
- const ret = await getAppliedTags(this.id, cardId);
696
- if (ret) {
697
- return ret;
698
- } else {
699
- throw new Error(`Failed to find tags for card ${this.id}-${cardId}`);
700
- }
701
- }
702
- async addTagToCard(cardId, tagId, updateELO) {
703
- return await addTagToCard(this.id, cardId, tagId, (await this._getCurrentUser()).getUsername(), updateELO);
704
- }
705
- async removeTagFromCard(cardId, tagId) {
706
- return await removeTagFromCard(this.id, cardId, tagId);
707
- }
708
- async createTag(name, author) {
709
- return await createTag(this.id, name, author);
710
- }
711
- async getTag(tagId) {
712
- return await getTag(this.id, tagId);
713
- }
714
- async updateTag(tag) {
715
- if (tag.course !== this.id) {
716
- throw new Error(`Tag ${JSON.stringify(tag)} does not belong to course ${this.id}`);
717
- }
718
- return await updateTag(tag);
719
- }
720
- async getCourseTagStubs() {
721
- return getCourseTagStubs(this.id);
722
- }
723
- async addNote(codeCourse, shape, data, author, tags, uploads, elo = (0, import_common.blankCourseElo)()) {
724
- try {
725
- const resp = await addNote55(this.id, codeCourse, shape, data, author, tags, uploads, elo);
726
- if (resp.ok) {
727
- if (resp.cardCreationFailed) {
728
- logger.warn(
729
- `[courseDB.addNote] Note added but card creation failed: ${resp.cardCreationError}`
730
- );
731
- return {
732
- status: import_common.Status.error,
733
- message: `Note was added but no cards were created: ${resp.cardCreationError}`,
734
- id: resp.id
735
- };
736
- }
737
- return {
738
- status: import_common.Status.ok,
739
- message: "",
740
- id: resp.id
741
- };
742
- } else {
743
- return {
744
- status: import_common.Status.error,
745
- message: "Unexpected error adding note"
746
- };
747
- }
748
- } catch (e) {
749
- const err = e;
750
- logger.error(
751
- `[addNote] error ${err.name}
752
- reason: ${err.reason}
753
- message: ${err.message}`
754
- );
755
- return {
756
- status: import_common.Status.error,
757
- message: `Error adding note to course. ${e.reason || err.message}`
758
- };
759
- }
760
- }
761
- async getCourseDoc(id, options) {
762
- return await getCourseDoc(this.id, id, options);
763
- }
764
- async getCourseDocs(ids, options = {}) {
765
- return await getCourseDocs(this.id, ids, options);
766
- }
767
- ////////////////////////////////////
768
- // NavigationStrategyManager implementation
769
- ////////////////////////////////////
770
- getNavigationStrategy(id) {
771
- logger.debug(`[courseDB] Getting navigation strategy: ${id}`);
772
- const strategy = {
773
- id: "ELO",
774
- docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
775
- name: "ELO",
776
- description: "ELO-based navigation strategy for ordering content by difficulty",
777
- implementingClass: "elo" /* ELO */,
778
- course: this.id,
779
- serializedData: ""
780
- // serde is a noop for ELO navigator.
781
- };
782
- return Promise.resolve(strategy);
783
- }
784
- getAllNavigationStrategies() {
785
- logger.debug("[courseDB] Returning hard-coded navigation strategies");
786
- const strategies = [
787
- {
788
- id: "ELO",
789
- docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
790
- name: "ELO",
791
- description: "ELO-based navigation strategy for ordering content by difficulty",
792
- implementingClass: "elo" /* ELO */,
793
- course: this.id,
794
- serializedData: ""
795
- // serde is a noop for ELO navigator.
796
- }
797
- ];
798
- return Promise.resolve(strategies);
799
- }
800
- addNavigationStrategy(data) {
801
- logger.debug(`[courseDB] Adding navigation strategy: ${data.id}`);
802
- logger.debug(JSON.stringify(data));
803
- return Promise.resolve();
804
- }
805
- updateNavigationStrategy(id, data) {
806
- logger.debug(`[courseDB] Updating navigation strategy: ${id}`);
807
- logger.debug(JSON.stringify(data));
808
- return Promise.resolve();
809
- }
810
- async surfaceNavigationStrategy() {
811
- logger.warn(`Returning hard-coded default ELO navigator`);
812
- const ret = {
813
- id: "ELO",
814
- docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
815
- name: "ELO",
816
- description: "ELO-based navigation strategy",
817
- implementingClass: "elo" /* ELO */,
818
- course: this.id,
819
- serializedData: ""
820
- // serde is a noop for ELO navigator.
821
- };
822
- return Promise.resolve(ret);
823
- }
824
- ////////////////////////////////////
825
- // END NavigationStrategyManager implementation
826
- ////////////////////////////////////
827
- ////////////////////////////////////
828
- // StudyContentSource implementation
829
- ////////////////////////////////////
830
- async getNewCards(limit = 99) {
831
- const u = await this._getCurrentUser();
832
- try {
833
- const strategy = await this.surfaceNavigationStrategy();
834
- const navigator = await ContentNavigator.create(u, this, strategy);
835
- return navigator.getNewCards(limit);
836
- } catch (e) {
837
- logger.error(`[courseDB] Error surfacing a NavigationStrategy: ${e}`);
838
- throw e;
839
- }
840
- }
841
- async getPendingReviews() {
842
- const u = await this._getCurrentUser();
843
- try {
844
- const strategy = await this.surfaceNavigationStrategy();
845
- const navigator = await ContentNavigator.create(u, this, strategy);
846
- return navigator.getPendingReviews();
847
- } catch (e) {
848
- logger.error(`[courseDB] Error surfacing a NavigationStrategy: ${e}`);
849
- throw e;
850
- }
851
- }
852
- async getCardsCenteredAtELO(options = {
853
- limit: 99,
854
- elo: "user"
855
- }, filter) {
856
- let targetElo;
857
- if (options.elo === "user") {
858
- const u = await this._getCurrentUser();
859
- targetElo = -1;
860
- try {
861
- const courseDoc = (await u.getCourseRegistrationsDoc()).courses.find((c) => {
862
- return c.courseID === this.id;
863
- });
864
- targetElo = (0, import_common.EloToNumber)(courseDoc.elo);
865
- } catch {
866
- targetElo = 1e3;
867
- }
868
- } else if (options.elo === "random") {
869
- const bounds = await GET_CACHED(`elo-bounds-${this.id}`, () => this.getELOBounds());
870
- targetElo = Math.round(bounds.low + Math.random() * (bounds.high - bounds.low));
871
- } else {
872
- targetElo = options.elo;
873
- }
874
- let cards = [];
875
- let mult = 4;
876
- let previousCount = -1;
877
- let newCount = 0;
878
- while (cards.length < options.limit && newCount !== previousCount) {
879
- cards = await this.getCardsByELO(targetElo, mult * options.limit);
880
- previousCount = newCount;
881
- newCount = cards.length;
882
- logger.debug(`Found ${cards.length} elo neighbor cards...`);
883
- if (filter) {
884
- cards = cards.filter(filter);
885
- logger.debug(`Filtered to ${cards.length} cards...`);
886
- }
887
- mult *= 2;
888
- }
889
- const selectedCards = [];
890
- while (selectedCards.length < options.limit && cards.length > 0) {
891
- const index = randIntWeightedTowardZero(cards.length);
892
- const card = cards.splice(index, 1)[0];
893
- selectedCards.push(card);
894
- }
895
- return selectedCards.map((c) => {
896
- const split = c.split("-");
897
- return {
898
- courseID: this.id,
899
- cardID: split[1],
900
- qualifiedID: `${split[0]}-${split[1]}`,
901
- contentSourceType: "course",
902
- contentSourceID: this.id,
903
- status: "new"
904
- };
905
- });
906
- }
907
- };
591
+ // src/impl/couch/classroomDB.ts
592
+ var import_moment3;
593
+ var init_classroomDB2 = __esm({
594
+ "src/impl/couch/classroomDB.ts"() {
595
+ "use strict";
596
+ init_factory();
597
+ init_logger();
598
+ import_moment3 = __toESM(require("moment"));
599
+ init_pouchdb_setup();
600
+ init_couch();
601
+ init_courseDB();
908
602
  }
909
603
  });
910
604
 
911
- // src/impl/common/SyncStrategy.ts
912
- var init_SyncStrategy = __esm({
913
- "src/impl/common/SyncStrategy.ts"() {
605
+ // src/impl/couch/adminDB.ts
606
+ var init_adminDB2 = __esm({
607
+ "src/impl/couch/adminDB.ts"() {
914
608
  "use strict";
609
+ init_pouchdb_setup();
610
+ init_factory();
611
+ init_couch();
612
+ init_classroomDB2();
613
+ init_courseLookupDB();
614
+ init_logger();
915
615
  }
916
616
  });
917
617
 
918
- // src/core/util/index.ts
919
- function getCardHistoryID(courseID, cardID) {
920
- return `${cardHistoryPrefix}-${courseID}-${cardID}`;
921
- }
922
- var init_util = __esm({
923
- "src/core/util/index.ts"() {
618
+ // src/impl/couch/auth.ts
619
+ var init_auth = __esm({
620
+ "src/impl/couch/auth.ts"() {
924
621
  "use strict";
622
+ init_factory();
925
623
  init_types_legacy();
624
+ init_logger();
926
625
  }
927
626
  });
928
627
 
929
- // src/impl/common/userDBHelpers.ts
930
- function filterAllDocsByPrefix2(db, prefix, opts) {
931
- const options = {
932
- startkey: prefix,
933
- endkey: prefix + "\uFFF0",
934
- include_docs: true
935
- };
936
- if (opts) {
937
- Object.assign(options, opts);
938
- }
939
- return db.allDocs(options);
940
- }
941
- function getStartAndEndKeys2(key) {
942
- return {
943
- startkey: key,
944
- endkey: key + "\uFFF0"
945
- };
946
- }
947
- function getLocalUserDB(username) {
948
- return new pouchdb_setup_default(`userdb-${username}`, {});
949
- }
950
- function scheduleCardReviewLocal(userDB, review) {
951
- const now = import_moment.default.utc();
952
- logger.info(`Scheduling for review in: ${review.time.diff(now, "h") / 24} days`);
953
- void userDB.put({
954
- _id: REVIEW_PREFIX + review.time.format(REVIEW_TIME_FORMAT),
955
- cardId: review.card_id,
956
- reviewTime: review.time,
957
- courseId: review.course_id,
958
- scheduledAt: now,
959
- scheduledFor: review.scheduledFor,
960
- schedulingAgentId: review.schedulingAgentId
961
- });
962
- }
963
- async function removeScheduledCardReviewLocal(userDB, reviewDocID) {
964
- const reviewDoc = await userDB.get(reviewDocID);
965
- userDB.remove(reviewDoc).then((res) => {
966
- if (res.ok) {
967
- log(`Removed Review Doc: ${reviewDocID}`);
968
- }
969
- }).catch((err) => {
970
- log(`Failed to remove Review Doc: ${reviewDocID},
971
- ${JSON.stringify(err)}`);
972
- });
973
- }
974
- var import_moment, REVIEW_PREFIX, REVIEW_TIME_FORMAT, log;
975
- var init_userDBHelpers = __esm({
976
- "src/impl/common/userDBHelpers.ts"() {
628
+ // src/impl/couch/CouchDBSyncStrategy.ts
629
+ var import_common6;
630
+ var init_CouchDBSyncStrategy = __esm({
631
+ "src/impl/couch/CouchDBSyncStrategy.ts"() {
977
632
  "use strict";
978
- import_moment = __toESM(require("moment"));
633
+ init_factory();
634
+ init_types_legacy();
979
635
  init_logger();
636
+ import_common6 = require("@vue-skuilder/common");
637
+ init_common();
980
638
  init_pouchdb_setup();
981
- REVIEW_PREFIX = "card_review_";
982
- REVIEW_TIME_FORMAT = "YYYY-MM-DD--kk:mm:ss-SSS";
983
- log = (s) => {
984
- logger.info(s);
985
- };
639
+ init_couch();
640
+ init_auth();
986
641
  }
987
642
  });
988
643
 
989
- // src/impl/couch/user-course-relDB.ts
990
- var import_moment2, UsrCrsData;
991
- var init_user_course_relDB = __esm({
992
- "src/impl/couch/user-course-relDB.ts"() {
644
+ // src/impl/couch/index.ts
645
+ var import_moment4, import_process, isBrowser, GUEST_LOCAL_DB, localUserDB, pouchDBincludeCredentialsConfig;
646
+ var init_couch = __esm({
647
+ "src/impl/couch/index.ts"() {
993
648
  "use strict";
994
- import_moment2 = __toESM(require("moment"));
995
- init_couch();
996
- init_courseDB();
649
+ init_factory();
650
+ init_types_legacy();
651
+ import_moment4 = __toESM(require("moment"));
997
652
  init_logger();
998
- UsrCrsData = class {
999
- user;
1000
- course;
1001
- _courseId;
1002
- constructor(user, courseId) {
1003
- this.user = user;
1004
- this.course = new CourseDB(courseId, async () => this.user);
1005
- this._courseId = courseId;
1006
- }
1007
- async getReviewsForcast(daysCount) {
1008
- const time = import_moment2.default.utc().add(daysCount, "days");
1009
- return this.getReviewstoDate(time);
1010
- }
1011
- async getPendingReviews() {
1012
- const now = import_moment2.default.utc();
1013
- return this.getReviewstoDate(now);
1014
- }
1015
- async getScheduledReviewCount() {
1016
- return (await this.getPendingReviews()).length;
1017
- }
1018
- async getCourseSettings() {
1019
- const regDoc = await this.user.getCourseRegistrationsDoc();
1020
- const crsDoc = regDoc.courses.find((c) => c.courseID === this._courseId);
1021
- if (crsDoc && crsDoc.settings) {
1022
- return crsDoc.settings;
1023
- } else {
1024
- logger.warn(`no settings found during lookup on course ${this._courseId}`);
1025
- return {};
1026
- }
1027
- }
1028
- updateCourseSettings(updates) {
1029
- void this.user.updateCourseSettings(this._courseId, updates);
1030
- }
1031
- async getReviewstoDate(targetDate) {
1032
- const keys = getStartAndEndKeys(REVIEW_PREFIX2);
1033
- const reviews = await this.user.remote().allDocs({
1034
- startkey: keys.startkey,
1035
- endkey: keys.endkey,
1036
- include_docs: true
1037
- });
1038
- logger.debug(
1039
- `Fetching ${this.user.getUsername()}'s scheduled reviews for course ${this._courseId}.`
1040
- );
1041
- return reviews.rows.filter((r) => {
1042
- if (r.id.startsWith(REVIEW_PREFIX2)) {
1043
- const date = import_moment2.default.utc(r.id.substr(REVIEW_PREFIX2.length), REVIEW_TIME_FORMAT2);
1044
- if (targetDate.isAfter(date)) {
1045
- if (this._courseId === void 0 || r.doc.courseId === this._courseId) {
1046
- return true;
1047
- }
1048
- }
1049
- }
1050
- }).map((r) => r.doc);
653
+ init_pouchdb_setup();
654
+ import_process = __toESM(require("process"));
655
+ init_contentSource();
656
+ init_adminDB2();
657
+ init_classroomDB2();
658
+ init_courseAPI();
659
+ init_courseDB();
660
+ init_CouchDBSyncStrategy();
661
+ isBrowser = typeof window !== "undefined";
662
+ if (isBrowser) {
663
+ window.process = import_process.default;
664
+ }
665
+ GUEST_LOCAL_DB = `userdb-${GuestUsername}`;
666
+ localUserDB = new pouchdb_setup_default(GUEST_LOCAL_DB);
667
+ pouchDBincludeCredentialsConfig = {
668
+ fetch(url, opts) {
669
+ opts.credentials = "include";
670
+ return pouchdb_setup_default.fetch(url, opts);
1051
671
  }
1052
672
  };
1053
673
  }
@@ -1113,7 +733,7 @@ async function updateUserElo(user, course_id, elo) {
1113
733
  return getLocalUserDB(user).put(regDoc);
1114
734
  }
1115
735
  async function registerUserForClassroom(user, classID, registerAs) {
1116
- log2(`Registering user: ${user} in course: ${classID}`);
736
+ log3(`Registering user: ${user} in course: ${classID}`);
1117
737
  return getOrCreateClassroomRegistrationsDoc(user).then((doc) => {
1118
738
  const regItem = {
1119
739
  classID,
@@ -1124,7 +744,7 @@ async function registerUserForClassroom(user, classID, registerAs) {
1124
744
  }).length === 0) {
1125
745
  doc.registrations.push(regItem);
1126
746
  } else {
1127
- log2(`User ${user} is already registered for class ${classID}`);
747
+ log3(`User ${user} is already registered for class ${classID}`);
1128
748
  }
1129
749
  return getLocalUserDB(user).put(doc);
1130
750
  });
@@ -1146,23 +766,23 @@ async function dropUserFromClassroom(user, classID) {
1146
766
  async function getUserClassrooms(user) {
1147
767
  return getOrCreateClassroomRegistrationsDoc(user);
1148
768
  }
1149
- var import_common2, import_moment3, log2, cardHistoryPrefix2, BaseUser, userCoursesDoc, userClassroomsDoc;
769
+ var import_common8, import_moment5, log3, BaseUser, userCoursesDoc, userClassroomsDoc;
1150
770
  var init_BaseUserDB = __esm({
1151
771
  "src/impl/common/BaseUserDB.ts"() {
1152
772
  "use strict";
773
+ init_core();
1153
774
  init_util();
1154
- import_common2 = require("@vue-skuilder/common");
1155
- import_moment3 = __toESM(require("moment"));
775
+ import_common8 = require("@vue-skuilder/common");
776
+ import_moment5 = __toESM(require("moment"));
1156
777
  init_types_legacy();
1157
778
  init_logger();
1158
779
  init_userDBHelpers();
1159
780
  init_updateQueue();
1160
781
  init_user_course_relDB();
1161
782
  init_couch();
1162
- log2 = (s) => {
783
+ log3 = (s) => {
1163
784
  logger.info(s);
1164
785
  };
1165
- cardHistoryPrefix2 = "cardH-";
1166
786
  BaseUser = class _BaseUser {
1167
787
  static _instance;
1168
788
  static _initialized = false;
@@ -1183,11 +803,13 @@ var init_BaseUserDB = __esm({
1183
803
  isLoggedIn() {
1184
804
  return !this._username.startsWith(GuestUsername);
1185
805
  }
1186
- remoteDB;
1187
806
  remote() {
1188
807
  return this.remoteDB;
1189
808
  }
1190
809
  localDB;
810
+ remoteDB;
811
+ writeDB;
812
+ // Database to use for write operations (local-first approach)
1191
813
  updateQueue;
1192
814
  async createAccount(username, password) {
1193
815
  if (!this.syncStrategy.canCreateAccount()) {
@@ -1200,10 +822,14 @@ Currently logged-in as ${this._username}.`
1200
822
  );
1201
823
  }
1202
824
  const result = await this.syncStrategy.createAccount(username, password);
1203
- if (result.status === import_common2.Status.ok) {
1204
- log2(`Account created successfully, updating username to ${username}`);
825
+ if (result.status === import_common8.Status.ok) {
826
+ log3(`Account created successfully, updating username to ${username}`);
1205
827
  this._username = username;
1206
- localStorage.removeItem("dbUUID");
828
+ try {
829
+ localStorage.removeItem("dbUUID");
830
+ } catch (e) {
831
+ logger.warn("localStorage not available (Node.js environment):", e);
832
+ }
1207
833
  await this.init();
1208
834
  }
1209
835
  return {
@@ -1215,15 +841,22 @@ Currently logged-in as ${this._username}.`
1215
841
  if (!this.syncStrategy.canAuthenticate()) {
1216
842
  throw new Error("Authentication not supported by current sync strategy");
1217
843
  }
1218
- if (!this._username.startsWith(GuestUsername)) {
1219
- throw new Error(`Cannot change accounts while logged in.
1220
- Log out of account ${this.getUsername()} before logging in as ${username}.`);
844
+ if (!this._username.startsWith(GuestUsername) && this._username != username) {
845
+ if (this._username != username) {
846
+ throw new Error(`Cannot change accounts while logged in.
847
+ Log out of account ${this.getUsername()} before logging in as ${username}.`);
848
+ }
849
+ logger.warn(`User ${this._username} is already logged in, but executing login again.`);
1221
850
  }
1222
851
  const loginResult = await this.syncStrategy.authenticate(username, password);
1223
852
  if (loginResult.ok) {
1224
- log2(`Logged in as ${username}`);
853
+ log3(`Logged in as ${username}`);
1225
854
  this._username = username;
1226
- localStorage.removeItem("dbUUID");
855
+ try {
856
+ localStorage.removeItem("dbUUID");
857
+ } catch (e) {
858
+ logger.warn("localStorage not available (Node.js environment):", e);
859
+ }
1227
860
  await this.init();
1228
861
  }
1229
862
  return loginResult;
@@ -1231,7 +864,7 @@ Currently logged-in as ${this._username}.`
1231
864
  async resetUserData() {
1232
865
  if (this.syncStrategy.canAuthenticate()) {
1233
866
  return {
1234
- status: import_common2.Status.error,
867
+ status: import_common8.Status.error,
1235
868
  error: "Reset user data is only available for local-only mode. Use logout instead for remote sync."
1236
869
  };
1237
870
  }
@@ -1240,8 +873,8 @@ Currently logged-in as ${this._username}.`
1240
873
  const allDocs = await localDB.allDocs({ include_docs: false });
1241
874
  const docsToDelete = allDocs.rows.filter((row) => {
1242
875
  const id = row.id;
1243
- return id.startsWith(cardHistoryPrefix2) || // Card interaction history
1244
- id.startsWith(REVIEW_PREFIX) || // Scheduled reviews
876
+ return id.startsWith(DocTypePrefixes["CARDRECORD" /* CARDRECORD */]) || // Card interaction history
877
+ id.startsWith(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */]) || // Scheduled reviews
1245
878
  id === _BaseUser.DOC_IDS.COURSE_REGISTRATIONS || // Course registrations
1246
879
  id === _BaseUser.DOC_IDS.CLASSROOM_REGISTRATIONS || // Classroom registrations
1247
880
  id === _BaseUser.DOC_IDS.CONFIG;
@@ -1250,11 +883,11 @@ Currently logged-in as ${this._username}.`
1250
883
  await localDB.bulkDocs(docsToDelete);
1251
884
  }
1252
885
  await this.init();
1253
- return { status: import_common2.Status.ok };
886
+ return { status: import_common8.Status.ok };
1254
887
  } catch (error) {
1255
888
  logger.error("Failed to reset user data:", error);
1256
889
  return {
1257
- status: import_common2.Status.error,
890
+ status: import_common8.Status.error,
1258
891
  error: error instanceof Error ? error.message : "Unknown error during reset"
1259
892
  };
1260
893
  }
@@ -1310,7 +943,7 @@ Currently logged-in as ${this._username}.`
1310
943
  *
1311
944
  */
1312
945
  async getActiveCards() {
1313
- const keys = getStartAndEndKeys2(REVIEW_PREFIX);
946
+ const keys = getStartAndEndKeys(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */]);
1314
947
  const reviews = await this.remoteDB.allDocs({
1315
948
  startkey: keys.startkey,
1316
949
  endkey: keys.endkey,
@@ -1382,18 +1015,21 @@ Currently logged-in as ${this._username}.`
1382
1015
  }
1383
1016
  }
1384
1017
  async getReviewstoDate(targetDate, course_id) {
1385
- const keys = getStartAndEndKeys2(REVIEW_PREFIX);
1018
+ const keys = getStartAndEndKeys(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */]);
1386
1019
  const reviews = await this.remoteDB.allDocs({
1387
1020
  startkey: keys.startkey,
1388
1021
  endkey: keys.endkey,
1389
1022
  include_docs: true
1390
1023
  });
1391
- log2(
1024
+ log3(
1392
1025
  `Fetching ${this._username}'s scheduled reviews${course_id ? ` for course ${course_id}` : ""}.`
1393
1026
  );
1394
1027
  return reviews.rows.filter((r) => {
1395
- if (r.id.startsWith(REVIEW_PREFIX)) {
1396
- const date = import_moment3.default.utc(r.id.substr(REVIEW_PREFIX.length), REVIEW_TIME_FORMAT);
1028
+ if (r.id.startsWith(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */])) {
1029
+ const date = import_moment5.default.utc(
1030
+ r.id.substr(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */].length),
1031
+ REVIEW_TIME_FORMAT
1032
+ );
1397
1033
  if (targetDate.isAfter(date)) {
1398
1034
  if (course_id === void 0 || r.doc.courseId === course_id) {
1399
1035
  return true;
@@ -1403,11 +1039,11 @@ Currently logged-in as ${this._username}.`
1403
1039
  }).map((r) => r.doc);
1404
1040
  }
1405
1041
  async getReviewsForcast(daysCount) {
1406
- const time = import_moment3.default.utc().add(daysCount, "days");
1042
+ const time = import_moment5.default.utc().add(daysCount, "days");
1407
1043
  return this.getReviewstoDate(time);
1408
1044
  }
1409
1045
  async getPendingReviews(course_id) {
1410
- const now = import_moment3.default.utc();
1046
+ const now = import_moment5.default.utc();
1411
1047
  return this.getReviewstoDate(now, course_id);
1412
1048
  }
1413
1049
  async getScheduledReviewCount(course_id) {
@@ -1450,12 +1086,12 @@ Currently logged-in as ${this._username}.`
1450
1086
  if (doc.courses.filter((course) => {
1451
1087
  return course.courseID === regItem.courseID;
1452
1088
  }).length === 0) {
1453
- log2(`It's a new course registration!`);
1089
+ log3(`It's a new course registration!`);
1454
1090
  doc.courses.push(regItem);
1455
1091
  doc.studyWeight[course_id] = 1;
1456
1092
  } else {
1457
1093
  doc.courses.forEach((c) => {
1458
- log2(`Found the previously registered course!`);
1094
+ log3(`Found the previously registered course!`);
1459
1095
  if (c.courseID === course_id) {
1460
1096
  c.status = status;
1461
1097
  }
@@ -1463,7 +1099,7 @@ Currently logged-in as ${this._username}.`
1463
1099
  }
1464
1100
  return this.localDB.put(doc);
1465
1101
  }).catch((e) => {
1466
- log2(`Registration failed because of: ${JSON.stringify(e)}`);
1102
+ log3(`Registration failed because of: ${JSON.stringify(e)}`);
1467
1103
  throw e;
1468
1104
  });
1469
1105
  }
@@ -1508,7 +1144,8 @@ Currently logged-in as ${this._username}.`
1508
1144
  const defaultConfig = {
1509
1145
  _id: _BaseUser.DOC_IDS.CONFIG,
1510
1146
  darkMode: false,
1511
- likesConfetti: false
1147
+ likesConfetti: false,
1148
+ sessionTimeLimit: 5
1512
1149
  };
1513
1150
  try {
1514
1151
  const cfg = await this.localDB.get(_BaseUser.DOC_IDS.CONFIG);
@@ -1581,10 +1218,15 @@ Currently logged-in as ${this._username}.`
1581
1218
  setDBandQ() {
1582
1219
  this.localDB = getLocalUserDB(this._username);
1583
1220
  this.remoteDB = this.syncStrategy.setupRemoteDB(this._username);
1584
- this.updateQueue = new UpdateQueue(this.localDB);
1221
+ this.writeDB = this.syncStrategy.getWriteDB ? this.syncStrategy.getWriteDB(this._username) : this.localDB;
1222
+ this.updateQueue = new UpdateQueue(this.localDB, this.writeDB);
1585
1223
  }
1586
1224
  async init() {
1587
1225
  _BaseUser._initialized = false;
1226
+ if (this._username === "admin") {
1227
+ _BaseUser._initialized = true;
1228
+ return;
1229
+ }
1588
1230
  this.setDBandQ();
1589
1231
  this.syncStrategy.startSync(this.localDB, this.remoteDB);
1590
1232
  void this.applyDesignDocs();
@@ -1606,6 +1248,9 @@ Currently logged-in as ${this._username}.`
1606
1248
  }
1607
1249
  ];
1608
1250
  async applyDesignDocs() {
1251
+ if (this._username === "admin") {
1252
+ return;
1253
+ }
1609
1254
  for (const doc of _BaseUser.designDocs) {
1610
1255
  try {
1611
1256
  try {
@@ -1622,7 +1267,7 @@ Currently logged-in as ${this._username}.`
1622
1267
  }
1623
1268
  }
1624
1269
  } catch (error) {
1625
- if (error instanceof Error && error.name === "conflict") {
1270
+ if (error.name && error.name === "conflict") {
1626
1271
  logger.warn(`Design doc ${doc._id} update conflict - will retry`);
1627
1272
  await new Promise((resolve) => setTimeout(resolve, 1e3));
1628
1273
  await this.applyDesignDoc(doc);
@@ -1660,7 +1305,7 @@ Currently logged-in as ${this._username}.`
1660
1305
  */
1661
1306
  async putCardRecord(record) {
1662
1307
  const cardHistoryID = getCardHistoryID(record.courseID, record.cardID);
1663
- record.timeStamp = import_moment3.default.utc(record.timeStamp).toString();
1308
+ record.timeStamp = import_moment5.default.utc(record.timeStamp).toString();
1664
1309
  try {
1665
1310
  const cardHistory = await this.update(
1666
1311
  cardHistoryID,
@@ -1676,7 +1321,7 @@ Currently logged-in as ${this._username}.`
1676
1321
  const ret = {
1677
1322
  ...record2
1678
1323
  };
1679
- ret.timeStamp = import_moment3.default.utc(record2.timeStamp);
1324
+ ret.timeStamp = import_moment5.default.utc(record2.timeStamp);
1680
1325
  return ret;
1681
1326
  });
1682
1327
  return cardHistory;
@@ -1692,8 +1337,8 @@ Currently logged-in as ${this._username}.`
1692
1337
  streak: 0,
1693
1338
  bestInterval: 0
1694
1339
  };
1695
- void this.remoteDB.put(initCardHistory);
1696
- return initCardHistory;
1340
+ const putResult = await this.writeDB.put(initCardHistory);
1341
+ return { ...initCardHistory, _rev: putResult.rev };
1697
1342
  } else {
1698
1343
  throw new Error(`putCardRecord failed because of:
1699
1344
  name:${reason.name}
@@ -1704,17 +1349,17 @@ Currently logged-in as ${this._username}.`
1704
1349
  }
1705
1350
  async deduplicateReviews() {
1706
1351
  try {
1707
- log2("Starting deduplication of scheduled reviews...");
1352
+ log3("Starting deduplication of scheduled reviews...");
1708
1353
  const reviewsMap = {};
1709
1354
  const duplicateDocIds = [];
1710
1355
  const scheduledReviews = await this.remoteDB.query("reviewCards/reviewCards");
1711
- log2(`Found ${scheduledReviews.rows.length} scheduled reviews to process`);
1356
+ log3(`Found ${scheduledReviews.rows.length} scheduled reviews to process`);
1712
1357
  scheduledReviews.rows.forEach((r) => {
1713
1358
  const qualifiedCardId = r.value;
1714
1359
  const docId = r.key;
1715
1360
  if (reviewsMap[qualifiedCardId]) {
1716
- log2(`Found duplicate scheduled review for card: ${qualifiedCardId}`);
1717
- log2(
1361
+ log3(`Found duplicate scheduled review for card: ${qualifiedCardId}`);
1362
+ log3(
1718
1363
  `Marking earlier review ${reviewsMap[qualifiedCardId]} for deletion, keeping ${docId}`
1719
1364
  );
1720
1365
  duplicateDocIds.push(reviewsMap[qualifiedCardId]);
@@ -1724,23 +1369,23 @@ Currently logged-in as ${this._username}.`
1724
1369
  }
1725
1370
  });
1726
1371
  if (duplicateDocIds.length > 0) {
1727
- log2(`Removing ${duplicateDocIds.length} duplicate reviews...`);
1372
+ log3(`Removing ${duplicateDocIds.length} duplicate reviews...`);
1728
1373
  const deletePromises = duplicateDocIds.map(async (docId) => {
1729
1374
  try {
1730
1375
  const doc = await this.remoteDB.get(docId);
1731
- await this.remoteDB.remove(doc);
1732
- log2(`Successfully removed duplicate review: ${docId}`);
1376
+ await this.writeDB.remove(doc);
1377
+ log3(`Successfully removed duplicate review: ${docId}`);
1733
1378
  } catch (error) {
1734
- log2(`Failed to remove duplicate review ${docId}: ${error}`);
1379
+ log3(`Failed to remove duplicate review ${docId}: ${error}`);
1735
1380
  }
1736
1381
  });
1737
1382
  await Promise.all(deletePromises);
1738
- log2(`Deduplication complete. Processed ${duplicateDocIds.length} duplicates`);
1383
+ log3(`Deduplication complete. Processed ${duplicateDocIds.length} duplicates`);
1739
1384
  } else {
1740
- log2("No duplicate reviews found");
1385
+ log3("No duplicate reviews found");
1741
1386
  }
1742
1387
  } catch (error) {
1743
- log2(`Error during review deduplication: ${error}`);
1388
+ log3(`Error during review deduplication: ${error}`);
1744
1389
  }
1745
1390
  }
1746
1391
  /**
@@ -1750,17 +1395,17 @@ Currently logged-in as ${this._username}.`
1750
1395
  * @param course_id optional specification of individual course
1751
1396
  */
1752
1397
  async getSeenCards(course_id) {
1753
- let prefix = cardHistoryPrefix2;
1398
+ let prefix = DocTypePrefixes["CARDRECORD" /* CARDRECORD */];
1754
1399
  if (course_id) {
1755
1400
  prefix += course_id;
1756
1401
  }
1757
- const docs = await filterAllDocsByPrefix2(this.localDB, prefix, {
1402
+ const docs = await filterAllDocsByPrefix(this.localDB, prefix, {
1758
1403
  include_docs: false
1759
1404
  });
1760
1405
  const ret = [];
1761
1406
  docs.rows.forEach((row) => {
1762
- if (row.id.startsWith(cardHistoryPrefix2)) {
1763
- ret.push(row.id.substr(cardHistoryPrefix2.length));
1407
+ if (row.id.startsWith(DocTypePrefixes["CARDRECORD" /* CARDRECORD */])) {
1408
+ ret.push(row.id.substr(DocTypePrefixes["CARDRECORD" /* CARDRECORD */].length));
1764
1409
  }
1765
1410
  });
1766
1411
  return ret;
@@ -1770,9 +1415,9 @@ Currently logged-in as ${this._username}.`
1770
1415
  * @returns A promise of the cards that the user has seen in the past.
1771
1416
  */
1772
1417
  async getHistory() {
1773
- const cards = await filterAllDocsByPrefix2(
1418
+ const cards = await filterAllDocsByPrefix(
1774
1419
  this.remoteDB,
1775
- cardHistoryPrefix2,
1420
+ DocTypePrefixes["CARDRECORD" /* CARDRECORD */],
1776
1421
  {
1777
1422
  include_docs: true,
1778
1423
  attachments: false
@@ -1813,7 +1458,7 @@ Currently logged-in as ${this._username}.`
1813
1458
  } catch (e) {
1814
1459
  const err = e;
1815
1460
  if (err.status === 404) {
1816
- await this.remoteDB.put({
1461
+ await this.writeDB.put({
1817
1462
  _id: _BaseUser.DOC_IDS.CLASSROOM_REGISTRATIONS,
1818
1463
  registrations: []
1819
1464
  });
@@ -1861,10 +1506,10 @@ Currently logged-in as ${this._username}.`
1861
1506
  }
1862
1507
  }
1863
1508
  async scheduleCardReview(review) {
1864
- return scheduleCardReviewLocal(this.remoteDB, review);
1509
+ return scheduleCardReviewLocal(this.writeDB, review);
1865
1510
  }
1866
1511
  async removeScheduledCardReview(reviewId) {
1867
- return removeScheduledCardReviewLocal(this.remoteDB, reviewId);
1512
+ return removeScheduledCardReviewLocal(this.writeDB, reviewId);
1868
1513
  }
1869
1514
  async registerForClassroom(_classId, _registerAs) {
1870
1515
  return registerUserForClassroom(this._username, _classId, _registerAs);
@@ -1894,300 +1539,17 @@ var init_common = __esm({
1894
1539
  }
1895
1540
  });
1896
1541
 
1897
- // src/impl/couch/courseAPI.ts
1898
- async function addNote55(courseID, codeCourse, shape, data, author, tags, uploads, elo = (0, import_common4.blankCourseElo)()) {
1899
- const db = getCourseDB2(courseID);
1900
- const payload = (0, import_common5.prepareNote55)(courseID, codeCourse, shape, data, author, tags, uploads);
1901
- const result = await db.post(payload);
1902
- const dataShapeId = import_common3.NameSpacer.getDataShapeString({
1903
- course: codeCourse,
1904
- dataShape: shape.name
1905
- });
1906
- if (result.ok) {
1907
- try {
1908
- await createCards(courseID, dataShapeId, result.id, tags, elo, author);
1909
- } catch (error) {
1910
- let errorMessage = "Unknown error";
1911
- if (error instanceof Error) {
1912
- errorMessage = error.message;
1913
- } else if (error && typeof error === "object" && "reason" in error) {
1914
- errorMessage = error.reason;
1915
- } else if (error && typeof error === "object" && "message" in error) {
1916
- errorMessage = error.message;
1917
- } else {
1918
- errorMessage = String(error);
1919
- }
1920
- logger.error(`[addNote55] Failed to create cards for note ${result.id}: ${errorMessage}`);
1921
- result.cardCreationFailed = true;
1922
- result.cardCreationError = errorMessage;
1923
- }
1924
- } else {
1925
- logger.error(`[addNote55] Error adding note. Result: ${JSON.stringify(result)}`);
1926
- }
1927
- return result;
1928
- }
1929
- async function createCards(courseID, datashapeID, noteID, tags, elo = (0, import_common4.blankCourseElo)(), author) {
1930
- const cfg = await getCredentialledCourseConfig(courseID);
1931
- const dsDescriptor = import_common3.NameSpacer.getDataShapeDescriptor(datashapeID);
1932
- let questionViewTypes = [];
1933
- for (const ds of cfg.dataShapes) {
1934
- if (ds.name === datashapeID) {
1935
- questionViewTypes = ds.questionTypes;
1936
- }
1937
- }
1938
- if (questionViewTypes.length === 0) {
1939
- const errorMsg = `No questionViewTypes found for datashapeID: ${datashapeID} in course config. Cards cannot be created.`;
1940
- logger.error(errorMsg);
1941
- throw new Error(errorMsg);
1942
- }
1943
- for (const questionView of questionViewTypes) {
1944
- await createCard(questionView, courseID, dsDescriptor, noteID, tags, elo, author);
1945
- }
1946
- }
1947
- async function createCard(questionViewName, courseID, dsDescriptor, noteID, tags, elo = (0, import_common4.blankCourseElo)(), author) {
1948
- const qDescriptor = import_common3.NameSpacer.getQuestionDescriptor(questionViewName);
1949
- const cfg = await getCredentialledCourseConfig(courseID);
1950
- for (const rQ of cfg.questionTypes) {
1951
- if (rQ.name === questionViewName) {
1952
- for (const view of rQ.viewList) {
1953
- await addCard(
1954
- courseID,
1955
- dsDescriptor.course,
1956
- [noteID],
1957
- import_common3.NameSpacer.getViewString({
1958
- course: qDescriptor.course,
1959
- questionType: qDescriptor.questionType,
1960
- view
1961
- }),
1962
- elo,
1963
- tags,
1964
- author
1965
- );
1966
- }
1967
- }
1968
- }
1969
- }
1970
- async function addCard(courseID, course, id_displayable_data, id_view, elo, tags, author) {
1971
- const db = getCourseDB2(courseID);
1972
- const card = await db.post({
1973
- course,
1974
- id_displayable_data,
1975
- id_view,
1976
- docType: "CARD" /* CARD */,
1977
- elo: elo || (0, import_common4.toCourseElo)(990 + Math.round(20 * Math.random())),
1978
- author
1979
- });
1980
- for (const tag of tags) {
1981
- logger.info(`adding tag: ${tag} to card ${card.id}`);
1982
- await addTagToCard(courseID, card.id, tag, author, false);
1983
- }
1984
- return card;
1985
- }
1986
- async function getCredentialledCourseConfig(courseID) {
1987
- try {
1988
- const db = getCourseDB2(courseID);
1989
- const ret = await db.get("CourseConfig");
1990
- ret.courseID = courseID;
1991
- logger.info(`Returning course config: ${JSON.stringify(ret)}`);
1992
- return ret;
1993
- } catch (e) {
1994
- logger.error(`Error fetching config for ${courseID}:`, e);
1995
- throw e;
1996
- }
1997
- }
1998
- async function addTagToCard(courseID, cardID, tagID, author, updateELO = true) {
1999
- const prefixedTagID = getTagID(tagID);
2000
- const courseDB = getCourseDB2(courseID);
2001
- const courseApi = new CourseDB(courseID, async () => {
2002
- const dummySyncStrategy = {
2003
- setupRemoteDB: () => null,
2004
- startSync: () => {
2005
- },
2006
- canCreateAccount: () => false,
2007
- canAuthenticate: () => false,
2008
- getCurrentUsername: async () => "DummyUser"
2009
- };
2010
- return BaseUser.Dummy(dummySyncStrategy);
2011
- });
2012
- try {
2013
- logger.info(`Applying tag ${tagID} to card ${courseID + "-" + cardID}...`);
2014
- const tag = await courseDB.get(prefixedTagID);
2015
- if (!tag.taggedCards.includes(cardID)) {
2016
- tag.taggedCards.push(cardID);
2017
- if (updateELO) {
2018
- try {
2019
- const eloData = await courseApi.getCardEloData([cardID]);
2020
- const elo = eloData[0];
2021
- elo.tags[tagID] = {
2022
- count: 0,
2023
- score: elo.global.score
2024
- // todo: or 1000?
2025
- };
2026
- await updateCardElo(courseID, cardID, elo);
2027
- } catch (error) {
2028
- logger.error("Failed to update ELO data for card:", cardID, error);
2029
- }
2030
- }
2031
- return courseDB.put(tag);
2032
- } else throw new AlreadyTaggedErr(`Card ${cardID} is already tagged with ${tagID}`);
2033
- } catch (e) {
2034
- if (e instanceof AlreadyTaggedErr) {
2035
- throw e;
2036
- }
2037
- await createTag(courseID, tagID, author);
2038
- return addTagToCard(courseID, cardID, tagID, author, updateELO);
2039
- }
2040
- }
2041
- async function updateCardElo(courseID, cardID, elo) {
2042
- if (elo) {
2043
- const cDB = getCourseDB2(courseID);
2044
- const card = await cDB.get(cardID);
2045
- logger.debug(`Replacing ${JSON.stringify(card.elo)} with ${JSON.stringify(elo)}`);
2046
- card.elo = elo;
2047
- return cDB.put(card);
2048
- }
2049
- }
2050
- function getTagID(tagName) {
2051
- const tagPrefix = "TAG" /* TAG */.valueOf() + "-";
2052
- if (tagName.indexOf(tagPrefix) === 0) {
2053
- return tagName;
2054
- } else {
2055
- return tagPrefix + tagName;
2056
- }
2057
- }
2058
- function getCourseDB2(courseID) {
2059
- const dbName = `coursedb-${courseID}`;
2060
- return new pouchdb_setup_default(
2061
- ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + dbName,
2062
- pouchDBincludeCredentialsConfig
2063
- );
2064
- }
2065
- var import_common3, import_common4, import_common5, AlreadyTaggedErr;
2066
- var init_courseAPI = __esm({
2067
- "src/impl/couch/courseAPI.ts"() {
2068
- "use strict";
2069
- init_pouchdb_setup();
2070
- init_couch();
2071
- init_factory();
2072
- import_common3 = require("@vue-skuilder/common");
2073
- import_common4 = require("@vue-skuilder/common");
2074
- init_courseDB();
2075
- init_types_legacy();
2076
- import_common5 = require("@vue-skuilder/common");
2077
- init_common();
2078
- init_logger();
2079
- AlreadyTaggedErr = class extends Error {
2080
- constructor(message) {
2081
- super(message);
2082
- this.name = "AlreadyTaggedErr";
2083
- }
2084
- };
2085
- }
2086
- });
2087
-
2088
- // src/impl/couch/auth.ts
2089
- var init_auth = __esm({
2090
- "src/impl/couch/auth.ts"() {
2091
- "use strict";
2092
- init_factory();
2093
- init_types_legacy();
2094
- init_logger();
2095
- }
2096
- });
2097
-
2098
- // src/impl/couch/CouchDBSyncStrategy.ts
2099
- var import_common7;
2100
- var init_CouchDBSyncStrategy = __esm({
2101
- "src/impl/couch/CouchDBSyncStrategy.ts"() {
1542
+ // src/factory.ts
1543
+ var ENV;
1544
+ var init_factory = __esm({
1545
+ "src/factory.ts"() {
2102
1546
  "use strict";
2103
- init_factory();
2104
- init_types_legacy();
2105
- init_logger();
2106
- import_common7 = require("@vue-skuilder/common");
2107
1547
  init_common();
2108
- init_pouchdb_setup();
2109
- init_couch();
2110
- init_auth();
2111
- }
2112
- });
2113
-
2114
- // src/impl/couch/index.ts
2115
- function getCourseDB(courseID) {
2116
- return new pouchdb_setup_default(
2117
- ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + "coursedb-" + courseID,
2118
- pouchDBincludeCredentialsConfig
2119
- );
2120
- }
2121
- function getCourseDocs(courseID, docIDs, options = {}) {
2122
- return getCourseDB(courseID).allDocs({
2123
- ...options,
2124
- keys: docIDs
2125
- });
2126
- }
2127
- function getCourseDoc(courseID, docID, options = {}) {
2128
- return getCourseDB(courseID).get(docID, options);
2129
- }
2130
- function filterAllDocsByPrefix(db, prefix, opts) {
2131
- const options = {
2132
- startkey: prefix,
2133
- endkey: prefix + "\uFFF0",
2134
- include_docs: true
2135
- };
2136
- if (opts) {
2137
- Object.assign(options, opts);
2138
- }
2139
- return db.allDocs(options);
2140
- }
2141
- function getStartAndEndKeys(key) {
2142
- return {
2143
- startkey: key,
2144
- endkey: key + "\uFFF0"
2145
- };
2146
- }
2147
- var import_moment4, import_process, isBrowser, GUEST_LOCAL_DB, localUserDB, pouchDBincludeCredentialsConfig, REVIEW_PREFIX2, REVIEW_TIME_FORMAT2;
2148
- var init_couch = __esm({
2149
- "src/impl/couch/index.ts"() {
2150
- "use strict";
2151
- init_factory();
2152
- init_types_legacy();
2153
- import_moment4 = __toESM(require("moment"));
2154
1548
  init_logger();
2155
- init_pouchdb_setup();
2156
- import_process = __toESM(require("process"));
2157
- init_contentSource();
2158
- init_adminDB2();
2159
- init_classroomDB2();
2160
- init_courseAPI();
2161
- init_courseDB();
2162
- init_CouchDBSyncStrategy();
2163
- isBrowser = typeof window !== "undefined";
2164
- if (isBrowser) {
2165
- window.process = import_process.default;
2166
- }
2167
- GUEST_LOCAL_DB = `userdb-${GuestUsername}`;
2168
- localUserDB = new pouchdb_setup_default(GUEST_LOCAL_DB);
2169
- pouchDBincludeCredentialsConfig = {
2170
- fetch(url, opts) {
2171
- opts.credentials = "include";
2172
- return pouchdb_setup_default.fetch(url, opts);
2173
- }
1549
+ ENV = {
1550
+ COUCHDB_SERVER_PROTOCOL: "NOT_SET",
1551
+ COUCHDB_SERVER_URL: "NOT_SET"
2174
1552
  };
2175
- REVIEW_PREFIX2 = "card_review_";
2176
- REVIEW_TIME_FORMAT2 = "YYYY-MM-DD--kk:mm:ss-SSS";
2177
- }
2178
- });
2179
-
2180
- // src/impl/couch/classroomDB.ts
2181
- var import_moment5;
2182
- var init_classroomDB2 = __esm({
2183
- "src/impl/couch/classroomDB.ts"() {
2184
- "use strict";
2185
- init_factory();
2186
- init_logger();
2187
- import_moment5 = __toESM(require("moment"));
2188
- init_pouchdb_setup();
2189
- init_couch();
2190
- init_courseDB();
2191
1553
  }
2192
1554
  });
2193
1555
 
@@ -2242,11 +1604,11 @@ var init_user = __esm({
2242
1604
  });
2243
1605
 
2244
1606
  // src/core/bulkImport/cardProcessor.ts
2245
- var import_common9;
1607
+ var import_common10;
2246
1608
  var init_cardProcessor = __esm({
2247
1609
  "src/core/bulkImport/cardProcessor.ts"() {
2248
1610
  "use strict";
2249
- import_common9 = require("@vue-skuilder/common");
1611
+ import_common10 = require("@vue-skuilder/common");
2250
1612
  init_logger();
2251
1613
  }
2252
1614
  });
@@ -2289,11 +1651,11 @@ var init_StaticDataUnpacker = __esm({
2289
1651
  init_logger();
2290
1652
  init_core();
2291
1653
  pathUtils = {
2292
- isAbsolute: (path) => {
2293
- if (/^[a-zA-Z]:[\\/]/.test(path) || /^\\\\/.test(path)) {
1654
+ isAbsolute: (path2) => {
1655
+ if (/^[a-zA-Z]:[\\/]/.test(path2) || /^\\\\/.test(path2)) {
2294
1656
  return true;
2295
1657
  }
2296
- if (path.startsWith("/")) {
1658
+ if (path2.startsWith("/")) {
2297
1659
  return true;
2298
1660
  }
2299
1661
  return false;
@@ -2378,18 +1740,21 @@ var init_StaticDataUnpacker = __esm({
2378
1740
  async getTagsIndex() {
2379
1741
  return await this.loadIndex("tags");
2380
1742
  }
1743
+ getDocTypeFromId(id) {
1744
+ for (const docTypeKey in DocTypePrefixes) {
1745
+ const prefix = DocTypePrefixes[docTypeKey];
1746
+ if (id.startsWith(`${prefix}-`)) {
1747
+ return docTypeKey;
1748
+ }
1749
+ }
1750
+ return void 0;
1751
+ }
2381
1752
  /**
2382
1753
  * Find which chunk contains a specific document ID
2383
1754
  */
2384
1755
  async findChunkForDocument(docId) {
2385
- let expectedDocType = void 0;
2386
- for (const docType of Object.values(DocType)) {
2387
- if (docId.startsWith(`${docType}-`)) {
2388
- expectedDocType = docType;
2389
- break;
2390
- }
2391
- }
2392
- if (expectedDocType !== void 0) {
1756
+ const expectedDocType = this.getDocTypeFromId(docId);
1757
+ if (expectedDocType) {
2393
1758
  const typeChunks = this.manifest.chunks.filter((c) => c.docType === expectedDocType);
2394
1759
  for (const chunk of typeChunks) {
2395
1760
  if (docId >= chunk.startKey && docId <= chunk.endKey) {
@@ -2399,21 +1764,8 @@ var init_StaticDataUnpacker = __esm({
2399
1764
  }
2400
1765
  }
2401
1766
  }
2402
- return void 0;
2403
1767
  } else {
2404
- const displayableChunks = this.manifest.chunks.filter(
2405
- (c) => c.docType === "DISPLAYABLE_DATA"
2406
- );
2407
- for (const chunk of displayableChunks) {
2408
- if (docId >= chunk.startKey && docId <= chunk.endKey) {
2409
- const exists = await this.verifyDocumentInChunk(docId, chunk);
2410
- if (exists) {
2411
- return chunk;
2412
- }
2413
- }
2414
- }
2415
- const cardChunks = this.manifest.chunks.filter((c) => c.docType === "CARD");
2416
- for (const chunk of cardChunks) {
1768
+ for (const chunk of this.manifest.chunks) {
2417
1769
  if (docId >= chunk.startKey && docId <= chunk.endKey) {
2418
1770
  const exists = await this.verifyDocumentInChunk(docId, chunk);
2419
1771
  if (exists) {
@@ -2434,6 +1786,7 @@ var init_StaticDataUnpacker = __esm({
2434
1786
  }
2435
1787
  return void 0;
2436
1788
  }
1789
+ return void 0;
2437
1790
  }
2438
1791
  /**
2439
1792
  * Verify that a document actually exists in a specific chunk by loading and checking it
@@ -2670,13 +2023,14 @@ var init_StaticDataUnpacker = __esm({
2670
2023
  });
2671
2024
 
2672
2025
  // src/impl/static/courseDB.ts
2673
- var import_common10, StaticCourseDB;
2026
+ var import_common11, StaticCourseDB;
2674
2027
  var init_courseDB3 = __esm({
2675
2028
  "src/impl/static/courseDB.ts"() {
2676
2029
  "use strict";
2677
- import_common10 = require("@vue-skuilder/common");
2030
+ import_common11 = require("@vue-skuilder/common");
2678
2031
  init_types_legacy();
2679
2032
  init_navigators();
2033
+ init_logger();
2680
2034
  StaticCourseDB = class {
2681
2035
  constructor(courseId, unpacker, userDB, manifest) {
2682
2036
  this.courseId = courseId;
@@ -2698,10 +2052,11 @@ var init_courseDB3 = __esm({
2698
2052
  throw new Error("Cannot update course config in static mode");
2699
2053
  }
2700
2054
  async getCourseInfo() {
2055
+ const cardCount = this.manifest.chunks.filter((chunk) => chunk.docType === "CARD" /* CARD */).reduce((total, chunk) => total + chunk.documentCount, 0);
2701
2056
  return {
2702
- cardCount: 0,
2703
- // Would come from manifest
2057
+ cardCount,
2704
2058
  registeredUsers: 0
2059
+ // Always 0 in static mode
2705
2060
  };
2706
2061
  }
2707
2062
  async getCourseDoc(id, _options) {
@@ -2790,12 +2145,56 @@ var init_courseDB3 = __esm({
2790
2145
  courseID: this.courseId
2791
2146
  }));
2792
2147
  }
2793
- async getAppliedTags(_cardId) {
2794
- return {
2795
- total_rows: 0,
2796
- offset: 0,
2797
- rows: []
2798
- };
2148
+ async getAppliedTags(cardId) {
2149
+ try {
2150
+ const tagsIndex = await this.unpacker.getTagsIndex();
2151
+ const cardTags = tagsIndex.byCard[cardId] || [];
2152
+ const rows = await Promise.all(
2153
+ cardTags.map(async (tagName) => {
2154
+ const tagId = `${"TAG" /* TAG */}-${tagName}`;
2155
+ try {
2156
+ const tagDoc = await this.unpacker.getDocument(tagId);
2157
+ return {
2158
+ id: tagId,
2159
+ key: cardId,
2160
+ value: {
2161
+ name: tagDoc.name,
2162
+ snippet: tagDoc.snippet,
2163
+ count: tagDoc.taggedCards?.length || 0
2164
+ }
2165
+ };
2166
+ } catch (error) {
2167
+ if (error && error.status === 404) {
2168
+ logger.warn(`Tag document not found for ${tagName}, creating stub`);
2169
+ } else {
2170
+ logger.error(`Error getting tag document for ${tagName}:`, error);
2171
+ throw error;
2172
+ }
2173
+ return {
2174
+ id: tagId,
2175
+ key: cardId,
2176
+ value: {
2177
+ name: tagName,
2178
+ snippet: `Tag: ${tagName}`,
2179
+ count: tagsIndex.byTag[tagName]?.length || 0
2180
+ }
2181
+ };
2182
+ }
2183
+ })
2184
+ );
2185
+ return {
2186
+ total_rows: rows.length,
2187
+ offset: 0,
2188
+ rows
2189
+ };
2190
+ } catch (error) {
2191
+ logger.error(`Error getting applied tags for card ${cardId}:`, error);
2192
+ return {
2193
+ total_rows: 0,
2194
+ offset: 0,
2195
+ rows: []
2196
+ };
2197
+ }
2799
2198
  }
2800
2199
  async addTagToCard(_cardId, _tagId) {
2801
2200
  throw new Error("Cannot modify tags in static mode");
@@ -2813,15 +2212,73 @@ var init_courseDB3 = __esm({
2813
2212
  throw new Error("Cannot update tags in static mode");
2814
2213
  }
2815
2214
  async getCourseTagStubs() {
2816
- return {
2817
- total_rows: 0,
2818
- offset: 0,
2819
- rows: []
2820
- };
2215
+ try {
2216
+ const tagsIndex = await this.unpacker.getTagsIndex();
2217
+ if (!tagsIndex || !tagsIndex.byTag) {
2218
+ logger.warn("Tags index not found or empty");
2219
+ return {
2220
+ total_rows: 0,
2221
+ offset: 0,
2222
+ rows: []
2223
+ };
2224
+ }
2225
+ const tagNames = Object.keys(tagsIndex.byTag);
2226
+ const rows = await Promise.all(
2227
+ tagNames.map(async (tagName) => {
2228
+ const cardIds = tagsIndex.byTag[tagName] || [];
2229
+ const tagId = `${"TAG" /* TAG */}-${tagName}`;
2230
+ try {
2231
+ const tagDoc = await this.unpacker.getDocument(tagId);
2232
+ return {
2233
+ id: tagId,
2234
+ key: tagId,
2235
+ value: { rev: "1-static" },
2236
+ doc: tagDoc
2237
+ };
2238
+ } catch (error) {
2239
+ if (error && error.status === 404) {
2240
+ logger.warn(`Tag document not found for ${tagName}, creating stub`);
2241
+ const stubDoc = {
2242
+ _id: tagId,
2243
+ _rev: "1-static",
2244
+ course: this.courseId,
2245
+ docType: "TAG" /* TAG */,
2246
+ name: tagName,
2247
+ snippet: `Tag: ${tagName}`,
2248
+ wiki: "",
2249
+ taggedCards: cardIds,
2250
+ author: "system"
2251
+ };
2252
+ return {
2253
+ id: tagId,
2254
+ key: tagId,
2255
+ value: { rev: "1-static" },
2256
+ doc: stubDoc
2257
+ };
2258
+ } else {
2259
+ logger.error(`Error getting tag document for ${tagName}:`, error);
2260
+ throw error;
2261
+ }
2262
+ }
2263
+ })
2264
+ );
2265
+ return {
2266
+ total_rows: rows.length,
2267
+ offset: 0,
2268
+ rows
2269
+ };
2270
+ } catch (error) {
2271
+ logger.error("Failed to get course tag stubs:", error);
2272
+ return {
2273
+ total_rows: 0,
2274
+ offset: 0,
2275
+ rows: []
2276
+ };
2277
+ }
2821
2278
  }
2822
2279
  async addNote(_codeCourse, _shape, _data, _author, _tags, _uploads, _elo) {
2823
2280
  return {
2824
- status: import_common10.Status.error,
2281
+ status: import_common11.Status.error,
2825
2282
  message: "Cannot add notes in static mode"
2826
2283
  };
2827
2284
  }
@@ -2923,6 +2380,9 @@ var init_NoOpSyncStrategy = __esm({
2923
2380
  setupRemoteDB(username) {
2924
2381
  return getLocalUserDB(username);
2925
2382
  }
2383
+ getWriteDB(username) {
2384
+ return getLocalUserDB(username);
2385
+ }
2926
2386
  startSync(_localDB, _remoteDB) {
2927
2387
  }
2928
2388
  stopSync() {