@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;
@@ -72,19 +72,47 @@ var init_classroomDB = __esm({
72
72
  }
73
73
  });
74
74
 
75
- // src/factory.ts
76
- var ENV;
77
- var init_factory = __esm({
78
- "src/factory.ts"() {
75
+ // src/impl/common/SyncStrategy.ts
76
+ var init_SyncStrategy = __esm({
77
+ "src/impl/common/SyncStrategy.ts"() {
78
+ "use strict";
79
+ }
80
+ });
81
+
82
+ // src/core/types/types-legacy.ts
83
+ var GuestUsername, DocTypePrefixes;
84
+ var init_types_legacy = __esm({
85
+ "src/core/types/types-legacy.ts"() {
79
86
  "use strict";
80
87
  init_logger();
81
- ENV = {
82
- COUCHDB_SERVER_PROTOCOL: "NOT_SET",
83
- COUCHDB_SERVER_URL: "NOT_SET"
88
+ GuestUsername = "Guest";
89
+ DocTypePrefixes = {
90
+ ["CARD" /* CARD */]: "c",
91
+ ["DISPLAYABLE_DATA" /* DISPLAYABLE_DATA */]: "dd",
92
+ ["TAG" /* TAG */]: "TAG",
93
+ ["CARDRECORD" /* CARDRECORD */]: "cardH",
94
+ ["SCHEDULED_CARD" /* SCHEDULED_CARD */]: "card_review_",
95
+ // Add other doctypes here as they get prefixed IDs
96
+ ["DATASHAPE" /* DATASHAPE */]: "DATASHAPE",
97
+ ["QUESTION" /* QUESTIONTYPE */]: "QUESTION",
98
+ ["VIEW" /* VIEW */]: "VIEW",
99
+ ["PEDAGOGY" /* PEDAGOGY */]: "PEDAGOGY",
100
+ ["NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */]: "NAVIGATION_STRATEGY"
84
101
  };
85
102
  }
86
103
  });
87
104
 
105
+ // src/core/util/index.ts
106
+ function getCardHistoryID(courseID, cardID) {
107
+ return `${DocTypePrefixes["CARDRECORD" /* CARDRECORD */]}-${courseID}-${cardID}`;
108
+ }
109
+ var init_util = __esm({
110
+ "src/core/util/index.ts"() {
111
+ "use strict";
112
+ init_types_legacy();
113
+ }
114
+ });
115
+
88
116
  // src/impl/couch/pouchdb-setup.ts
89
117
  import PouchDB from "pouchdb";
90
118
  import PouchDBFind from "pouchdb-find";
@@ -104,51 +132,93 @@ var init_pouchdb_setup = __esm({
104
132
  }
105
133
  });
106
134
 
107
- // src/core/types/types-legacy.ts
108
- var GuestUsername, DocType, cardHistoryPrefix;
109
- var init_types_legacy = __esm({
110
- "src/core/types/types-legacy.ts"() {
135
+ // src/util/tuiLogger.ts
136
+ var init_tuiLogger = __esm({
137
+ "src/util/tuiLogger.ts"() {
111
138
  "use strict";
112
- init_logger();
113
- GuestUsername = "Guest";
114
- DocType = /* @__PURE__ */ ((DocType2) => {
115
- DocType2["DISPLAYABLE_DATA"] = "DISPLAYABLE_DATA";
116
- DocType2["CARD"] = "CARD";
117
- DocType2["DATASHAPE"] = "DATASHAPE";
118
- DocType2["QUESTIONTYPE"] = "QUESTION";
119
- DocType2["VIEW"] = "VIEW";
120
- DocType2["PEDAGOGY"] = "PEDAGOGY";
121
- DocType2["CARDRECORD"] = "CARDRECORD";
122
- DocType2["SCHEDULED_CARD"] = "SCHEDULED_CARD";
123
- DocType2["TAG"] = "TAG";
124
- DocType2["NAVIGATION_STRATEGY"] = "NAVIGATION_STRATEGY";
125
- return DocType2;
126
- })(DocType || {});
127
- cardHistoryPrefix = "cardH";
139
+ init_dataDirectory();
128
140
  }
129
141
  });
130
142
 
131
- // src/impl/couch/courseLookupDB.ts
132
- var init_courseLookupDB = __esm({
133
- "src/impl/couch/courseLookupDB.ts"() {
143
+ // src/util/dataDirectory.ts
144
+ import * as path from "path";
145
+ import * as os from "os";
146
+ function getAppDataDirectory() {
147
+ return path.join(os.homedir(), ".tuilder");
148
+ }
149
+ function getDbPath(dbName) {
150
+ return path.join(getAppDataDirectory(), dbName);
151
+ }
152
+ var init_dataDirectory = __esm({
153
+ "src/util/dataDirectory.ts"() {
134
154
  "use strict";
135
- init_pouchdb_setup();
136
- init_factory();
137
- init_logger();
138
- logger.debug(`COURSELOOKUP FILE RUNNING`);
155
+ init_tuiLogger();
139
156
  }
140
157
  });
141
158
 
142
- // src/impl/couch/adminDB.ts
143
- var init_adminDB2 = __esm({
144
- "src/impl/couch/adminDB.ts"() {
159
+ // src/impl/common/userDBHelpers.ts
160
+ import moment from "moment";
161
+ function filterAllDocsByPrefix(db, prefix, opts) {
162
+ const options = {
163
+ startkey: prefix,
164
+ endkey: prefix + "\uFFF0",
165
+ include_docs: true
166
+ };
167
+ if (opts) {
168
+ Object.assign(options, opts);
169
+ }
170
+ return db.allDocs(options);
171
+ }
172
+ function getStartAndEndKeys(key) {
173
+ return {
174
+ startkey: key,
175
+ endkey: key + "\uFFF0"
176
+ };
177
+ }
178
+ function getLocalUserDB(username) {
179
+ const dbName = `userdb-${username}`;
180
+ if (typeof window === "undefined") {
181
+ return new pouchdb_setup_default(getDbPath(dbName), {});
182
+ } else {
183
+ return new pouchdb_setup_default(dbName, {});
184
+ }
185
+ }
186
+ function scheduleCardReviewLocal(userDB, review) {
187
+ const now = moment.utc();
188
+ logger.info(`Scheduling for review in: ${review.time.diff(now, "h") / 24} days`);
189
+ void userDB.put({
190
+ _id: DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */] + review.time.format(REVIEW_TIME_FORMAT),
191
+ cardId: review.card_id,
192
+ reviewTime: review.time.toISOString(),
193
+ courseId: review.course_id,
194
+ scheduledAt: now.toISOString(),
195
+ scheduledFor: review.scheduledFor,
196
+ schedulingAgentId: review.schedulingAgentId
197
+ });
198
+ }
199
+ async function removeScheduledCardReviewLocal(userDB, reviewDocID) {
200
+ const reviewDoc = await userDB.get(reviewDocID);
201
+ userDB.remove(reviewDoc).then((res) => {
202
+ if (res.ok) {
203
+ log(`Removed Review Doc: ${reviewDocID}`);
204
+ }
205
+ }).catch((err) => {
206
+ log(`Failed to remove Review Doc: ${reviewDocID},
207
+ ${JSON.stringify(err)}`);
208
+ });
209
+ }
210
+ var REVIEW_TIME_FORMAT, log;
211
+ var init_userDBHelpers = __esm({
212
+ "src/impl/common/userDBHelpers.ts"() {
145
213
  "use strict";
146
- init_pouchdb_setup();
147
- init_factory();
148
- init_couch();
149
- init_classroomDB2();
150
- init_courseLookupDB();
214
+ init_core();
151
215
  init_logger();
216
+ init_pouchdb_setup();
217
+ init_dataDirectory();
218
+ REVIEW_TIME_FORMAT = "YYYY-MM-DD--kk:mm:ss-SSS";
219
+ log = (s) => {
220
+ logger.info(s);
221
+ };
152
222
  }
153
223
  });
154
224
 
@@ -179,7 +249,10 @@ var init_updateQueue = __esm({
179
249
  _className = "UpdateQueue";
180
250
  pendingUpdates = {};
181
251
  inprogressUpdates = {};
182
- db;
252
+ readDB;
253
+ // Database for read operations
254
+ writeDB;
255
+ // Database for write operations (local-first)
183
256
  update(id, update) {
184
257
  logger.debug(`Update requested on doc: ${id}`);
185
258
  if (this.pendingUpdates[id]) {
@@ -189,24 +262,25 @@ var init_updateQueue = __esm({
189
262
  }
190
263
  return this.applyUpdates(id);
191
264
  }
192
- constructor(db) {
265
+ constructor(readDB, writeDB) {
193
266
  super();
194
- this.db = db;
267
+ this.readDB = readDB;
268
+ this.writeDB = writeDB || readDB;
195
269
  logger.debug(`UpdateQ initialized...`);
196
- void this.db.info().then((i) => {
270
+ void this.readDB.info().then((i) => {
197
271
  logger.debug(`db info: ${JSON.stringify(i)}`);
198
272
  });
199
273
  }
200
274
  async applyUpdates(id) {
201
275
  logger.debug(`Applying updates on doc: ${id}`);
202
276
  if (this.inprogressUpdates[id]) {
203
- await this.db.info();
277
+ await this.readDB.info();
204
278
  return this.applyUpdates(id);
205
279
  } else {
206
280
  if (this.pendingUpdates[id] && this.pendingUpdates[id].length > 0) {
207
281
  this.inprogressUpdates[id] = true;
208
282
  try {
209
- let doc = await this.db.get(id);
283
+ let doc = await this.readDB.get(id);
210
284
  logger.debug(`Retrieved doc: ${id}`);
211
285
  while (this.pendingUpdates[id].length !== 0) {
212
286
  const update = this.pendingUpdates[id].splice(0, 1)[0];
@@ -219,7 +293,7 @@ var init_updateQueue = __esm({
219
293
  };
220
294
  }
221
295
  }
222
- await this.db.put(doc);
296
+ await this.writeDB.put(doc);
223
297
  logger.debug(`Put doc: ${id}`);
224
298
  if (this.pendingUpdates[id].length === 0) {
225
299
  this.inprogressUpdates[id] = false;
@@ -245,22 +319,112 @@ var init_updateQueue = __esm({
245
319
  }
246
320
  });
247
321
 
322
+ // src/impl/couch/user-course-relDB.ts
323
+ import moment2 from "moment";
324
+ var UsrCrsData;
325
+ var init_user_course_relDB = __esm({
326
+ "src/impl/couch/user-course-relDB.ts"() {
327
+ "use strict";
328
+ init_logger();
329
+ UsrCrsData = class {
330
+ user;
331
+ _courseId;
332
+ constructor(user, courseId) {
333
+ this.user = user;
334
+ this._courseId = courseId;
335
+ }
336
+ async getReviewsForcast(daysCount) {
337
+ const time = moment2.utc().add(daysCount, "days");
338
+ return this.getReviewstoDate(time);
339
+ }
340
+ async getPendingReviews() {
341
+ const now = moment2.utc();
342
+ return this.getReviewstoDate(now);
343
+ }
344
+ async getScheduledReviewCount() {
345
+ return (await this.getPendingReviews()).length;
346
+ }
347
+ async getCourseSettings() {
348
+ const regDoc = await this.user.getCourseRegistrationsDoc();
349
+ const crsDoc = regDoc.courses.find((c) => c.courseID === this._courseId);
350
+ if (crsDoc && crsDoc.settings) {
351
+ return crsDoc.settings;
352
+ } else {
353
+ logger.warn(`no settings found during lookup on course ${this._courseId}`);
354
+ return {};
355
+ }
356
+ }
357
+ updateCourseSettings(updates) {
358
+ if ("updateCourseSettings" in this.user) {
359
+ void this.user.updateCourseSettings(this._courseId, updates);
360
+ }
361
+ }
362
+ async getReviewstoDate(targetDate) {
363
+ const allReviews = await this.user.getPendingReviews(this._courseId);
364
+ logger.debug(
365
+ `Fetching ${this.user.getUsername()}'s scheduled reviews for course ${this._courseId}.`
366
+ );
367
+ return allReviews.filter((review) => {
368
+ const reviewTime = moment2.utc(review.reviewTime);
369
+ return targetDate.isAfter(reviewTime);
370
+ });
371
+ }
372
+ };
373
+ }
374
+ });
375
+
248
376
  // src/impl/couch/clientCache.ts
249
- async function GET_CACHED(k, f) {
250
- if (CLIENT_CACHE[k]) {
251
- return CLIENT_CACHE[k];
377
+ var init_clientCache = __esm({
378
+ "src/impl/couch/clientCache.ts"() {
379
+ "use strict";
380
+ }
381
+ });
382
+
383
+ // src/impl/couch/courseAPI.ts
384
+ import { NameSpacer } from "@vue-skuilder/common";
385
+ import { blankCourseElo, toCourseElo } from "@vue-skuilder/common";
386
+ import { prepareNote55 } from "@vue-skuilder/common";
387
+ import { v4 as uuidv4 } from "uuid";
388
+ async function getCredentialledCourseConfig(courseID) {
389
+ try {
390
+ const db = getCourseDB(courseID);
391
+ const ret = await db.get("CourseConfig");
392
+ ret.courseID = courseID;
393
+ logger.info(`Returning course config: ${JSON.stringify(ret)}`);
394
+ return ret;
395
+ } catch (e) {
396
+ logger.error(`Error fetching config for ${courseID}:`, e);
397
+ throw e;
252
398
  }
253
- CLIENT_CACHE[k] = f ? await f(k) : await GET_ITEM(k);
254
- return GET_CACHED(k);
255
399
  }
256
- async function GET_ITEM(k) {
257
- throw new Error(`No implementation found for GET_CACHED(${k})`);
400
+ function getCourseDB(courseID) {
401
+ const dbName = `coursedb-${courseID}`;
402
+ return new pouchdb_setup_default(
403
+ ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + dbName,
404
+ pouchDBincludeCredentialsConfig
405
+ );
258
406
  }
259
- var CLIENT_CACHE;
260
- var init_clientCache = __esm({
261
- "src/impl/couch/clientCache.ts"() {
407
+ var init_courseAPI = __esm({
408
+ "src/impl/couch/courseAPI.ts"() {
409
+ "use strict";
410
+ init_pouchdb_setup();
411
+ init_couch();
412
+ init_factory();
413
+ init_courseDB();
414
+ init_types_legacy();
415
+ init_common();
416
+ init_logger();
417
+ }
418
+ });
419
+
420
+ // src/impl/couch/courseLookupDB.ts
421
+ var init_courseLookupDB = __esm({
422
+ "src/impl/couch/courseLookupDB.ts"() {
262
423
  "use strict";
263
- CLIENT_CACHE = {};
424
+ init_pouchdb_setup();
425
+ init_factory();
426
+ init_logger();
427
+ logger.debug(`COURSELOOKUP FILE RUNNING`);
264
428
  }
265
429
  });
266
430
 
@@ -387,85 +551,9 @@ var init_navigators = __esm({
387
551
  import {
388
552
  EloToNumber,
389
553
  Status,
390
- blankCourseElo,
391
- toCourseElo
554
+ blankCourseElo as blankCourseElo2,
555
+ toCourseElo as toCourseElo2
392
556
  } from "@vue-skuilder/common";
393
- function randIntWeightedTowardZero(n) {
394
- return Math.floor(Math.random() * Math.random() * Math.random() * n);
395
- }
396
- async function getCourseTagStubs(courseID) {
397
- logger.debug(`Getting tag stubs for course: ${courseID}`);
398
- const stubs = await filterAllDocsByPrefix(
399
- getCourseDB(courseID),
400
- "TAG" /* TAG */.valueOf() + "-"
401
- );
402
- stubs.rows.forEach((row) => {
403
- logger.debug(` Tag stub for doc: ${row.id}`);
404
- });
405
- return stubs;
406
- }
407
- async function createTag(courseID, tagName, author) {
408
- logger.debug(`Creating tag: ${tagName}...`);
409
- const tagID = getTagID(tagName);
410
- const courseDB = getCourseDB(courseID);
411
- const resp = await courseDB.put({
412
- course: courseID,
413
- docType: "TAG" /* TAG */,
414
- name: tagName,
415
- snippet: "",
416
- taggedCards: [],
417
- wiki: "",
418
- author,
419
- _id: tagID
420
- });
421
- return resp;
422
- }
423
- async function updateTag(tag) {
424
- const prior = await getTag(tag.course, tag.name);
425
- return await getCourseDB(tag.course).put({
426
- ...tag,
427
- _rev: prior._rev
428
- });
429
- }
430
- async function getTag(courseID, tagName) {
431
- const tagID = getTagID(tagName);
432
- const courseDB = getCourseDB(courseID);
433
- return courseDB.get(tagID);
434
- }
435
- async function removeTagFromCard(courseID, cardID, tagID) {
436
- tagID = getTagID(tagID);
437
- const courseDB = getCourseDB(courseID);
438
- const tag = await courseDB.get(tagID);
439
- tag.taggedCards = tag.taggedCards.filter((taggedID) => {
440
- return cardID !== taggedID;
441
- });
442
- return courseDB.put(tag);
443
- }
444
- async function getAppliedTags(id_course, id_card) {
445
- const db = getCourseDB(id_course);
446
- const result = await db.query("getTags", {
447
- startkey: id_card,
448
- endkey: id_card
449
- // include_docs: true
450
- });
451
- return result;
452
- }
453
- async function updateCredentialledCourseConfig(courseID, config) {
454
- logger.debug(`Updating course config:
455
-
456
- ${JSON.stringify(config)}
457
- `);
458
- const db = getCourseDB(courseID);
459
- const old = await getCredentialledCourseConfig(courseID);
460
- return await db.put({
461
- ...config,
462
- _rev: old._rev
463
- });
464
- }
465
- function isSuccessRow(row) {
466
- return "doc" in row && row.doc !== null && row.doc !== void 0;
467
- }
468
- var CourseDB;
469
557
  var init_courseDB = __esm({
470
558
  "src/impl/couch/courseDB.ts"() {
471
559
  "use strict";
@@ -477,568 +565,95 @@ var init_courseDB = __esm({
477
565
  init_courseAPI();
478
566
  init_courseLookupDB();
479
567
  init_navigators();
480
- CourseDB = class {
481
- // private log(msg: string): void {
482
- // log(`CourseLog: ${this.id}\n ${msg}`);
483
- // }
484
- db;
485
- id;
486
- _getCurrentUser;
487
- updateQueue;
488
- constructor(id, userLookup) {
489
- this.id = id;
490
- this.db = getCourseDB(this.id);
491
- this._getCurrentUser = userLookup;
492
- this.updateQueue = new UpdateQueue(this.db);
493
- }
494
- getCourseID() {
495
- return this.id;
496
- }
497
- async getCourseInfo() {
498
- const cardCount = (await this.db.find({
499
- selector: {
500
- docType: "CARD" /* CARD */
501
- },
502
- limit: 1e3
503
- })).docs.length;
504
- return {
505
- cardCount,
506
- registeredUsers: 0
507
- };
508
- }
509
- async getInexperiencedCards(limit = 2) {
510
- return (await this.db.query("cardsByInexperience", {
511
- limit
512
- })).rows.map((r) => {
513
- const ret = {
514
- courseId: this.id,
515
- cardId: r.id,
516
- count: r.key,
517
- elo: r.value
518
- };
519
- return ret;
520
- });
521
- }
522
- async getCardsByEloLimits(options = {
523
- low: 0,
524
- high: Number.MIN_SAFE_INTEGER,
525
- limit: 25,
526
- page: 0
527
- }) {
528
- return (await this.db.query("elo", {
529
- startkey: options.low,
530
- endkey: options.high,
531
- limit: options.limit,
532
- skip: options.limit * options.page
533
- })).rows.map((r) => {
534
- return `${this.id}-${r.id}-${r.key}`;
535
- });
536
- }
537
- async getCardEloData(id) {
538
- const docs = await this.db.allDocs({
539
- keys: id,
540
- include_docs: true
541
- });
542
- const ret = [];
543
- docs.rows.forEach((r) => {
544
- if (isSuccessRow(r)) {
545
- if (r.doc && r.doc.elo) {
546
- ret.push(toCourseElo(r.doc.elo));
547
- } else {
548
- logger.warn("no elo data for card: " + r.id);
549
- ret.push(blankCourseElo());
550
- }
551
- } else {
552
- logger.warn("no elo data for card: " + JSON.stringify(r));
553
- ret.push(blankCourseElo());
554
- }
555
- });
556
- return ret;
557
- }
558
- /**
559
- * Returns the lowest and highest `global` ELO ratings in the course
560
- */
561
- async getELOBounds() {
562
- const [low, high] = await Promise.all([
563
- (await this.db.query("elo", {
564
- startkey: 0,
565
- limit: 1,
566
- include_docs: false
567
- })).rows[0].key,
568
- (await this.db.query("elo", {
569
- limit: 1,
570
- descending: true,
571
- startkey: 1e5
572
- })).rows[0].key
573
- ]);
574
- return {
575
- low,
576
- high
577
- };
578
- }
579
- async removeCard(id) {
580
- const doc = await this.db.get(id);
581
- if (!doc.docType || !(doc.docType === "CARD" /* CARD */)) {
582
- throw new Error(`failed to remove ${id} from course ${this.id}. id does not point to a card`);
583
- }
584
- return this.db.remove(doc);
585
- }
586
- async getCardDisplayableDataIDs(id) {
587
- logger.debug(id.join(", "));
588
- const cards = await this.db.allDocs({
589
- keys: id,
590
- include_docs: true
591
- });
592
- const ret = {};
593
- cards.rows.forEach((r) => {
594
- if (isSuccessRow(r)) {
595
- ret[r.id] = r.doc.id_displayable_data;
596
- }
597
- });
598
- await Promise.all(
599
- cards.rows.map((r) => {
600
- return async () => {
601
- if (isSuccessRow(r)) {
602
- ret[r.id] = r.doc.id_displayable_data;
603
- }
604
- };
605
- })
606
- );
607
- return ret;
608
- }
609
- async getCardsByELO(elo, cardLimit) {
610
- elo = parseInt(elo);
611
- const limit = cardLimit ? cardLimit : 25;
612
- const below = await this.db.query("elo", {
613
- limit: Math.ceil(limit / 2),
614
- startkey: elo,
615
- descending: true
616
- });
617
- const aboveLimit = limit - below.rows.length;
618
- const above = await this.db.query("elo", {
619
- limit: aboveLimit,
620
- startkey: elo + 1
621
- });
622
- let cards = below.rows;
623
- cards = cards.concat(above.rows);
624
- const ret = cards.sort((a, b) => {
625
- const s = Math.abs(a.key - elo) - Math.abs(b.key - elo);
626
- if (s === 0) {
627
- return Math.random() - 0.5;
628
- } else {
629
- return s;
630
- }
631
- }).map((c) => `${this.id}-${c.id}-${c.key}`);
632
- const str = `below:
633
- ${below.rows.map((r) => ` ${r.id}-${r.key}
634
- `)}
635
-
636
- above:
637
- ${above.rows.map((r) => ` ${r.id}-${r.key}
638
- `)}`;
639
- logger.debug(`Getting ${limit} cards centered around elo: ${elo}:
568
+ }
569
+ });
640
570
 
641
- ` + str);
642
- return ret;
643
- }
644
- async getCourseConfig() {
645
- const ret = await getCredentialledCourseConfig(this.id);
646
- if (ret) {
647
- return ret;
648
- } else {
649
- throw new Error(`Course config not found for course ID: ${this.id}`);
650
- }
651
- }
652
- async updateCourseConfig(cfg) {
653
- logger.debug(`Updating: ${JSON.stringify(cfg)}`);
654
- try {
655
- return await updateCredentialledCourseConfig(this.id, cfg);
656
- } catch (error) {
657
- logger.error(`Error updating course config in course DB: ${error}`);
658
- throw error;
659
- }
660
- }
661
- async updateCardElo(cardId, elo) {
662
- if (!elo) {
663
- throw new Error(`Cannot update card elo with null or undefined value for card ID: ${cardId}`);
664
- }
665
- try {
666
- const result = await this.updateQueue.update(cardId, (card) => {
667
- logger.debug(`Replacing ${JSON.stringify(card.elo)} with ${JSON.stringify(elo)}`);
668
- card.elo = elo;
669
- return card;
670
- });
671
- return { ok: true, id: cardId, rev: result._rev };
672
- } catch (error) {
673
- logger.error(`Failed to update card elo for card ID: ${cardId}`, error);
674
- throw new Error(`Failed to update card elo for card ID: ${cardId}`);
675
- }
676
- }
677
- async getAppliedTags(cardId) {
678
- const ret = await getAppliedTags(this.id, cardId);
679
- if (ret) {
680
- return ret;
681
- } else {
682
- throw new Error(`Failed to find tags for card ${this.id}-${cardId}`);
683
- }
684
- }
685
- async addTagToCard(cardId, tagId, updateELO) {
686
- return await addTagToCard(this.id, cardId, tagId, (await this._getCurrentUser()).getUsername(), updateELO);
687
- }
688
- async removeTagFromCard(cardId, tagId) {
689
- return await removeTagFromCard(this.id, cardId, tagId);
690
- }
691
- async createTag(name, author) {
692
- return await createTag(this.id, name, author);
693
- }
694
- async getTag(tagId) {
695
- return await getTag(this.id, tagId);
696
- }
697
- async updateTag(tag) {
698
- if (tag.course !== this.id) {
699
- throw new Error(`Tag ${JSON.stringify(tag)} does not belong to course ${this.id}`);
700
- }
701
- return await updateTag(tag);
702
- }
703
- async getCourseTagStubs() {
704
- return getCourseTagStubs(this.id);
705
- }
706
- async addNote(codeCourse, shape, data, author, tags, uploads, elo = blankCourseElo()) {
707
- try {
708
- const resp = await addNote55(this.id, codeCourse, shape, data, author, tags, uploads, elo);
709
- if (resp.ok) {
710
- if (resp.cardCreationFailed) {
711
- logger.warn(
712
- `[courseDB.addNote] Note added but card creation failed: ${resp.cardCreationError}`
713
- );
714
- return {
715
- status: Status.error,
716
- message: `Note was added but no cards were created: ${resp.cardCreationError}`,
717
- id: resp.id
718
- };
719
- }
720
- return {
721
- status: Status.ok,
722
- message: "",
723
- id: resp.id
724
- };
725
- } else {
726
- return {
727
- status: Status.error,
728
- message: "Unexpected error adding note"
729
- };
730
- }
731
- } catch (e) {
732
- const err = e;
733
- logger.error(
734
- `[addNote] error ${err.name}
735
- reason: ${err.reason}
736
- message: ${err.message}`
737
- );
738
- return {
739
- status: Status.error,
740
- message: `Error adding note to course. ${e.reason || err.message}`
741
- };
742
- }
743
- }
744
- async getCourseDoc(id, options) {
745
- return await getCourseDoc(this.id, id, options);
746
- }
747
- async getCourseDocs(ids, options = {}) {
748
- return await getCourseDocs(this.id, ids, options);
749
- }
750
- ////////////////////////////////////
751
- // NavigationStrategyManager implementation
752
- ////////////////////////////////////
753
- getNavigationStrategy(id) {
754
- logger.debug(`[courseDB] Getting navigation strategy: ${id}`);
755
- const strategy = {
756
- id: "ELO",
757
- docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
758
- name: "ELO",
759
- description: "ELO-based navigation strategy for ordering content by difficulty",
760
- implementingClass: "elo" /* ELO */,
761
- course: this.id,
762
- serializedData: ""
763
- // serde is a noop for ELO navigator.
764
- };
765
- return Promise.resolve(strategy);
766
- }
767
- getAllNavigationStrategies() {
768
- logger.debug("[courseDB] Returning hard-coded navigation strategies");
769
- const strategies = [
770
- {
771
- id: "ELO",
772
- docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
773
- name: "ELO",
774
- description: "ELO-based navigation strategy for ordering content by difficulty",
775
- implementingClass: "elo" /* ELO */,
776
- course: this.id,
777
- serializedData: ""
778
- // serde is a noop for ELO navigator.
779
- }
780
- ];
781
- return Promise.resolve(strategies);
782
- }
783
- addNavigationStrategy(data) {
784
- logger.debug(`[courseDB] Adding navigation strategy: ${data.id}`);
785
- logger.debug(JSON.stringify(data));
786
- return Promise.resolve();
787
- }
788
- updateNavigationStrategy(id, data) {
789
- logger.debug(`[courseDB] Updating navigation strategy: ${id}`);
790
- logger.debug(JSON.stringify(data));
791
- return Promise.resolve();
792
- }
793
- async surfaceNavigationStrategy() {
794
- logger.warn(`Returning hard-coded default ELO navigator`);
795
- const ret = {
796
- id: "ELO",
797
- docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
798
- name: "ELO",
799
- description: "ELO-based navigation strategy",
800
- implementingClass: "elo" /* ELO */,
801
- course: this.id,
802
- serializedData: ""
803
- // serde is a noop for ELO navigator.
804
- };
805
- return Promise.resolve(ret);
806
- }
807
- ////////////////////////////////////
808
- // END NavigationStrategyManager implementation
809
- ////////////////////////////////////
810
- ////////////////////////////////////
811
- // StudyContentSource implementation
812
- ////////////////////////////////////
813
- async getNewCards(limit = 99) {
814
- const u = await this._getCurrentUser();
815
- try {
816
- const strategy = await this.surfaceNavigationStrategy();
817
- const navigator = await ContentNavigator.create(u, this, strategy);
818
- return navigator.getNewCards(limit);
819
- } catch (e) {
820
- logger.error(`[courseDB] Error surfacing a NavigationStrategy: ${e}`);
821
- throw e;
822
- }
823
- }
824
- async getPendingReviews() {
825
- const u = await this._getCurrentUser();
826
- try {
827
- const strategy = await this.surfaceNavigationStrategy();
828
- const navigator = await ContentNavigator.create(u, this, strategy);
829
- return navigator.getPendingReviews();
830
- } catch (e) {
831
- logger.error(`[courseDB] Error surfacing a NavigationStrategy: ${e}`);
832
- throw e;
833
- }
834
- }
835
- async getCardsCenteredAtELO(options = {
836
- limit: 99,
837
- elo: "user"
838
- }, filter) {
839
- let targetElo;
840
- if (options.elo === "user") {
841
- const u = await this._getCurrentUser();
842
- targetElo = -1;
843
- try {
844
- const courseDoc = (await u.getCourseRegistrationsDoc()).courses.find((c) => {
845
- return c.courseID === this.id;
846
- });
847
- targetElo = EloToNumber(courseDoc.elo);
848
- } catch {
849
- targetElo = 1e3;
850
- }
851
- } else if (options.elo === "random") {
852
- const bounds = await GET_CACHED(`elo-bounds-${this.id}`, () => this.getELOBounds());
853
- targetElo = Math.round(bounds.low + Math.random() * (bounds.high - bounds.low));
854
- } else {
855
- targetElo = options.elo;
856
- }
857
- let cards = [];
858
- let mult = 4;
859
- let previousCount = -1;
860
- let newCount = 0;
861
- while (cards.length < options.limit && newCount !== previousCount) {
862
- cards = await this.getCardsByELO(targetElo, mult * options.limit);
863
- previousCount = newCount;
864
- newCount = cards.length;
865
- logger.debug(`Found ${cards.length} elo neighbor cards...`);
866
- if (filter) {
867
- cards = cards.filter(filter);
868
- logger.debug(`Filtered to ${cards.length} cards...`);
869
- }
870
- mult *= 2;
871
- }
872
- const selectedCards = [];
873
- while (selectedCards.length < options.limit && cards.length > 0) {
874
- const index = randIntWeightedTowardZero(cards.length);
875
- const card = cards.splice(index, 1)[0];
876
- selectedCards.push(card);
877
- }
878
- return selectedCards.map((c) => {
879
- const split = c.split("-");
880
- return {
881
- courseID: this.id,
882
- cardID: split[1],
883
- qualifiedID: `${split[0]}-${split[1]}`,
884
- contentSourceType: "course",
885
- contentSourceID: this.id,
886
- status: "new"
887
- };
888
- });
889
- }
890
- };
571
+ // src/impl/couch/classroomDB.ts
572
+ import moment3 from "moment";
573
+ var init_classroomDB2 = __esm({
574
+ "src/impl/couch/classroomDB.ts"() {
575
+ "use strict";
576
+ init_factory();
577
+ init_logger();
578
+ init_pouchdb_setup();
579
+ init_couch();
580
+ init_courseDB();
891
581
  }
892
582
  });
893
583
 
894
- // src/impl/common/SyncStrategy.ts
895
- var init_SyncStrategy = __esm({
896
- "src/impl/common/SyncStrategy.ts"() {
584
+ // src/impl/couch/adminDB.ts
585
+ var init_adminDB2 = __esm({
586
+ "src/impl/couch/adminDB.ts"() {
897
587
  "use strict";
588
+ init_pouchdb_setup();
589
+ init_factory();
590
+ init_couch();
591
+ init_classroomDB2();
592
+ init_courseLookupDB();
593
+ init_logger();
898
594
  }
899
595
  });
900
596
 
901
- // src/core/util/index.ts
902
- function getCardHistoryID(courseID, cardID) {
903
- return `${cardHistoryPrefix}-${courseID}-${cardID}`;
904
- }
905
- var init_util = __esm({
906
- "src/core/util/index.ts"() {
597
+ // src/impl/couch/auth.ts
598
+ var init_auth = __esm({
599
+ "src/impl/couch/auth.ts"() {
907
600
  "use strict";
601
+ init_factory();
908
602
  init_types_legacy();
603
+ init_logger();
909
604
  }
910
605
  });
911
606
 
912
- // src/impl/common/userDBHelpers.ts
913
- import moment from "moment";
914
- function filterAllDocsByPrefix2(db, prefix, opts) {
915
- const options = {
916
- startkey: prefix,
917
- endkey: prefix + "\uFFF0",
918
- include_docs: true
919
- };
920
- if (opts) {
921
- Object.assign(options, opts);
922
- }
923
- return db.allDocs(options);
924
- }
925
- function getStartAndEndKeys2(key) {
926
- return {
927
- startkey: key,
928
- endkey: key + "\uFFF0"
929
- };
930
- }
931
- function getLocalUserDB(username) {
932
- return new pouchdb_setup_default(`userdb-${username}`, {});
933
- }
934
- function scheduleCardReviewLocal(userDB, review) {
935
- const now = moment.utc();
936
- logger.info(`Scheduling for review in: ${review.time.diff(now, "h") / 24} days`);
937
- void userDB.put({
938
- _id: REVIEW_PREFIX + review.time.format(REVIEW_TIME_FORMAT),
939
- cardId: review.card_id,
940
- reviewTime: review.time,
941
- courseId: review.course_id,
942
- scheduledAt: now,
943
- scheduledFor: review.scheduledFor,
944
- schedulingAgentId: review.schedulingAgentId
945
- });
946
- }
947
- async function removeScheduledCardReviewLocal(userDB, reviewDocID) {
948
- const reviewDoc = await userDB.get(reviewDocID);
949
- userDB.remove(reviewDoc).then((res) => {
950
- if (res.ok) {
951
- log(`Removed Review Doc: ${reviewDocID}`);
952
- }
953
- }).catch((err) => {
954
- log(`Failed to remove Review Doc: ${reviewDocID},
955
- ${JSON.stringify(err)}`);
956
- });
957
- }
958
- var REVIEW_PREFIX, REVIEW_TIME_FORMAT, log;
959
- var init_userDBHelpers = __esm({
960
- "src/impl/common/userDBHelpers.ts"() {
607
+ // src/impl/couch/CouchDBSyncStrategy.ts
608
+ import { Status as Status2 } from "@vue-skuilder/common";
609
+ var init_CouchDBSyncStrategy = __esm({
610
+ "src/impl/couch/CouchDBSyncStrategy.ts"() {
961
611
  "use strict";
612
+ init_factory();
613
+ init_types_legacy();
962
614
  init_logger();
615
+ init_common();
963
616
  init_pouchdb_setup();
964
- REVIEW_PREFIX = "card_review_";
965
- REVIEW_TIME_FORMAT = "YYYY-MM-DD--kk:mm:ss-SSS";
966
- log = (s) => {
967
- logger.info(s);
968
- };
617
+ init_couch();
618
+ init_auth();
969
619
  }
970
620
  });
971
621
 
972
- // src/impl/couch/user-course-relDB.ts
973
- import moment2 from "moment";
974
- var UsrCrsData;
975
- var init_user_course_relDB = __esm({
976
- "src/impl/couch/user-course-relDB.ts"() {
622
+ // src/impl/couch/index.ts
623
+ import moment4 from "moment";
624
+ import process2 from "process";
625
+ var isBrowser, GUEST_LOCAL_DB, localUserDB, pouchDBincludeCredentialsConfig;
626
+ var init_couch = __esm({
627
+ "src/impl/couch/index.ts"() {
977
628
  "use strict";
978
- init_couch();
979
- init_courseDB();
629
+ init_factory();
630
+ init_types_legacy();
980
631
  init_logger();
981
- UsrCrsData = class {
982
- user;
983
- course;
984
- _courseId;
985
- constructor(user, courseId) {
986
- this.user = user;
987
- this.course = new CourseDB(courseId, async () => this.user);
988
- this._courseId = courseId;
989
- }
990
- async getReviewsForcast(daysCount) {
991
- const time = moment2.utc().add(daysCount, "days");
992
- return this.getReviewstoDate(time);
993
- }
994
- async getPendingReviews() {
995
- const now = moment2.utc();
996
- return this.getReviewstoDate(now);
997
- }
998
- async getScheduledReviewCount() {
999
- return (await this.getPendingReviews()).length;
1000
- }
1001
- async getCourseSettings() {
1002
- const regDoc = await this.user.getCourseRegistrationsDoc();
1003
- const crsDoc = regDoc.courses.find((c) => c.courseID === this._courseId);
1004
- if (crsDoc && crsDoc.settings) {
1005
- return crsDoc.settings;
1006
- } else {
1007
- logger.warn(`no settings found during lookup on course ${this._courseId}`);
1008
- return {};
1009
- }
1010
- }
1011
- updateCourseSettings(updates) {
1012
- void this.user.updateCourseSettings(this._courseId, updates);
1013
- }
1014
- async getReviewstoDate(targetDate) {
1015
- const keys = getStartAndEndKeys(REVIEW_PREFIX2);
1016
- const reviews = await this.user.remote().allDocs({
1017
- startkey: keys.startkey,
1018
- endkey: keys.endkey,
1019
- include_docs: true
1020
- });
1021
- logger.debug(
1022
- `Fetching ${this.user.getUsername()}'s scheduled reviews for course ${this._courseId}.`
1023
- );
1024
- return reviews.rows.filter((r) => {
1025
- if (r.id.startsWith(REVIEW_PREFIX2)) {
1026
- const date = moment2.utc(r.id.substr(REVIEW_PREFIX2.length), REVIEW_TIME_FORMAT2);
1027
- if (targetDate.isAfter(date)) {
1028
- if (this._courseId === void 0 || r.doc.courseId === this._courseId) {
1029
- return true;
1030
- }
1031
- }
1032
- }
1033
- }).map((r) => r.doc);
632
+ init_pouchdb_setup();
633
+ init_contentSource();
634
+ init_adminDB2();
635
+ init_classroomDB2();
636
+ init_courseAPI();
637
+ init_courseDB();
638
+ init_CouchDBSyncStrategy();
639
+ isBrowser = typeof window !== "undefined";
640
+ if (isBrowser) {
641
+ window.process = process2;
642
+ }
643
+ GUEST_LOCAL_DB = `userdb-${GuestUsername}`;
644
+ localUserDB = new pouchdb_setup_default(GUEST_LOCAL_DB);
645
+ pouchDBincludeCredentialsConfig = {
646
+ fetch(url, opts) {
647
+ opts.credentials = "include";
648
+ return pouchdb_setup_default.fetch(url, opts);
1034
649
  }
1035
650
  };
1036
651
  }
1037
652
  });
1038
653
 
1039
654
  // src/impl/common/BaseUserDB.ts
1040
- import { Status as Status2 } from "@vue-skuilder/common";
1041
- import moment3 from "moment";
655
+ import { Status as Status3 } from "@vue-skuilder/common";
656
+ import moment5 from "moment";
1042
657
  async function getOrCreateClassroomRegistrationsDoc(user) {
1043
658
  let ret;
1044
659
  try {
@@ -1098,7 +713,7 @@ async function updateUserElo(user, course_id, elo) {
1098
713
  return getLocalUserDB(user).put(regDoc);
1099
714
  }
1100
715
  async function registerUserForClassroom(user, classID, registerAs) {
1101
- log2(`Registering user: ${user} in course: ${classID}`);
716
+ log3(`Registering user: ${user} in course: ${classID}`);
1102
717
  return getOrCreateClassroomRegistrationsDoc(user).then((doc) => {
1103
718
  const regItem = {
1104
719
  classID,
@@ -1109,7 +724,7 @@ async function registerUserForClassroom(user, classID, registerAs) {
1109
724
  }).length === 0) {
1110
725
  doc.registrations.push(regItem);
1111
726
  } else {
1112
- log2(`User ${user} is already registered for class ${classID}`);
727
+ log3(`User ${user} is already registered for class ${classID}`);
1113
728
  }
1114
729
  return getLocalUserDB(user).put(doc);
1115
730
  });
@@ -1131,10 +746,11 @@ async function dropUserFromClassroom(user, classID) {
1131
746
  async function getUserClassrooms(user) {
1132
747
  return getOrCreateClassroomRegistrationsDoc(user);
1133
748
  }
1134
- var log2, cardHistoryPrefix2, BaseUser, userCoursesDoc, userClassroomsDoc;
749
+ var log3, BaseUser, userCoursesDoc, userClassroomsDoc;
1135
750
  var init_BaseUserDB = __esm({
1136
751
  "src/impl/common/BaseUserDB.ts"() {
1137
752
  "use strict";
753
+ init_core();
1138
754
  init_util();
1139
755
  init_types_legacy();
1140
756
  init_logger();
@@ -1142,10 +758,9 @@ var init_BaseUserDB = __esm({
1142
758
  init_updateQueue();
1143
759
  init_user_course_relDB();
1144
760
  init_couch();
1145
- log2 = (s) => {
761
+ log3 = (s) => {
1146
762
  logger.info(s);
1147
763
  };
1148
- cardHistoryPrefix2 = "cardH-";
1149
764
  BaseUser = class _BaseUser {
1150
765
  static _instance;
1151
766
  static _initialized = false;
@@ -1166,11 +781,13 @@ var init_BaseUserDB = __esm({
1166
781
  isLoggedIn() {
1167
782
  return !this._username.startsWith(GuestUsername);
1168
783
  }
1169
- remoteDB;
1170
784
  remote() {
1171
785
  return this.remoteDB;
1172
786
  }
1173
787
  localDB;
788
+ remoteDB;
789
+ writeDB;
790
+ // Database to use for write operations (local-first approach)
1174
791
  updateQueue;
1175
792
  async createAccount(username, password) {
1176
793
  if (!this.syncStrategy.canCreateAccount()) {
@@ -1183,10 +800,14 @@ Currently logged-in as ${this._username}.`
1183
800
  );
1184
801
  }
1185
802
  const result = await this.syncStrategy.createAccount(username, password);
1186
- if (result.status === Status2.ok) {
1187
- log2(`Account created successfully, updating username to ${username}`);
803
+ if (result.status === Status3.ok) {
804
+ log3(`Account created successfully, updating username to ${username}`);
1188
805
  this._username = username;
1189
- localStorage.removeItem("dbUUID");
806
+ try {
807
+ localStorage.removeItem("dbUUID");
808
+ } catch (e) {
809
+ logger.warn("localStorage not available (Node.js environment):", e);
810
+ }
1190
811
  await this.init();
1191
812
  }
1192
813
  return {
@@ -1198,15 +819,22 @@ Currently logged-in as ${this._username}.`
1198
819
  if (!this.syncStrategy.canAuthenticate()) {
1199
820
  throw new Error("Authentication not supported by current sync strategy");
1200
821
  }
1201
- if (!this._username.startsWith(GuestUsername)) {
1202
- throw new Error(`Cannot change accounts while logged in.
1203
- Log out of account ${this.getUsername()} before logging in as ${username}.`);
822
+ if (!this._username.startsWith(GuestUsername) && this._username != username) {
823
+ if (this._username != username) {
824
+ throw new Error(`Cannot change accounts while logged in.
825
+ Log out of account ${this.getUsername()} before logging in as ${username}.`);
826
+ }
827
+ logger.warn(`User ${this._username} is already logged in, but executing login again.`);
1204
828
  }
1205
829
  const loginResult = await this.syncStrategy.authenticate(username, password);
1206
830
  if (loginResult.ok) {
1207
- log2(`Logged in as ${username}`);
831
+ log3(`Logged in as ${username}`);
1208
832
  this._username = username;
1209
- localStorage.removeItem("dbUUID");
833
+ try {
834
+ localStorage.removeItem("dbUUID");
835
+ } catch (e) {
836
+ logger.warn("localStorage not available (Node.js environment):", e);
837
+ }
1210
838
  await this.init();
1211
839
  }
1212
840
  return loginResult;
@@ -1214,7 +842,7 @@ Currently logged-in as ${this._username}.`
1214
842
  async resetUserData() {
1215
843
  if (this.syncStrategy.canAuthenticate()) {
1216
844
  return {
1217
- status: Status2.error,
845
+ status: Status3.error,
1218
846
  error: "Reset user data is only available for local-only mode. Use logout instead for remote sync."
1219
847
  };
1220
848
  }
@@ -1223,8 +851,8 @@ Currently logged-in as ${this._username}.`
1223
851
  const allDocs = await localDB.allDocs({ include_docs: false });
1224
852
  const docsToDelete = allDocs.rows.filter((row) => {
1225
853
  const id = row.id;
1226
- return id.startsWith(cardHistoryPrefix2) || // Card interaction history
1227
- id.startsWith(REVIEW_PREFIX) || // Scheduled reviews
854
+ return id.startsWith(DocTypePrefixes["CARDRECORD" /* CARDRECORD */]) || // Card interaction history
855
+ id.startsWith(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */]) || // Scheduled reviews
1228
856
  id === _BaseUser.DOC_IDS.COURSE_REGISTRATIONS || // Course registrations
1229
857
  id === _BaseUser.DOC_IDS.CLASSROOM_REGISTRATIONS || // Classroom registrations
1230
858
  id === _BaseUser.DOC_IDS.CONFIG;
@@ -1233,11 +861,11 @@ Currently logged-in as ${this._username}.`
1233
861
  await localDB.bulkDocs(docsToDelete);
1234
862
  }
1235
863
  await this.init();
1236
- return { status: Status2.ok };
864
+ return { status: Status3.ok };
1237
865
  } catch (error) {
1238
866
  logger.error("Failed to reset user data:", error);
1239
867
  return {
1240
- status: Status2.error,
868
+ status: Status3.error,
1241
869
  error: error instanceof Error ? error.message : "Unknown error during reset"
1242
870
  };
1243
871
  }
@@ -1293,7 +921,7 @@ Currently logged-in as ${this._username}.`
1293
921
  *
1294
922
  */
1295
923
  async getActiveCards() {
1296
- const keys = getStartAndEndKeys2(REVIEW_PREFIX);
924
+ const keys = getStartAndEndKeys(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */]);
1297
925
  const reviews = await this.remoteDB.allDocs({
1298
926
  startkey: keys.startkey,
1299
927
  endkey: keys.endkey,
@@ -1365,18 +993,21 @@ Currently logged-in as ${this._username}.`
1365
993
  }
1366
994
  }
1367
995
  async getReviewstoDate(targetDate, course_id) {
1368
- const keys = getStartAndEndKeys2(REVIEW_PREFIX);
996
+ const keys = getStartAndEndKeys(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */]);
1369
997
  const reviews = await this.remoteDB.allDocs({
1370
998
  startkey: keys.startkey,
1371
999
  endkey: keys.endkey,
1372
1000
  include_docs: true
1373
1001
  });
1374
- log2(
1002
+ log3(
1375
1003
  `Fetching ${this._username}'s scheduled reviews${course_id ? ` for course ${course_id}` : ""}.`
1376
1004
  );
1377
1005
  return reviews.rows.filter((r) => {
1378
- if (r.id.startsWith(REVIEW_PREFIX)) {
1379
- const date = moment3.utc(r.id.substr(REVIEW_PREFIX.length), REVIEW_TIME_FORMAT);
1006
+ if (r.id.startsWith(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */])) {
1007
+ const date = moment5.utc(
1008
+ r.id.substr(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */].length),
1009
+ REVIEW_TIME_FORMAT
1010
+ );
1380
1011
  if (targetDate.isAfter(date)) {
1381
1012
  if (course_id === void 0 || r.doc.courseId === course_id) {
1382
1013
  return true;
@@ -1386,11 +1017,11 @@ Currently logged-in as ${this._username}.`
1386
1017
  }).map((r) => r.doc);
1387
1018
  }
1388
1019
  async getReviewsForcast(daysCount) {
1389
- const time = moment3.utc().add(daysCount, "days");
1020
+ const time = moment5.utc().add(daysCount, "days");
1390
1021
  return this.getReviewstoDate(time);
1391
1022
  }
1392
1023
  async getPendingReviews(course_id) {
1393
- const now = moment3.utc();
1024
+ const now = moment5.utc();
1394
1025
  return this.getReviewstoDate(now, course_id);
1395
1026
  }
1396
1027
  async getScheduledReviewCount(course_id) {
@@ -1433,12 +1064,12 @@ Currently logged-in as ${this._username}.`
1433
1064
  if (doc.courses.filter((course) => {
1434
1065
  return course.courseID === regItem.courseID;
1435
1066
  }).length === 0) {
1436
- log2(`It's a new course registration!`);
1067
+ log3(`It's a new course registration!`);
1437
1068
  doc.courses.push(regItem);
1438
1069
  doc.studyWeight[course_id] = 1;
1439
1070
  } else {
1440
1071
  doc.courses.forEach((c) => {
1441
- log2(`Found the previously registered course!`);
1072
+ log3(`Found the previously registered course!`);
1442
1073
  if (c.courseID === course_id) {
1443
1074
  c.status = status;
1444
1075
  }
@@ -1446,7 +1077,7 @@ Currently logged-in as ${this._username}.`
1446
1077
  }
1447
1078
  return this.localDB.put(doc);
1448
1079
  }).catch((e) => {
1449
- log2(`Registration failed because of: ${JSON.stringify(e)}`);
1080
+ log3(`Registration failed because of: ${JSON.stringify(e)}`);
1450
1081
  throw e;
1451
1082
  });
1452
1083
  }
@@ -1491,7 +1122,8 @@ Currently logged-in as ${this._username}.`
1491
1122
  const defaultConfig = {
1492
1123
  _id: _BaseUser.DOC_IDS.CONFIG,
1493
1124
  darkMode: false,
1494
- likesConfetti: false
1125
+ likesConfetti: false,
1126
+ sessionTimeLimit: 5
1495
1127
  };
1496
1128
  try {
1497
1129
  const cfg = await this.localDB.get(_BaseUser.DOC_IDS.CONFIG);
@@ -1564,10 +1196,15 @@ Currently logged-in as ${this._username}.`
1564
1196
  setDBandQ() {
1565
1197
  this.localDB = getLocalUserDB(this._username);
1566
1198
  this.remoteDB = this.syncStrategy.setupRemoteDB(this._username);
1567
- this.updateQueue = new UpdateQueue(this.localDB);
1199
+ this.writeDB = this.syncStrategy.getWriteDB ? this.syncStrategy.getWriteDB(this._username) : this.localDB;
1200
+ this.updateQueue = new UpdateQueue(this.localDB, this.writeDB);
1568
1201
  }
1569
1202
  async init() {
1570
1203
  _BaseUser._initialized = false;
1204
+ if (this._username === "admin") {
1205
+ _BaseUser._initialized = true;
1206
+ return;
1207
+ }
1571
1208
  this.setDBandQ();
1572
1209
  this.syncStrategy.startSync(this.localDB, this.remoteDB);
1573
1210
  void this.applyDesignDocs();
@@ -1589,6 +1226,9 @@ Currently logged-in as ${this._username}.`
1589
1226
  }
1590
1227
  ];
1591
1228
  async applyDesignDocs() {
1229
+ if (this._username === "admin") {
1230
+ return;
1231
+ }
1592
1232
  for (const doc of _BaseUser.designDocs) {
1593
1233
  try {
1594
1234
  try {
@@ -1605,7 +1245,7 @@ Currently logged-in as ${this._username}.`
1605
1245
  }
1606
1246
  }
1607
1247
  } catch (error) {
1608
- if (error instanceof Error && error.name === "conflict") {
1248
+ if (error.name && error.name === "conflict") {
1609
1249
  logger.warn(`Design doc ${doc._id} update conflict - will retry`);
1610
1250
  await new Promise((resolve) => setTimeout(resolve, 1e3));
1611
1251
  await this.applyDesignDoc(doc);
@@ -1643,7 +1283,7 @@ Currently logged-in as ${this._username}.`
1643
1283
  */
1644
1284
  async putCardRecord(record) {
1645
1285
  const cardHistoryID = getCardHistoryID(record.courseID, record.cardID);
1646
- record.timeStamp = moment3.utc(record.timeStamp).toString();
1286
+ record.timeStamp = moment5.utc(record.timeStamp).toString();
1647
1287
  try {
1648
1288
  const cardHistory = await this.update(
1649
1289
  cardHistoryID,
@@ -1659,7 +1299,7 @@ Currently logged-in as ${this._username}.`
1659
1299
  const ret = {
1660
1300
  ...record2
1661
1301
  };
1662
- ret.timeStamp = moment3.utc(record2.timeStamp);
1302
+ ret.timeStamp = moment5.utc(record2.timeStamp);
1663
1303
  return ret;
1664
1304
  });
1665
1305
  return cardHistory;
@@ -1675,8 +1315,8 @@ Currently logged-in as ${this._username}.`
1675
1315
  streak: 0,
1676
1316
  bestInterval: 0
1677
1317
  };
1678
- void this.remoteDB.put(initCardHistory);
1679
- return initCardHistory;
1318
+ const putResult = await this.writeDB.put(initCardHistory);
1319
+ return { ...initCardHistory, _rev: putResult.rev };
1680
1320
  } else {
1681
1321
  throw new Error(`putCardRecord failed because of:
1682
1322
  name:${reason.name}
@@ -1687,17 +1327,17 @@ Currently logged-in as ${this._username}.`
1687
1327
  }
1688
1328
  async deduplicateReviews() {
1689
1329
  try {
1690
- log2("Starting deduplication of scheduled reviews...");
1330
+ log3("Starting deduplication of scheduled reviews...");
1691
1331
  const reviewsMap = {};
1692
1332
  const duplicateDocIds = [];
1693
1333
  const scheduledReviews = await this.remoteDB.query("reviewCards/reviewCards");
1694
- log2(`Found ${scheduledReviews.rows.length} scheduled reviews to process`);
1334
+ log3(`Found ${scheduledReviews.rows.length} scheduled reviews to process`);
1695
1335
  scheduledReviews.rows.forEach((r) => {
1696
1336
  const qualifiedCardId = r.value;
1697
1337
  const docId = r.key;
1698
1338
  if (reviewsMap[qualifiedCardId]) {
1699
- log2(`Found duplicate scheduled review for card: ${qualifiedCardId}`);
1700
- log2(
1339
+ log3(`Found duplicate scheduled review for card: ${qualifiedCardId}`);
1340
+ log3(
1701
1341
  `Marking earlier review ${reviewsMap[qualifiedCardId]} for deletion, keeping ${docId}`
1702
1342
  );
1703
1343
  duplicateDocIds.push(reviewsMap[qualifiedCardId]);
@@ -1707,23 +1347,23 @@ Currently logged-in as ${this._username}.`
1707
1347
  }
1708
1348
  });
1709
1349
  if (duplicateDocIds.length > 0) {
1710
- log2(`Removing ${duplicateDocIds.length} duplicate reviews...`);
1350
+ log3(`Removing ${duplicateDocIds.length} duplicate reviews...`);
1711
1351
  const deletePromises = duplicateDocIds.map(async (docId) => {
1712
1352
  try {
1713
1353
  const doc = await this.remoteDB.get(docId);
1714
- await this.remoteDB.remove(doc);
1715
- log2(`Successfully removed duplicate review: ${docId}`);
1354
+ await this.writeDB.remove(doc);
1355
+ log3(`Successfully removed duplicate review: ${docId}`);
1716
1356
  } catch (error) {
1717
- log2(`Failed to remove duplicate review ${docId}: ${error}`);
1357
+ log3(`Failed to remove duplicate review ${docId}: ${error}`);
1718
1358
  }
1719
1359
  });
1720
1360
  await Promise.all(deletePromises);
1721
- log2(`Deduplication complete. Processed ${duplicateDocIds.length} duplicates`);
1361
+ log3(`Deduplication complete. Processed ${duplicateDocIds.length} duplicates`);
1722
1362
  } else {
1723
- log2("No duplicate reviews found");
1363
+ log3("No duplicate reviews found");
1724
1364
  }
1725
1365
  } catch (error) {
1726
- log2(`Error during review deduplication: ${error}`);
1366
+ log3(`Error during review deduplication: ${error}`);
1727
1367
  }
1728
1368
  }
1729
1369
  /**
@@ -1733,17 +1373,17 @@ Currently logged-in as ${this._username}.`
1733
1373
  * @param course_id optional specification of individual course
1734
1374
  */
1735
1375
  async getSeenCards(course_id) {
1736
- let prefix = cardHistoryPrefix2;
1376
+ let prefix = DocTypePrefixes["CARDRECORD" /* CARDRECORD */];
1737
1377
  if (course_id) {
1738
1378
  prefix += course_id;
1739
1379
  }
1740
- const docs = await filterAllDocsByPrefix2(this.localDB, prefix, {
1380
+ const docs = await filterAllDocsByPrefix(this.localDB, prefix, {
1741
1381
  include_docs: false
1742
1382
  });
1743
1383
  const ret = [];
1744
1384
  docs.rows.forEach((row) => {
1745
- if (row.id.startsWith(cardHistoryPrefix2)) {
1746
- ret.push(row.id.substr(cardHistoryPrefix2.length));
1385
+ if (row.id.startsWith(DocTypePrefixes["CARDRECORD" /* CARDRECORD */])) {
1386
+ ret.push(row.id.substr(DocTypePrefixes["CARDRECORD" /* CARDRECORD */].length));
1747
1387
  }
1748
1388
  });
1749
1389
  return ret;
@@ -1753,9 +1393,9 @@ Currently logged-in as ${this._username}.`
1753
1393
  * @returns A promise of the cards that the user has seen in the past.
1754
1394
  */
1755
1395
  async getHistory() {
1756
- const cards = await filterAllDocsByPrefix2(
1396
+ const cards = await filterAllDocsByPrefix(
1757
1397
  this.remoteDB,
1758
- cardHistoryPrefix2,
1398
+ DocTypePrefixes["CARDRECORD" /* CARDRECORD */],
1759
1399
  {
1760
1400
  include_docs: true,
1761
1401
  attachments: false
@@ -1796,7 +1436,7 @@ Currently logged-in as ${this._username}.`
1796
1436
  } catch (e) {
1797
1437
  const err = e;
1798
1438
  if (err.status === 404) {
1799
- await this.remoteDB.put({
1439
+ await this.writeDB.put({
1800
1440
  _id: _BaseUser.DOC_IDS.CLASSROOM_REGISTRATIONS,
1801
1441
  registrations: []
1802
1442
  });
@@ -1844,10 +1484,10 @@ Currently logged-in as ${this._username}.`
1844
1484
  }
1845
1485
  }
1846
1486
  async scheduleCardReview(review) {
1847
- return scheduleCardReviewLocal(this.remoteDB, review);
1487
+ return scheduleCardReviewLocal(this.writeDB, review);
1848
1488
  }
1849
1489
  async removeScheduledCardReview(reviewId) {
1850
- return removeScheduledCardReviewLocal(this.remoteDB, reviewId);
1490
+ return removeScheduledCardReviewLocal(this.writeDB, reviewId);
1851
1491
  }
1852
1492
  async registerForClassroom(_classId, _registerAs) {
1853
1493
  return registerUserForClassroom(this._username, _classId, _registerAs);
@@ -1877,298 +1517,17 @@ var init_common = __esm({
1877
1517
  }
1878
1518
  });
1879
1519
 
1880
- // src/impl/couch/courseAPI.ts
1881
- import { NameSpacer } from "@vue-skuilder/common";
1882
- import { blankCourseElo as blankCourseElo2, toCourseElo as toCourseElo2 } from "@vue-skuilder/common";
1883
- import { prepareNote55 } from "@vue-skuilder/common";
1884
- async function addNote55(courseID, codeCourse, shape, data, author, tags, uploads, elo = blankCourseElo2()) {
1885
- const db = getCourseDB2(courseID);
1886
- const payload = prepareNote55(courseID, codeCourse, shape, data, author, tags, uploads);
1887
- const result = await db.post(payload);
1888
- const dataShapeId = NameSpacer.getDataShapeString({
1889
- course: codeCourse,
1890
- dataShape: shape.name
1891
- });
1892
- if (result.ok) {
1893
- try {
1894
- await createCards(courseID, dataShapeId, result.id, tags, elo, author);
1895
- } catch (error) {
1896
- let errorMessage = "Unknown error";
1897
- if (error instanceof Error) {
1898
- errorMessage = error.message;
1899
- } else if (error && typeof error === "object" && "reason" in error) {
1900
- errorMessage = error.reason;
1901
- } else if (error && typeof error === "object" && "message" in error) {
1902
- errorMessage = error.message;
1903
- } else {
1904
- errorMessage = String(error);
1905
- }
1906
- logger.error(`[addNote55] Failed to create cards for note ${result.id}: ${errorMessage}`);
1907
- result.cardCreationFailed = true;
1908
- result.cardCreationError = errorMessage;
1909
- }
1910
- } else {
1911
- logger.error(`[addNote55] Error adding note. Result: ${JSON.stringify(result)}`);
1912
- }
1913
- return result;
1914
- }
1915
- async function createCards(courseID, datashapeID, noteID, tags, elo = blankCourseElo2(), author) {
1916
- const cfg = await getCredentialledCourseConfig(courseID);
1917
- const dsDescriptor = NameSpacer.getDataShapeDescriptor(datashapeID);
1918
- let questionViewTypes = [];
1919
- for (const ds of cfg.dataShapes) {
1920
- if (ds.name === datashapeID) {
1921
- questionViewTypes = ds.questionTypes;
1922
- }
1923
- }
1924
- if (questionViewTypes.length === 0) {
1925
- const errorMsg = `No questionViewTypes found for datashapeID: ${datashapeID} in course config. Cards cannot be created.`;
1926
- logger.error(errorMsg);
1927
- throw new Error(errorMsg);
1928
- }
1929
- for (const questionView of questionViewTypes) {
1930
- await createCard(questionView, courseID, dsDescriptor, noteID, tags, elo, author);
1931
- }
1932
- }
1933
- async function createCard(questionViewName, courseID, dsDescriptor, noteID, tags, elo = blankCourseElo2(), author) {
1934
- const qDescriptor = NameSpacer.getQuestionDescriptor(questionViewName);
1935
- const cfg = await getCredentialledCourseConfig(courseID);
1936
- for (const rQ of cfg.questionTypes) {
1937
- if (rQ.name === questionViewName) {
1938
- for (const view of rQ.viewList) {
1939
- await addCard(
1940
- courseID,
1941
- dsDescriptor.course,
1942
- [noteID],
1943
- NameSpacer.getViewString({
1944
- course: qDescriptor.course,
1945
- questionType: qDescriptor.questionType,
1946
- view
1947
- }),
1948
- elo,
1949
- tags,
1950
- author
1951
- );
1952
- }
1953
- }
1954
- }
1955
- }
1956
- async function addCard(courseID, course, id_displayable_data, id_view, elo, tags, author) {
1957
- const db = getCourseDB2(courseID);
1958
- const card = await db.post({
1959
- course,
1960
- id_displayable_data,
1961
- id_view,
1962
- docType: "CARD" /* CARD */,
1963
- elo: elo || toCourseElo2(990 + Math.round(20 * Math.random())),
1964
- author
1965
- });
1966
- for (const tag of tags) {
1967
- logger.info(`adding tag: ${tag} to card ${card.id}`);
1968
- await addTagToCard(courseID, card.id, tag, author, false);
1969
- }
1970
- return card;
1971
- }
1972
- async function getCredentialledCourseConfig(courseID) {
1973
- try {
1974
- const db = getCourseDB2(courseID);
1975
- const ret = await db.get("CourseConfig");
1976
- ret.courseID = courseID;
1977
- logger.info(`Returning course config: ${JSON.stringify(ret)}`);
1978
- return ret;
1979
- } catch (e) {
1980
- logger.error(`Error fetching config for ${courseID}:`, e);
1981
- throw e;
1982
- }
1983
- }
1984
- async function addTagToCard(courseID, cardID, tagID, author, updateELO = true) {
1985
- const prefixedTagID = getTagID(tagID);
1986
- const courseDB = getCourseDB2(courseID);
1987
- const courseApi = new CourseDB(courseID, async () => {
1988
- const dummySyncStrategy = {
1989
- setupRemoteDB: () => null,
1990
- startSync: () => {
1991
- },
1992
- canCreateAccount: () => false,
1993
- canAuthenticate: () => false,
1994
- getCurrentUsername: async () => "DummyUser"
1995
- };
1996
- return BaseUser.Dummy(dummySyncStrategy);
1997
- });
1998
- try {
1999
- logger.info(`Applying tag ${tagID} to card ${courseID + "-" + cardID}...`);
2000
- const tag = await courseDB.get(prefixedTagID);
2001
- if (!tag.taggedCards.includes(cardID)) {
2002
- tag.taggedCards.push(cardID);
2003
- if (updateELO) {
2004
- try {
2005
- const eloData = await courseApi.getCardEloData([cardID]);
2006
- const elo = eloData[0];
2007
- elo.tags[tagID] = {
2008
- count: 0,
2009
- score: elo.global.score
2010
- // todo: or 1000?
2011
- };
2012
- await updateCardElo(courseID, cardID, elo);
2013
- } catch (error) {
2014
- logger.error("Failed to update ELO data for card:", cardID, error);
2015
- }
2016
- }
2017
- return courseDB.put(tag);
2018
- } else throw new AlreadyTaggedErr(`Card ${cardID} is already tagged with ${tagID}`);
2019
- } catch (e) {
2020
- if (e instanceof AlreadyTaggedErr) {
2021
- throw e;
2022
- }
2023
- await createTag(courseID, tagID, author);
2024
- return addTagToCard(courseID, cardID, tagID, author, updateELO);
2025
- }
2026
- }
2027
- async function updateCardElo(courseID, cardID, elo) {
2028
- if (elo) {
2029
- const cDB = getCourseDB2(courseID);
2030
- const card = await cDB.get(cardID);
2031
- logger.debug(`Replacing ${JSON.stringify(card.elo)} with ${JSON.stringify(elo)}`);
2032
- card.elo = elo;
2033
- return cDB.put(card);
2034
- }
2035
- }
2036
- function getTagID(tagName) {
2037
- const tagPrefix = "TAG" /* TAG */.valueOf() + "-";
2038
- if (tagName.indexOf(tagPrefix) === 0) {
2039
- return tagName;
2040
- } else {
2041
- return tagPrefix + tagName;
2042
- }
2043
- }
2044
- function getCourseDB2(courseID) {
2045
- const dbName = `coursedb-${courseID}`;
2046
- return new pouchdb_setup_default(
2047
- ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + dbName,
2048
- pouchDBincludeCredentialsConfig
2049
- );
2050
- }
2051
- var AlreadyTaggedErr;
2052
- var init_courseAPI = __esm({
2053
- "src/impl/couch/courseAPI.ts"() {
2054
- "use strict";
2055
- init_pouchdb_setup();
2056
- init_couch();
2057
- init_factory();
2058
- init_courseDB();
2059
- init_types_legacy();
2060
- init_common();
2061
- init_logger();
2062
- AlreadyTaggedErr = class extends Error {
2063
- constructor(message) {
2064
- super(message);
2065
- this.name = "AlreadyTaggedErr";
2066
- }
2067
- };
2068
- }
2069
- });
2070
-
2071
- // src/impl/couch/auth.ts
2072
- var init_auth = __esm({
2073
- "src/impl/couch/auth.ts"() {
2074
- "use strict";
2075
- init_factory();
2076
- init_types_legacy();
2077
- init_logger();
2078
- }
2079
- });
2080
-
2081
- // src/impl/couch/CouchDBSyncStrategy.ts
2082
- import { Status as Status3 } from "@vue-skuilder/common";
2083
- var init_CouchDBSyncStrategy = __esm({
2084
- "src/impl/couch/CouchDBSyncStrategy.ts"() {
1520
+ // src/factory.ts
1521
+ var ENV;
1522
+ var init_factory = __esm({
1523
+ "src/factory.ts"() {
2085
1524
  "use strict";
2086
- init_factory();
2087
- init_types_legacy();
2088
- init_logger();
2089
1525
  init_common();
2090
- init_pouchdb_setup();
2091
- init_couch();
2092
- init_auth();
2093
- }
2094
- });
2095
-
2096
- // src/impl/couch/index.ts
2097
- import moment4 from "moment";
2098
- import process2 from "process";
2099
- function getCourseDB(courseID) {
2100
- return new pouchdb_setup_default(
2101
- ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + "coursedb-" + courseID,
2102
- pouchDBincludeCredentialsConfig
2103
- );
2104
- }
2105
- function getCourseDocs(courseID, docIDs, options = {}) {
2106
- return getCourseDB(courseID).allDocs({
2107
- ...options,
2108
- keys: docIDs
2109
- });
2110
- }
2111
- function getCourseDoc(courseID, docID, options = {}) {
2112
- return getCourseDB(courseID).get(docID, options);
2113
- }
2114
- function filterAllDocsByPrefix(db, prefix, opts) {
2115
- const options = {
2116
- startkey: prefix,
2117
- endkey: prefix + "\uFFF0",
2118
- include_docs: true
2119
- };
2120
- if (opts) {
2121
- Object.assign(options, opts);
2122
- }
2123
- return db.allDocs(options);
2124
- }
2125
- function getStartAndEndKeys(key) {
2126
- return {
2127
- startkey: key,
2128
- endkey: key + "\uFFF0"
2129
- };
2130
- }
2131
- var isBrowser, GUEST_LOCAL_DB, localUserDB, pouchDBincludeCredentialsConfig, REVIEW_PREFIX2, REVIEW_TIME_FORMAT2;
2132
- var init_couch = __esm({
2133
- "src/impl/couch/index.ts"() {
2134
- "use strict";
2135
- init_factory();
2136
- init_types_legacy();
2137
1526
  init_logger();
2138
- init_pouchdb_setup();
2139
- init_contentSource();
2140
- init_adminDB2();
2141
- init_classroomDB2();
2142
- init_courseAPI();
2143
- init_courseDB();
2144
- init_CouchDBSyncStrategy();
2145
- isBrowser = typeof window !== "undefined";
2146
- if (isBrowser) {
2147
- window.process = process2;
2148
- }
2149
- GUEST_LOCAL_DB = `userdb-${GuestUsername}`;
2150
- localUserDB = new pouchdb_setup_default(GUEST_LOCAL_DB);
2151
- pouchDBincludeCredentialsConfig = {
2152
- fetch(url, opts) {
2153
- opts.credentials = "include";
2154
- return pouchdb_setup_default.fetch(url, opts);
2155
- }
1527
+ ENV = {
1528
+ COUCHDB_SERVER_PROTOCOL: "NOT_SET",
1529
+ COUCHDB_SERVER_URL: "NOT_SET"
2156
1530
  };
2157
- REVIEW_PREFIX2 = "card_review_";
2158
- REVIEW_TIME_FORMAT2 = "YYYY-MM-DD--kk:mm:ss-SSS";
2159
- }
2160
- });
2161
-
2162
- // src/impl/couch/classroomDB.ts
2163
- import moment5 from "moment";
2164
- var init_classroomDB2 = __esm({
2165
- "src/impl/couch/classroomDB.ts"() {
2166
- "use strict";
2167
- init_factory();
2168
- init_logger();
2169
- init_pouchdb_setup();
2170
- init_couch();
2171
- init_courseDB();
2172
1531
  }
2173
1532
  });
2174
1533
 
@@ -2269,11 +1628,11 @@ var init_StaticDataUnpacker = __esm({
2269
1628
  init_logger();
2270
1629
  init_core();
2271
1630
  pathUtils = {
2272
- isAbsolute: (path) => {
2273
- if (/^[a-zA-Z]:[\\/]/.test(path) || /^\\\\/.test(path)) {
1631
+ isAbsolute: (path2) => {
1632
+ if (/^[a-zA-Z]:[\\/]/.test(path2) || /^\\\\/.test(path2)) {
2274
1633
  return true;
2275
1634
  }
2276
- if (path.startsWith("/")) {
1635
+ if (path2.startsWith("/")) {
2277
1636
  return true;
2278
1637
  }
2279
1638
  return false;
@@ -2358,18 +1717,21 @@ var init_StaticDataUnpacker = __esm({
2358
1717
  async getTagsIndex() {
2359
1718
  return await this.loadIndex("tags");
2360
1719
  }
1720
+ getDocTypeFromId(id) {
1721
+ for (const docTypeKey in DocTypePrefixes) {
1722
+ const prefix = DocTypePrefixes[docTypeKey];
1723
+ if (id.startsWith(`${prefix}-`)) {
1724
+ return docTypeKey;
1725
+ }
1726
+ }
1727
+ return void 0;
1728
+ }
2361
1729
  /**
2362
1730
  * Find which chunk contains a specific document ID
2363
1731
  */
2364
1732
  async findChunkForDocument(docId) {
2365
- let expectedDocType = void 0;
2366
- for (const docType of Object.values(DocType)) {
2367
- if (docId.startsWith(`${docType}-`)) {
2368
- expectedDocType = docType;
2369
- break;
2370
- }
2371
- }
2372
- if (expectedDocType !== void 0) {
1733
+ const expectedDocType = this.getDocTypeFromId(docId);
1734
+ if (expectedDocType) {
2373
1735
  const typeChunks = this.manifest.chunks.filter((c) => c.docType === expectedDocType);
2374
1736
  for (const chunk of typeChunks) {
2375
1737
  if (docId >= chunk.startKey && docId <= chunk.endKey) {
@@ -2379,21 +1741,8 @@ var init_StaticDataUnpacker = __esm({
2379
1741
  }
2380
1742
  }
2381
1743
  }
2382
- return void 0;
2383
1744
  } else {
2384
- const displayableChunks = this.manifest.chunks.filter(
2385
- (c) => c.docType === "DISPLAYABLE_DATA"
2386
- );
2387
- for (const chunk of displayableChunks) {
2388
- if (docId >= chunk.startKey && docId <= chunk.endKey) {
2389
- const exists = await this.verifyDocumentInChunk(docId, chunk);
2390
- if (exists) {
2391
- return chunk;
2392
- }
2393
- }
2394
- }
2395
- const cardChunks = this.manifest.chunks.filter((c) => c.docType === "CARD");
2396
- for (const chunk of cardChunks) {
1745
+ for (const chunk of this.manifest.chunks) {
2397
1746
  if (docId >= chunk.startKey && docId <= chunk.endKey) {
2398
1747
  const exists = await this.verifyDocumentInChunk(docId, chunk);
2399
1748
  if (exists) {
@@ -2414,6 +1763,7 @@ var init_StaticDataUnpacker = __esm({
2414
1763
  }
2415
1764
  return void 0;
2416
1765
  }
1766
+ return void 0;
2417
1767
  }
2418
1768
  /**
2419
1769
  * Verify that a document actually exists in a specific chunk by loading and checking it
@@ -2657,6 +2007,7 @@ var init_courseDB3 = __esm({
2657
2007
  "use strict";
2658
2008
  init_types_legacy();
2659
2009
  init_navigators();
2010
+ init_logger();
2660
2011
  StaticCourseDB = class {
2661
2012
  constructor(courseId, unpacker, userDB, manifest) {
2662
2013
  this.courseId = courseId;
@@ -2678,10 +2029,11 @@ var init_courseDB3 = __esm({
2678
2029
  throw new Error("Cannot update course config in static mode");
2679
2030
  }
2680
2031
  async getCourseInfo() {
2032
+ const cardCount = this.manifest.chunks.filter((chunk) => chunk.docType === "CARD" /* CARD */).reduce((total, chunk) => total + chunk.documentCount, 0);
2681
2033
  return {
2682
- cardCount: 0,
2683
- // Would come from manifest
2034
+ cardCount,
2684
2035
  registeredUsers: 0
2036
+ // Always 0 in static mode
2685
2037
  };
2686
2038
  }
2687
2039
  async getCourseDoc(id, _options) {
@@ -2770,12 +2122,56 @@ var init_courseDB3 = __esm({
2770
2122
  courseID: this.courseId
2771
2123
  }));
2772
2124
  }
2773
- async getAppliedTags(_cardId) {
2774
- return {
2775
- total_rows: 0,
2776
- offset: 0,
2777
- rows: []
2778
- };
2125
+ async getAppliedTags(cardId) {
2126
+ try {
2127
+ const tagsIndex = await this.unpacker.getTagsIndex();
2128
+ const cardTags = tagsIndex.byCard[cardId] || [];
2129
+ const rows = await Promise.all(
2130
+ cardTags.map(async (tagName) => {
2131
+ const tagId = `${"TAG" /* TAG */}-${tagName}`;
2132
+ try {
2133
+ const tagDoc = await this.unpacker.getDocument(tagId);
2134
+ return {
2135
+ id: tagId,
2136
+ key: cardId,
2137
+ value: {
2138
+ name: tagDoc.name,
2139
+ snippet: tagDoc.snippet,
2140
+ count: tagDoc.taggedCards?.length || 0
2141
+ }
2142
+ };
2143
+ } catch (error) {
2144
+ if (error && error.status === 404) {
2145
+ logger.warn(`Tag document not found for ${tagName}, creating stub`);
2146
+ } else {
2147
+ logger.error(`Error getting tag document for ${tagName}:`, error);
2148
+ throw error;
2149
+ }
2150
+ return {
2151
+ id: tagId,
2152
+ key: cardId,
2153
+ value: {
2154
+ name: tagName,
2155
+ snippet: `Tag: ${tagName}`,
2156
+ count: tagsIndex.byTag[tagName]?.length || 0
2157
+ }
2158
+ };
2159
+ }
2160
+ })
2161
+ );
2162
+ return {
2163
+ total_rows: rows.length,
2164
+ offset: 0,
2165
+ rows
2166
+ };
2167
+ } catch (error) {
2168
+ logger.error(`Error getting applied tags for card ${cardId}:`, error);
2169
+ return {
2170
+ total_rows: 0,
2171
+ offset: 0,
2172
+ rows: []
2173
+ };
2174
+ }
2779
2175
  }
2780
2176
  async addTagToCard(_cardId, _tagId) {
2781
2177
  throw new Error("Cannot modify tags in static mode");
@@ -2793,11 +2189,69 @@ var init_courseDB3 = __esm({
2793
2189
  throw new Error("Cannot update tags in static mode");
2794
2190
  }
2795
2191
  async getCourseTagStubs() {
2796
- return {
2797
- total_rows: 0,
2798
- offset: 0,
2799
- rows: []
2800
- };
2192
+ try {
2193
+ const tagsIndex = await this.unpacker.getTagsIndex();
2194
+ if (!tagsIndex || !tagsIndex.byTag) {
2195
+ logger.warn("Tags index not found or empty");
2196
+ return {
2197
+ total_rows: 0,
2198
+ offset: 0,
2199
+ rows: []
2200
+ };
2201
+ }
2202
+ const tagNames = Object.keys(tagsIndex.byTag);
2203
+ const rows = await Promise.all(
2204
+ tagNames.map(async (tagName) => {
2205
+ const cardIds = tagsIndex.byTag[tagName] || [];
2206
+ const tagId = `${"TAG" /* TAG */}-${tagName}`;
2207
+ try {
2208
+ const tagDoc = await this.unpacker.getDocument(tagId);
2209
+ return {
2210
+ id: tagId,
2211
+ key: tagId,
2212
+ value: { rev: "1-static" },
2213
+ doc: tagDoc
2214
+ };
2215
+ } catch (error) {
2216
+ if (error && error.status === 404) {
2217
+ logger.warn(`Tag document not found for ${tagName}, creating stub`);
2218
+ const stubDoc = {
2219
+ _id: tagId,
2220
+ _rev: "1-static",
2221
+ course: this.courseId,
2222
+ docType: "TAG" /* TAG */,
2223
+ name: tagName,
2224
+ snippet: `Tag: ${tagName}`,
2225
+ wiki: "",
2226
+ taggedCards: cardIds,
2227
+ author: "system"
2228
+ };
2229
+ return {
2230
+ id: tagId,
2231
+ key: tagId,
2232
+ value: { rev: "1-static" },
2233
+ doc: stubDoc
2234
+ };
2235
+ } else {
2236
+ logger.error(`Error getting tag document for ${tagName}:`, error);
2237
+ throw error;
2238
+ }
2239
+ }
2240
+ })
2241
+ );
2242
+ return {
2243
+ total_rows: rows.length,
2244
+ offset: 0,
2245
+ rows
2246
+ };
2247
+ } catch (error) {
2248
+ logger.error("Failed to get course tag stubs:", error);
2249
+ return {
2250
+ total_rows: 0,
2251
+ offset: 0,
2252
+ rows: []
2253
+ };
2254
+ }
2801
2255
  }
2802
2256
  async addNote(_codeCourse, _shape, _data, _author, _tags, _uploads, _elo) {
2803
2257
  return {
@@ -2903,6 +2357,9 @@ var init_NoOpSyncStrategy = __esm({
2903
2357
  setupRemoteDB(username) {
2904
2358
  return getLocalUserDB(username);
2905
2359
  }
2360
+ getWriteDB(username) {
2361
+ return getLocalUserDB(username);
2362
+ }
2906
2363
  startSync(_localDB, _remoteDB) {
2907
2364
  }
2908
2365
  stopSync() {