@vue-skuilder/db 0.1.7 → 0.1.8-0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/{SyncStrategy-DnJRj-Xp.d.mts → SyncStrategy-CyATpyLQ.d.mts} +6 -0
  2. package/dist/{SyncStrategy-DnJRj-Xp.d.ts → SyncStrategy-CyATpyLQ.d.ts} +6 -0
  3. package/dist/core/index.d.mts +5 -5
  4. package/dist/core/index.d.ts +5 -5
  5. package/dist/core/index.js +131 -118
  6. package/dist/core/index.js.map +1 -1
  7. package/dist/core/index.mjs +128 -115
  8. package/dist/core/index.mjs.map +1 -1
  9. package/dist/{dataLayerProvider-BbW9EnZK.d.mts → dataLayerProvider-BInqI_RF.d.mts} +1 -1
  10. package/dist/{dataLayerProvider-6stCgDME.d.ts → dataLayerProvider-DqtNroSh.d.ts} +1 -1
  11. package/dist/impl/couch/index.d.mts +6 -6
  12. package/dist/impl/couch/index.d.ts +6 -6
  13. package/dist/impl/couch/index.js +1365 -1252
  14. package/dist/impl/couch/index.js.map +1 -1
  15. package/dist/impl/couch/index.mjs +1359 -1246
  16. package/dist/impl/couch/index.mjs.map +1 -1
  17. package/dist/impl/static/index.d.mts +8 -6
  18. package/dist/impl/static/index.d.ts +8 -6
  19. package/dist/impl/static/index.js +253 -843
  20. package/dist/impl/static/index.js.map +1 -1
  21. package/dist/impl/static/index.mjs +250 -842
  22. package/dist/impl/static/index.mjs.map +1 -1
  23. package/dist/index-CLL31bEy.d.ts +137 -0
  24. package/dist/index-CUNnL38E.d.mts +137 -0
  25. package/dist/index.d.mts +10 -55
  26. package/dist/index.d.ts +10 -55
  27. package/dist/index.js +343 -170
  28. package/dist/index.js.map +1 -1
  29. package/dist/index.mjs +340 -167
  30. package/dist/index.mjs.map +1 -1
  31. package/dist/{types-BvzcRAys.d.ts → types-BefDGkKa.d.ts} +1 -1
  32. package/dist/{types-CQQ80R5N.d.mts → types-DC-ckZug.d.mts} +1 -1
  33. package/dist/{types-legacy-CtrmkOLu.d.mts → types-legacy-Birv-Jx6.d.mts} +2 -2
  34. package/dist/{types-legacy-CtrmkOLu.d.ts → types-legacy-Birv-Jx6.d.ts} +2 -2
  35. package/dist/{userDB-DUY63VMN.d.ts → userDB-C33Hzjgn.d.mts} +10 -3
  36. package/dist/{userDB-7fM4tpgr.d.mts → userDB-DusL7OXe.d.ts} +10 -3
  37. package/dist/util/packer/index.d.mts +3 -63
  38. package/dist/util/packer/index.d.ts +3 -63
  39. package/dist/util/packer/index.js +53 -1
  40. package/dist/util/packer/index.js.map +1 -1
  41. package/dist/util/packer/index.mjs +53 -1
  42. package/dist/util/packer/index.mjs.map +1 -1
  43. package/package.json +7 -4
  44. package/src/core/types/types-legacy.ts +13 -1
  45. package/src/core/types/user.ts +9 -2
  46. package/src/core/util/index.ts +5 -4
  47. package/src/impl/common/BaseUserDB.ts +33 -22
  48. package/src/impl/common/SyncStrategy.ts +7 -0
  49. package/src/impl/common/index.ts +0 -1
  50. package/src/impl/common/userDBHelpers.ts +4 -4
  51. package/src/impl/couch/CouchDBSyncStrategy.ts +10 -0
  52. package/src/impl/couch/courseAPI.ts +7 -6
  53. package/src/impl/couch/index.ts +10 -5
  54. package/src/impl/couch/updateQueue.ts +12 -8
  55. package/src/impl/couch/user-course-relDB.ts +17 -27
  56. package/src/impl/static/NoOpSyncStrategy.ts +5 -0
  57. package/src/impl/static/StaticDataUnpacker.ts +18 -36
  58. package/src/impl/static/courseDB.ts +135 -17
  59. package/src/util/migrator/FileSystemAdapter.ts +20 -0
  60. package/src/util/migrator/StaticToCouchDBMigrator.ts +6 -0
  61. package/src/util/packer/CouchDBToStaticPacker.ts +92 -2
package/dist/index.mjs CHANGED
@@ -80,7 +80,7 @@ var init_logger = __esm({
80
80
  });
81
81
 
82
82
  // src/core/types/types-legacy.ts
83
- var GuestUsername, log, DocType, cardHistoryPrefix;
83
+ var GuestUsername, log, DocType, DocTypePrefixes;
84
84
  var init_types_legacy = __esm({
85
85
  "src/core/types/types-legacy.ts"() {
86
86
  "use strict";
@@ -89,20 +89,32 @@ var init_types_legacy = __esm({
89
89
  log = (message) => {
90
90
  logger.log(message);
91
91
  };
92
- DocType = /* @__PURE__ */ ((DocType2) => {
93
- DocType2["DISPLAYABLE_DATA"] = "DISPLAYABLE_DATA";
94
- DocType2["CARD"] = "CARD";
95
- DocType2["DATASHAPE"] = "DATASHAPE";
96
- DocType2["QUESTIONTYPE"] = "QUESTION";
97
- DocType2["VIEW"] = "VIEW";
98
- DocType2["PEDAGOGY"] = "PEDAGOGY";
99
- DocType2["CARDRECORD"] = "CARDRECORD";
100
- DocType2["SCHEDULED_CARD"] = "SCHEDULED_CARD";
101
- DocType2["TAG"] = "TAG";
102
- DocType2["NAVIGATION_STRATEGY"] = "NAVIGATION_STRATEGY";
103
- return DocType2;
92
+ DocType = /* @__PURE__ */ ((DocType3) => {
93
+ DocType3["DISPLAYABLE_DATA"] = "DISPLAYABLE_DATA";
94
+ DocType3["CARD"] = "CARD";
95
+ DocType3["DATASHAPE"] = "DATASHAPE";
96
+ DocType3["QUESTIONTYPE"] = "QUESTION";
97
+ DocType3["VIEW"] = "VIEW";
98
+ DocType3["PEDAGOGY"] = "PEDAGOGY";
99
+ DocType3["CARDRECORD"] = "CARDRECORD";
100
+ DocType3["SCHEDULED_CARD"] = "SCHEDULED_CARD";
101
+ DocType3["TAG"] = "TAG";
102
+ DocType3["NAVIGATION_STRATEGY"] = "NAVIGATION_STRATEGY";
103
+ return DocType3;
104
104
  })(DocType || {});
105
- cardHistoryPrefix = "cardH";
105
+ DocTypePrefixes = {
106
+ ["CARD" /* CARD */]: "c",
107
+ ["DISPLAYABLE_DATA" /* DISPLAYABLE_DATA */]: "dd",
108
+ ["TAG" /* TAG */]: "TAG",
109
+ ["CARDRECORD" /* CARDRECORD */]: "cardH",
110
+ ["SCHEDULED_CARD" /* SCHEDULED_CARD */]: "card_review_",
111
+ // Add other doctypes here as they get prefixed IDs
112
+ ["DATASHAPE" /* DATASHAPE */]: "DATASHAPE",
113
+ ["QUESTION" /* QUESTIONTYPE */]: "QUESTION",
114
+ ["VIEW" /* VIEW */]: "VIEW",
115
+ ["PEDAGOGY" /* PEDAGOGY */]: "PEDAGOGY",
116
+ ["NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */]: "NAVIGATION_STRATEGY"
117
+ };
106
118
  }
107
119
  });
108
120
 
@@ -114,16 +126,16 @@ function isQuestionRecord(c) {
114
126
  return c.userAnswer !== void 0;
115
127
  }
116
128
  function getCardHistoryID(courseID, cardID) {
117
- return `${cardHistoryPrefix}-${courseID}-${cardID}`;
129
+ return `${DocTypePrefixes["CARDRECORD" /* CARDRECORD */]}-${courseID}-${cardID}`;
118
130
  }
119
131
  function parseCardHistoryID(id) {
120
132
  const split = id.split("-");
121
133
  let error = "";
122
134
  error += split.length === 3 ? "" : `
123
135
  given ID has incorrect number of '-' characters`;
124
- error += split[0] === cardHistoryPrefix ? "" : `
125
- given ID does not start with ${cardHistoryPrefix}`;
126
- if (split.length === 3 && split[0] === cardHistoryPrefix) {
136
+ error += split[0] === DocTypePrefixes["CARDRECORD" /* CARDRECORD */] ? "" : `
137
+ given ID does not start with ${DocTypePrefixes["CARDRECORD" /* CARDRECORD */]}`;
138
+ if (split.length === 3 && split[0] === DocTypePrefixes["CARDRECORD" /* CARDRECORD */]) {
127
139
  return {
128
140
  courseID: split[1],
129
141
  cardID: split[2]
@@ -341,11 +353,11 @@ function scheduleCardReviewLocal(userDB, review) {
341
353
  const now = moment.utc();
342
354
  logger.info(`Scheduling for review in: ${review.time.diff(now, "h") / 24} days`);
343
355
  void userDB.put({
344
- _id: REVIEW_PREFIX + review.time.format(REVIEW_TIME_FORMAT),
356
+ _id: DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */] + review.time.format(REVIEW_TIME_FORMAT),
345
357
  cardId: review.card_id,
346
- reviewTime: review.time,
358
+ reviewTime: review.time.toISOString(),
347
359
  courseId: review.course_id,
348
- scheduledAt: now,
360
+ scheduledAt: now.toISOString(),
349
361
  scheduledFor: review.scheduledFor,
350
362
  schedulingAgentId: review.schedulingAgentId
351
363
  });
@@ -361,14 +373,14 @@ async function removeScheduledCardReviewLocal(userDB, reviewDocID) {
361
373
  ${JSON.stringify(err)}`);
362
374
  });
363
375
  }
364
- var REVIEW_PREFIX, REVIEW_TIME_FORMAT, log2;
376
+ var REVIEW_TIME_FORMAT, log2;
365
377
  var init_userDBHelpers = __esm({
366
378
  "src/impl/common/userDBHelpers.ts"() {
367
379
  "use strict";
380
+ init_core();
368
381
  init_logger();
369
382
  init_pouchdb_setup();
370
383
  init_dataDirectory();
371
- REVIEW_PREFIX = "card_review_";
372
384
  REVIEW_TIME_FORMAT = "YYYY-MM-DD--kk:mm:ss-SSS";
373
385
  log2 = (s) => {
374
386
  logger.info(s);
@@ -403,7 +415,10 @@ var init_updateQueue = __esm({
403
415
  _className = "UpdateQueue";
404
416
  pendingUpdates = {};
405
417
  inprogressUpdates = {};
406
- db;
418
+ readDB;
419
+ // Database for read operations
420
+ writeDB;
421
+ // Database for write operations (local-first)
407
422
  update(id, update) {
408
423
  logger.debug(`Update requested on doc: ${id}`);
409
424
  if (this.pendingUpdates[id]) {
@@ -413,24 +428,25 @@ var init_updateQueue = __esm({
413
428
  }
414
429
  return this.applyUpdates(id);
415
430
  }
416
- constructor(db) {
431
+ constructor(readDB, writeDB) {
417
432
  super();
418
- this.db = db;
433
+ this.readDB = readDB;
434
+ this.writeDB = writeDB || readDB;
419
435
  logger.debug(`UpdateQ initialized...`);
420
- void this.db.info().then((i) => {
436
+ void this.readDB.info().then((i) => {
421
437
  logger.debug(`db info: ${JSON.stringify(i)}`);
422
438
  });
423
439
  }
424
440
  async applyUpdates(id) {
425
441
  logger.debug(`Applying updates on doc: ${id}`);
426
442
  if (this.inprogressUpdates[id]) {
427
- await this.db.info();
443
+ await this.readDB.info();
428
444
  return this.applyUpdates(id);
429
445
  } else {
430
446
  if (this.pendingUpdates[id] && this.pendingUpdates[id].length > 0) {
431
447
  this.inprogressUpdates[id] = true;
432
448
  try {
433
- let doc = await this.db.get(id);
449
+ let doc = await this.readDB.get(id);
434
450
  logger.debug(`Retrieved doc: ${id}`);
435
451
  while (this.pendingUpdates[id].length !== 0) {
436
452
  const update = this.pendingUpdates[id].splice(0, 1)[0];
@@ -443,7 +459,7 @@ var init_updateQueue = __esm({
443
459
  };
444
460
  }
445
461
  }
446
- await this.db.put(doc);
462
+ await this.writeDB.put(doc);
447
463
  logger.debug(`Put doc: ${id}`);
448
464
  if (this.pendingUpdates[id].length === 0) {
449
465
  this.inprogressUpdates[id] = false;
@@ -469,6 +485,60 @@ var init_updateQueue = __esm({
469
485
  }
470
486
  });
471
487
 
488
+ // src/impl/couch/user-course-relDB.ts
489
+ import moment2 from "moment";
490
+ var UsrCrsData;
491
+ var init_user_course_relDB = __esm({
492
+ "src/impl/couch/user-course-relDB.ts"() {
493
+ "use strict";
494
+ init_logger();
495
+ UsrCrsData = class {
496
+ user;
497
+ _courseId;
498
+ constructor(user, courseId) {
499
+ this.user = user;
500
+ this._courseId = courseId;
501
+ }
502
+ async getReviewsForcast(daysCount) {
503
+ const time = moment2.utc().add(daysCount, "days");
504
+ return this.getReviewstoDate(time);
505
+ }
506
+ async getPendingReviews() {
507
+ const now = moment2.utc();
508
+ return this.getReviewstoDate(now);
509
+ }
510
+ async getScheduledReviewCount() {
511
+ return (await this.getPendingReviews()).length;
512
+ }
513
+ async getCourseSettings() {
514
+ const regDoc = await this.user.getCourseRegistrationsDoc();
515
+ const crsDoc = regDoc.courses.find((c) => c.courseID === this._courseId);
516
+ if (crsDoc && crsDoc.settings) {
517
+ return crsDoc.settings;
518
+ } else {
519
+ logger.warn(`no settings found during lookup on course ${this._courseId}`);
520
+ return {};
521
+ }
522
+ }
523
+ updateCourseSettings(updates) {
524
+ if ("updateCourseSettings" in this.user) {
525
+ void this.user.updateCourseSettings(this._courseId, updates);
526
+ }
527
+ }
528
+ async getReviewstoDate(targetDate) {
529
+ const allReviews = await this.user.getPendingReviews(this._courseId);
530
+ logger.debug(
531
+ `Fetching ${this.user.getUsername()}'s scheduled reviews for course ${this._courseId}.`
532
+ );
533
+ return allReviews.filter((review) => {
534
+ const reviewTime = moment2.utc(review.reviewTime);
535
+ return targetDate.isAfter(reviewTime);
536
+ });
537
+ }
538
+ };
539
+ }
540
+ });
541
+
472
542
  // src/impl/couch/clientCache.ts
473
543
  async function GET_CACHED(k, f) {
474
544
  if (CLIENT_CACHE[k]) {
@@ -492,10 +562,12 @@ var init_clientCache = __esm({
492
562
  import { NameSpacer } from "@vue-skuilder/common";
493
563
  import { blankCourseElo, toCourseElo } from "@vue-skuilder/common";
494
564
  import { prepareNote55 } from "@vue-skuilder/common";
565
+ import { v4 as uuidv4 } from "uuid";
495
566
  async function addNote55(courseID, codeCourse, shape, data, author, tags, uploads, elo = blankCourseElo()) {
496
567
  const db = getCourseDB(courseID);
497
568
  const payload = prepareNote55(courseID, codeCourse, shape, data, author, tags, uploads);
498
- const result = await db.post(payload);
569
+ const _id = `${DocTypePrefixes["DISPLAYABLE_DATA" /* DISPLAYABLE_DATA */]}-${uuidv4()}`;
570
+ const result = await db.put({ ...payload, _id });
499
571
  const dataShapeId = NameSpacer.getDataShapeString({
500
572
  course: codeCourse,
501
573
  dataShape: shape.name
@@ -566,7 +638,9 @@ async function createCard(questionViewName, courseID, dsDescriptor, noteID, tags
566
638
  }
567
639
  async function addCard(courseID, course, id_displayable_data, id_view, elo, tags, author) {
568
640
  const db = getCourseDB(courseID);
569
- const card = await db.post({
641
+ const _id = `${DocTypePrefixes["CARD" /* CARD */]}-${uuidv4()}`;
642
+ const card = await db.put({
643
+ _id,
570
644
  course,
571
645
  id_displayable_data,
572
646
  id_view,
@@ -1475,7 +1549,7 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
1475
1549
  });
1476
1550
 
1477
1551
  // src/impl/couch/classroomDB.ts
1478
- import moment2 from "moment";
1552
+ import moment3 from "moment";
1479
1553
  var classroomLookupDBTitle, CLASSROOM_CONFIG, ClassroomDBBase, StudentClassroomDB, TeacherClassroomDB, ClassroomLookupDB;
1480
1554
  var init_classroomDB2 = __esm({
1481
1555
  "src/impl/couch/classroomDB.ts"() {
@@ -1576,9 +1650,9 @@ var init_classroomDB2 = __esm({
1576
1650
  }
1577
1651
  async getNewCards() {
1578
1652
  const activeCards = await this._user.getActiveCards();
1579
- const now = moment2.utc();
1653
+ const now = moment3.utc();
1580
1654
  const assigned = await this.getAssignedContent();
1581
- const due = assigned.filter((c) => now.isAfter(moment2.utc(c.activeOn, REVIEW_TIME_FORMAT2)));
1655
+ const due = assigned.filter((c) => now.isAfter(moment3.utc(c.activeOn, REVIEW_TIME_FORMAT2)));
1582
1656
  logger.info(`Due content: ${JSON.stringify(due)}`);
1583
1657
  let ret = [];
1584
1658
  for (let i = 0; i < due.length; i++) {
@@ -1669,8 +1743,8 @@ var init_classroomDB2 = __esm({
1669
1743
  type: "tag",
1670
1744
  _id: id,
1671
1745
  assignedBy: content.assignedBy,
1672
- assignedOn: moment2.utc(),
1673
- activeOn: content.activeOn || moment2.utc()
1746
+ assignedOn: moment3.utc(),
1747
+ activeOn: content.activeOn || moment3.utc()
1674
1748
  });
1675
1749
  } else {
1676
1750
  put = await this._db.put({
@@ -1678,8 +1752,8 @@ var init_classroomDB2 = __esm({
1678
1752
  type: "course",
1679
1753
  _id: id,
1680
1754
  assignedBy: content.assignedBy,
1681
- assignedOn: moment2.utc(),
1682
- activeOn: content.activeOn || moment2.utc()
1755
+ assignedOn: moment3.utc(),
1756
+ activeOn: content.activeOn || moment3.utc()
1683
1757
  });
1684
1758
  }
1685
1759
  if (put.ok) {
@@ -1842,6 +1916,13 @@ var init_CouchDBSyncStrategy = __esm({
1842
1916
  return this.getUserDB(username);
1843
1917
  }
1844
1918
  }
1919
+ getWriteDB(username) {
1920
+ if (username === GuestUsername || username.startsWith(GuestUsername)) {
1921
+ return getLocalUserDB(username);
1922
+ } else {
1923
+ return this.getUserDB(username);
1924
+ }
1925
+ }
1845
1926
  startSync(localDB, remoteDB) {
1846
1927
  if (localDB !== remoteDB) {
1847
1928
  this.syncHandle = pouchdb_setup_default.sync(localDB, remoteDB, {
@@ -1987,7 +2068,7 @@ var init_CouchDBSyncStrategy = __esm({
1987
2068
  });
1988
2069
 
1989
2070
  // src/impl/couch/index.ts
1990
- import moment3 from "moment";
2071
+ import moment4 from "moment";
1991
2072
  import process2 from "process";
1992
2073
  function getCourseDB2(courseID) {
1993
2074
  return new pouchdb_setup_default(
@@ -2021,7 +2102,7 @@ function getStartAndEndKeys2(key) {
2021
2102
  endkey: key + "\uFFF0"
2022
2103
  };
2023
2104
  }
2024
- var isBrowser, GUEST_LOCAL_DB, localUserDB, pouchDBincludeCredentialsConfig, REVIEW_PREFIX2, REVIEW_TIME_FORMAT2;
2105
+ var isBrowser, GUEST_LOCAL_DB, localUserDB, pouchDBincludeCredentialsConfig, REVIEW_TIME_FORMAT2;
2025
2106
  var init_couch = __esm({
2026
2107
  "src/impl/couch/index.ts"() {
2027
2108
  "use strict";
@@ -2047,78 +2128,10 @@ var init_couch = __esm({
2047
2128
  return pouchdb_setup_default.fetch(url, opts);
2048
2129
  }
2049
2130
  };
2050
- REVIEW_PREFIX2 = "card_review_";
2051
2131
  REVIEW_TIME_FORMAT2 = "YYYY-MM-DD--kk:mm:ss-SSS";
2052
2132
  }
2053
2133
  });
2054
2134
 
2055
- // src/impl/couch/user-course-relDB.ts
2056
- import moment4 from "moment";
2057
- var UsrCrsData;
2058
- var init_user_course_relDB = __esm({
2059
- "src/impl/couch/user-course-relDB.ts"() {
2060
- "use strict";
2061
- init_couch();
2062
- init_courseDB();
2063
- init_logger();
2064
- UsrCrsData = class {
2065
- user;
2066
- course;
2067
- _courseId;
2068
- constructor(user, courseId) {
2069
- this.user = user;
2070
- this.course = new CourseDB(courseId, async () => this.user);
2071
- this._courseId = courseId;
2072
- }
2073
- async getReviewsForcast(daysCount) {
2074
- const time = moment4.utc().add(daysCount, "days");
2075
- return this.getReviewstoDate(time);
2076
- }
2077
- async getPendingReviews() {
2078
- const now = moment4.utc();
2079
- return this.getReviewstoDate(now);
2080
- }
2081
- async getScheduledReviewCount() {
2082
- return (await this.getPendingReviews()).length;
2083
- }
2084
- async getCourseSettings() {
2085
- const regDoc = await this.user.getCourseRegistrationsDoc();
2086
- const crsDoc = regDoc.courses.find((c) => c.courseID === this._courseId);
2087
- if (crsDoc && crsDoc.settings) {
2088
- return crsDoc.settings;
2089
- } else {
2090
- logger.warn(`no settings found during lookup on course ${this._courseId}`);
2091
- return {};
2092
- }
2093
- }
2094
- updateCourseSettings(updates) {
2095
- void this.user.updateCourseSettings(this._courseId, updates);
2096
- }
2097
- async getReviewstoDate(targetDate) {
2098
- const keys = getStartAndEndKeys2(REVIEW_PREFIX2);
2099
- const reviews = await this.user.remote().allDocs({
2100
- startkey: keys.startkey,
2101
- endkey: keys.endkey,
2102
- include_docs: true
2103
- });
2104
- logger.debug(
2105
- `Fetching ${this.user.getUsername()}'s scheduled reviews for course ${this._courseId}.`
2106
- );
2107
- return reviews.rows.filter((r) => {
2108
- if (r.id.startsWith(REVIEW_PREFIX2)) {
2109
- const date = moment4.utc(r.id.substr(REVIEW_PREFIX2.length), REVIEW_TIME_FORMAT2);
2110
- if (targetDate.isAfter(date)) {
2111
- if (this._courseId === void 0 || r.doc.courseId === this._courseId) {
2112
- return true;
2113
- }
2114
- }
2115
- }
2116
- }).map((r) => r.doc);
2117
- }
2118
- };
2119
- }
2120
- });
2121
-
2122
2135
  // src/impl/common/BaseUserDB.ts
2123
2136
  import { Status as Status3 } from "@vue-skuilder/common";
2124
2137
  import moment5 from "moment";
@@ -2214,10 +2227,11 @@ async function dropUserFromClassroom(user, classID) {
2214
2227
  async function getUserClassrooms(user) {
2215
2228
  return getOrCreateClassroomRegistrationsDoc(user);
2216
2229
  }
2217
- var log4, cardHistoryPrefix2, BaseUser, userCoursesDoc, userClassroomsDoc;
2230
+ var log4, BaseUser, userCoursesDoc, userClassroomsDoc;
2218
2231
  var init_BaseUserDB = __esm({
2219
2232
  "src/impl/common/BaseUserDB.ts"() {
2220
2233
  "use strict";
2234
+ init_core();
2221
2235
  init_util();
2222
2236
  init_types_legacy();
2223
2237
  init_logger();
@@ -2228,7 +2242,6 @@ var init_BaseUserDB = __esm({
2228
2242
  log4 = (s) => {
2229
2243
  logger.info(s);
2230
2244
  };
2231
- cardHistoryPrefix2 = "cardH-";
2232
2245
  BaseUser = class _BaseUser {
2233
2246
  static _instance;
2234
2247
  static _initialized = false;
@@ -2249,11 +2262,13 @@ var init_BaseUserDB = __esm({
2249
2262
  isLoggedIn() {
2250
2263
  return !this._username.startsWith(GuestUsername);
2251
2264
  }
2252
- remoteDB;
2253
2265
  remote() {
2254
2266
  return this.remoteDB;
2255
2267
  }
2256
2268
  localDB;
2269
+ remoteDB;
2270
+ writeDB;
2271
+ // Database to use for write operations (local-first approach)
2257
2272
  updateQueue;
2258
2273
  async createAccount(username, password) {
2259
2274
  if (!this.syncStrategy.canCreateAccount()) {
@@ -2317,8 +2332,8 @@ Currently logged-in as ${this._username}.`
2317
2332
  const allDocs = await localDB.allDocs({ include_docs: false });
2318
2333
  const docsToDelete = allDocs.rows.filter((row) => {
2319
2334
  const id = row.id;
2320
- return id.startsWith(cardHistoryPrefix2) || // Card interaction history
2321
- id.startsWith(REVIEW_PREFIX) || // Scheduled reviews
2335
+ return id.startsWith(DocTypePrefixes["CARDRECORD" /* CARDRECORD */]) || // Card interaction history
2336
+ id.startsWith(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */]) || // Scheduled reviews
2322
2337
  id === _BaseUser.DOC_IDS.COURSE_REGISTRATIONS || // Course registrations
2323
2338
  id === _BaseUser.DOC_IDS.CLASSROOM_REGISTRATIONS || // Classroom registrations
2324
2339
  id === _BaseUser.DOC_IDS.CONFIG;
@@ -2387,7 +2402,7 @@ Currently logged-in as ${this._username}.`
2387
2402
  *
2388
2403
  */
2389
2404
  async getActiveCards() {
2390
- const keys = getStartAndEndKeys(REVIEW_PREFIX);
2405
+ const keys = getStartAndEndKeys(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */]);
2391
2406
  const reviews = await this.remoteDB.allDocs({
2392
2407
  startkey: keys.startkey,
2393
2408
  endkey: keys.endkey,
@@ -2459,7 +2474,7 @@ Currently logged-in as ${this._username}.`
2459
2474
  }
2460
2475
  }
2461
2476
  async getReviewstoDate(targetDate, course_id) {
2462
- const keys = getStartAndEndKeys(REVIEW_PREFIX);
2477
+ const keys = getStartAndEndKeys(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */]);
2463
2478
  const reviews = await this.remoteDB.allDocs({
2464
2479
  startkey: keys.startkey,
2465
2480
  endkey: keys.endkey,
@@ -2469,8 +2484,11 @@ Currently logged-in as ${this._username}.`
2469
2484
  `Fetching ${this._username}'s scheduled reviews${course_id ? ` for course ${course_id}` : ""}.`
2470
2485
  );
2471
2486
  return reviews.rows.filter((r) => {
2472
- if (r.id.startsWith(REVIEW_PREFIX)) {
2473
- const date = moment5.utc(r.id.substr(REVIEW_PREFIX.length), REVIEW_TIME_FORMAT);
2487
+ if (r.id.startsWith(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */])) {
2488
+ const date = moment5.utc(
2489
+ r.id.substr(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */].length),
2490
+ REVIEW_TIME_FORMAT
2491
+ );
2474
2492
  if (targetDate.isAfter(date)) {
2475
2493
  if (course_id === void 0 || r.doc.courseId === course_id) {
2476
2494
  return true;
@@ -2585,7 +2603,8 @@ Currently logged-in as ${this._username}.`
2585
2603
  const defaultConfig = {
2586
2604
  _id: _BaseUser.DOC_IDS.CONFIG,
2587
2605
  darkMode: false,
2588
- likesConfetti: false
2606
+ likesConfetti: false,
2607
+ sessionTimeLimit: 5
2589
2608
  };
2590
2609
  try {
2591
2610
  const cfg = await this.localDB.get(_BaseUser.DOC_IDS.CONFIG);
@@ -2658,7 +2677,8 @@ Currently logged-in as ${this._username}.`
2658
2677
  setDBandQ() {
2659
2678
  this.localDB = getLocalUserDB(this._username);
2660
2679
  this.remoteDB = this.syncStrategy.setupRemoteDB(this._username);
2661
- this.updateQueue = new UpdateQueue(this.localDB);
2680
+ this.writeDB = this.syncStrategy.getWriteDB ? this.syncStrategy.getWriteDB(this._username) : this.localDB;
2681
+ this.updateQueue = new UpdateQueue(this.localDB, this.writeDB);
2662
2682
  }
2663
2683
  async init() {
2664
2684
  _BaseUser._initialized = false;
@@ -2776,8 +2796,8 @@ Currently logged-in as ${this._username}.`
2776
2796
  streak: 0,
2777
2797
  bestInterval: 0
2778
2798
  };
2779
- void this.remoteDB.put(initCardHistory);
2780
- return initCardHistory;
2799
+ const putResult = await this.writeDB.put(initCardHistory);
2800
+ return { ...initCardHistory, _rev: putResult.rev };
2781
2801
  } else {
2782
2802
  throw new Error(`putCardRecord failed because of:
2783
2803
  name:${reason.name}
@@ -2812,7 +2832,7 @@ Currently logged-in as ${this._username}.`
2812
2832
  const deletePromises = duplicateDocIds.map(async (docId) => {
2813
2833
  try {
2814
2834
  const doc = await this.remoteDB.get(docId);
2815
- await this.remoteDB.remove(doc);
2835
+ await this.writeDB.remove(doc);
2816
2836
  log4(`Successfully removed duplicate review: ${docId}`);
2817
2837
  } catch (error) {
2818
2838
  log4(`Failed to remove duplicate review ${docId}: ${error}`);
@@ -2834,7 +2854,7 @@ Currently logged-in as ${this._username}.`
2834
2854
  * @param course_id optional specification of individual course
2835
2855
  */
2836
2856
  async getSeenCards(course_id) {
2837
- let prefix = cardHistoryPrefix2;
2857
+ let prefix = DocTypePrefixes["CARDRECORD" /* CARDRECORD */];
2838
2858
  if (course_id) {
2839
2859
  prefix += course_id;
2840
2860
  }
@@ -2843,8 +2863,8 @@ Currently logged-in as ${this._username}.`
2843
2863
  });
2844
2864
  const ret = [];
2845
2865
  docs.rows.forEach((row) => {
2846
- if (row.id.startsWith(cardHistoryPrefix2)) {
2847
- ret.push(row.id.substr(cardHistoryPrefix2.length));
2866
+ if (row.id.startsWith(DocTypePrefixes["CARDRECORD" /* CARDRECORD */])) {
2867
+ ret.push(row.id.substr(DocTypePrefixes["CARDRECORD" /* CARDRECORD */].length));
2848
2868
  }
2849
2869
  });
2850
2870
  return ret;
@@ -2856,7 +2876,7 @@ Currently logged-in as ${this._username}.`
2856
2876
  async getHistory() {
2857
2877
  const cards = await filterAllDocsByPrefix(
2858
2878
  this.remoteDB,
2859
- cardHistoryPrefix2,
2879
+ DocTypePrefixes["CARDRECORD" /* CARDRECORD */],
2860
2880
  {
2861
2881
  include_docs: true,
2862
2882
  attachments: false
@@ -2897,7 +2917,7 @@ Currently logged-in as ${this._username}.`
2897
2917
  } catch (e) {
2898
2918
  const err = e;
2899
2919
  if (err.status === 404) {
2900
- await this.remoteDB.put({
2920
+ await this.writeDB.put({
2901
2921
  _id: _BaseUser.DOC_IDS.CLASSROOM_REGISTRATIONS,
2902
2922
  registrations: []
2903
2923
  });
@@ -2945,10 +2965,10 @@ Currently logged-in as ${this._username}.`
2945
2965
  }
2946
2966
  }
2947
2967
  async scheduleCardReview(review) {
2948
- return scheduleCardReviewLocal(this.remoteDB, review);
2968
+ return scheduleCardReviewLocal(this.writeDB, review);
2949
2969
  }
2950
2970
  async removeScheduledCardReview(reviewId) {
2951
- return removeScheduledCardReviewLocal(this.remoteDB, reviewId);
2971
+ return removeScheduledCardReviewLocal(this.writeDB, reviewId);
2952
2972
  }
2953
2973
  async registerForClassroom(_classId, _registerAs) {
2954
2974
  return registerUserForClassroom(this._username, _classId, _registerAs);
@@ -3160,18 +3180,21 @@ var init_StaticDataUnpacker = __esm({
3160
3180
  async getTagsIndex() {
3161
3181
  return await this.loadIndex("tags");
3162
3182
  }
3183
+ getDocTypeFromId(id) {
3184
+ for (const docTypeKey in DocTypePrefixes) {
3185
+ const prefix = DocTypePrefixes[docTypeKey];
3186
+ if (id.startsWith(`${prefix}-`)) {
3187
+ return docTypeKey;
3188
+ }
3189
+ }
3190
+ return void 0;
3191
+ }
3163
3192
  /**
3164
3193
  * Find which chunk contains a specific document ID
3165
3194
  */
3166
3195
  async findChunkForDocument(docId) {
3167
- let expectedDocType = void 0;
3168
- for (const docType of Object.values(DocType)) {
3169
- if (docId.startsWith(`${docType}-`)) {
3170
- expectedDocType = docType;
3171
- break;
3172
- }
3173
- }
3174
- if (expectedDocType !== void 0) {
3196
+ const expectedDocType = this.getDocTypeFromId(docId);
3197
+ if (expectedDocType) {
3175
3198
  const typeChunks = this.manifest.chunks.filter((c) => c.docType === expectedDocType);
3176
3199
  for (const chunk of typeChunks) {
3177
3200
  if (docId >= chunk.startKey && docId <= chunk.endKey) {
@@ -3181,21 +3204,8 @@ var init_StaticDataUnpacker = __esm({
3181
3204
  }
3182
3205
  }
3183
3206
  }
3184
- return void 0;
3185
3207
  } else {
3186
- const displayableChunks = this.manifest.chunks.filter(
3187
- (c) => c.docType === "DISPLAYABLE_DATA"
3188
- );
3189
- for (const chunk of displayableChunks) {
3190
- if (docId >= chunk.startKey && docId <= chunk.endKey) {
3191
- const exists = await this.verifyDocumentInChunk(docId, chunk);
3192
- if (exists) {
3193
- return chunk;
3194
- }
3195
- }
3196
- }
3197
- const cardChunks = this.manifest.chunks.filter((c) => c.docType === "CARD");
3198
- for (const chunk of cardChunks) {
3208
+ for (const chunk of this.manifest.chunks) {
3199
3209
  if (docId >= chunk.startKey && docId <= chunk.endKey) {
3200
3210
  const exists = await this.verifyDocumentInChunk(docId, chunk);
3201
3211
  if (exists) {
@@ -3216,6 +3226,7 @@ var init_StaticDataUnpacker = __esm({
3216
3226
  }
3217
3227
  return void 0;
3218
3228
  }
3229
+ return void 0;
3219
3230
  }
3220
3231
  /**
3221
3232
  * Verify that a document actually exists in a specific chunk by loading and checking it
@@ -3459,6 +3470,7 @@ var init_courseDB2 = __esm({
3459
3470
  "use strict";
3460
3471
  init_types_legacy();
3461
3472
  init_navigators();
3473
+ init_logger();
3462
3474
  StaticCourseDB = class {
3463
3475
  constructor(courseId, unpacker, userDB, manifest) {
3464
3476
  this.courseId = courseId;
@@ -3480,10 +3492,11 @@ var init_courseDB2 = __esm({
3480
3492
  throw new Error("Cannot update course config in static mode");
3481
3493
  }
3482
3494
  async getCourseInfo() {
3495
+ const cardCount = this.manifest.chunks.filter((chunk) => chunk.docType === "CARD" /* CARD */).reduce((total, chunk) => total + chunk.documentCount, 0);
3483
3496
  return {
3484
- cardCount: 0,
3485
- // Would come from manifest
3497
+ cardCount,
3486
3498
  registeredUsers: 0
3499
+ // Always 0 in static mode
3487
3500
  };
3488
3501
  }
3489
3502
  async getCourseDoc(id, _options) {
@@ -3572,12 +3585,56 @@ var init_courseDB2 = __esm({
3572
3585
  courseID: this.courseId
3573
3586
  }));
3574
3587
  }
3575
- async getAppliedTags(_cardId) {
3576
- return {
3577
- total_rows: 0,
3578
- offset: 0,
3579
- rows: []
3580
- };
3588
+ async getAppliedTags(cardId) {
3589
+ try {
3590
+ const tagsIndex = await this.unpacker.getTagsIndex();
3591
+ const cardTags = tagsIndex.byCard[cardId] || [];
3592
+ const rows = await Promise.all(
3593
+ cardTags.map(async (tagName) => {
3594
+ const tagId = `${"TAG" /* TAG */}-${tagName}`;
3595
+ try {
3596
+ const tagDoc = await this.unpacker.getDocument(tagId);
3597
+ return {
3598
+ id: tagId,
3599
+ key: cardId,
3600
+ value: {
3601
+ name: tagDoc.name,
3602
+ snippet: tagDoc.snippet,
3603
+ count: tagDoc.taggedCards?.length || 0
3604
+ }
3605
+ };
3606
+ } catch (error) {
3607
+ if (error && error.status === 404) {
3608
+ logger.warn(`Tag document not found for ${tagName}, creating stub`);
3609
+ } else {
3610
+ logger.error(`Error getting tag document for ${tagName}:`, error);
3611
+ throw error;
3612
+ }
3613
+ return {
3614
+ id: tagId,
3615
+ key: cardId,
3616
+ value: {
3617
+ name: tagName,
3618
+ snippet: `Tag: ${tagName}`,
3619
+ count: tagsIndex.byTag[tagName]?.length || 0
3620
+ }
3621
+ };
3622
+ }
3623
+ })
3624
+ );
3625
+ return {
3626
+ total_rows: rows.length,
3627
+ offset: 0,
3628
+ rows
3629
+ };
3630
+ } catch (error) {
3631
+ logger.error(`Error getting applied tags for card ${cardId}:`, error);
3632
+ return {
3633
+ total_rows: 0,
3634
+ offset: 0,
3635
+ rows: []
3636
+ };
3637
+ }
3581
3638
  }
3582
3639
  async addTagToCard(_cardId, _tagId) {
3583
3640
  throw new Error("Cannot modify tags in static mode");
@@ -3595,11 +3652,69 @@ var init_courseDB2 = __esm({
3595
3652
  throw new Error("Cannot update tags in static mode");
3596
3653
  }
3597
3654
  async getCourseTagStubs() {
3598
- return {
3599
- total_rows: 0,
3600
- offset: 0,
3601
- rows: []
3602
- };
3655
+ try {
3656
+ const tagsIndex = await this.unpacker.getTagsIndex();
3657
+ if (!tagsIndex || !tagsIndex.byTag) {
3658
+ logger.warn("Tags index not found or empty");
3659
+ return {
3660
+ total_rows: 0,
3661
+ offset: 0,
3662
+ rows: []
3663
+ };
3664
+ }
3665
+ const tagNames = Object.keys(tagsIndex.byTag);
3666
+ const rows = await Promise.all(
3667
+ tagNames.map(async (tagName) => {
3668
+ const cardIds = tagsIndex.byTag[tagName] || [];
3669
+ const tagId = `${"TAG" /* TAG */}-${tagName}`;
3670
+ try {
3671
+ const tagDoc = await this.unpacker.getDocument(tagId);
3672
+ return {
3673
+ id: tagId,
3674
+ key: tagId,
3675
+ value: { rev: "1-static" },
3676
+ doc: tagDoc
3677
+ };
3678
+ } catch (error) {
3679
+ if (error && error.status === 404) {
3680
+ logger.warn(`Tag document not found for ${tagName}, creating stub`);
3681
+ const stubDoc = {
3682
+ _id: tagId,
3683
+ _rev: "1-static",
3684
+ course: this.courseId,
3685
+ docType: "TAG" /* TAG */,
3686
+ name: tagName,
3687
+ snippet: `Tag: ${tagName}`,
3688
+ wiki: "",
3689
+ taggedCards: cardIds,
3690
+ author: "system"
3691
+ };
3692
+ return {
3693
+ id: tagId,
3694
+ key: tagId,
3695
+ value: { rev: "1-static" },
3696
+ doc: stubDoc
3697
+ };
3698
+ } else {
3699
+ logger.error(`Error getting tag document for ${tagName}:`, error);
3700
+ throw error;
3701
+ }
3702
+ }
3703
+ })
3704
+ );
3705
+ return {
3706
+ total_rows: rows.length,
3707
+ offset: 0,
3708
+ rows
3709
+ };
3710
+ } catch (error) {
3711
+ logger.error("Failed to get course tag stubs:", error);
3712
+ return {
3713
+ total_rows: 0,
3714
+ offset: 0,
3715
+ rows: []
3716
+ };
3717
+ }
3603
3718
  }
3604
3719
  async addNote(_codeCourse, _shape, _data, _author, _tags, _uploads, _elo) {
3605
3720
  return {
@@ -3705,6 +3820,9 @@ var init_NoOpSyncStrategy = __esm({
3705
3820
  setupRemoteDB(username) {
3706
3821
  return getLocalUserDB(username);
3707
3822
  }
3823
+ getWriteDB(username) {
3824
+ return getLocalUserDB(username);
3825
+ }
3708
3826
  startSync(_localDB, _remoteDB) {
3709
3827
  }
3710
3828
  stopSync() {
@@ -4156,6 +4274,57 @@ var CouchDBToStaticPacker = class {
4156
4274
  attachments
4157
4275
  };
4158
4276
  }
4277
+ /**
4278
+ * Pack a CouchDB course database and write the static files to disk
4279
+ */
4280
+ async packCourseToFiles(sourceDB, courseId, outputDir, fsAdapter) {
4281
+ logger.info(`Packing course ${courseId} to files in ${outputDir}`);
4282
+ const packedData = await this.packCourse(sourceDB, courseId);
4283
+ const filesWritten = await this.writePackedDataToFiles(packedData, outputDir, fsAdapter);
4284
+ return {
4285
+ manifest: packedData.manifest,
4286
+ filesWritten,
4287
+ attachmentsFound: packedData.attachments ? packedData.attachments.size : 0
4288
+ };
4289
+ }
4290
+ /**
4291
+ * Write packed course data to files using FileSystemAdapter
4292
+ */
4293
+ async writePackedDataToFiles(packedData, outputDir, fsAdapter) {
4294
+ let totalFiles = 0;
4295
+ await fsAdapter.ensureDir(outputDir);
4296
+ const manifestPath = fsAdapter.joinPath(outputDir, "manifest.json");
4297
+ await fsAdapter.writeJson(manifestPath, packedData.manifest, { spaces: 2 });
4298
+ totalFiles++;
4299
+ logger.info(`Wrote manifest: ${manifestPath}`);
4300
+ const chunksDir = fsAdapter.joinPath(outputDir, "chunks");
4301
+ const indicesDir = fsAdapter.joinPath(outputDir, "indices");
4302
+ await fsAdapter.ensureDir(chunksDir);
4303
+ await fsAdapter.ensureDir(indicesDir);
4304
+ for (const [chunkId, chunkData] of packedData.chunks) {
4305
+ const chunkPath = fsAdapter.joinPath(chunksDir, `${chunkId}.json`);
4306
+ await fsAdapter.writeJson(chunkPath, chunkData);
4307
+ totalFiles++;
4308
+ }
4309
+ logger.info(`Wrote ${packedData.chunks.size} chunk files`);
4310
+ for (const [indexName, indexData] of packedData.indices) {
4311
+ const indexPath = fsAdapter.joinPath(indicesDir, `${indexName}.json`);
4312
+ await fsAdapter.writeJson(indexPath, indexData, { spaces: 2 });
4313
+ totalFiles++;
4314
+ }
4315
+ logger.info(`Wrote ${packedData.indices.size} index files`);
4316
+ if (packedData.attachments && packedData.attachments.size > 0) {
4317
+ for (const [attachmentPath, attachmentData] of packedData.attachments) {
4318
+ const fullAttachmentPath = fsAdapter.joinPath(outputDir, attachmentPath);
4319
+ const attachmentDir = fsAdapter.dirname(fullAttachmentPath);
4320
+ await fsAdapter.ensureDir(attachmentDir);
4321
+ await fsAdapter.writeFile(fullAttachmentPath, attachmentData.buffer);
4322
+ totalFiles++;
4323
+ }
4324
+ logger.info(`Wrote ${packedData.attachments.size} attachment files`);
4325
+ }
4326
+ return totalFiles;
4327
+ }
4159
4328
  async extractCourseConfig(db) {
4160
4329
  try {
4161
4330
  return await db.get("CourseConfig");
@@ -4324,7 +4493,8 @@ var CouchDBToStaticPacker = class {
4324
4493
  }
4325
4494
  try {
4326
4495
  const designDocId = designDoc._id;
4327
- const viewPath = `${designDocId}/${viewName}`;
4496
+ const designDocName = designDocId.replace("_design/", "");
4497
+ const viewPath = `${designDocName}/${viewName}`;
4328
4498
  logger.info(`Querying CouchDB view: ${viewPath}`);
4329
4499
  const viewResults = await this.sourceDB.query(viewPath, {
4330
4500
  include_docs: false
@@ -5249,6 +5419,7 @@ var StaticToCouchDBMigrator = class {
5249
5419
  const docsToInsert = batch.map((doc) => {
5250
5420
  const cleanDoc = { ...doc };
5251
5421
  delete cleanDoc._rev;
5422
+ delete cleanDoc._attachments;
5252
5423
  return cleanDoc;
5253
5424
  });
5254
5425
  const bulkResult = await db.bulkDocs(docsToInsert);
@@ -5362,9 +5533,11 @@ var StaticToCouchDBMigrator = class {
5362
5533
  attachmentData = await response.arrayBuffer();
5363
5534
  }
5364
5535
  }
5536
+ const doc = await db.get(docId);
5365
5537
  await db.putAttachment(
5366
5538
  docId,
5367
5539
  attachmentName,
5540
+ doc._rev,
5368
5541
  attachmentData,
5369
5542
  // PouchDB accepts both ArrayBuffer and Buffer
5370
5543
  attachmentMeta.content_type
@@ -5832,6 +6005,7 @@ export {
5832
6005
  CouchDBToStaticPacker,
5833
6006
  CourseLookup,
5834
6007
  DocType,
6008
+ DocTypePrefixes,
5835
6009
  ENV,
5836
6010
  FileSystemError,
5837
6011
  GuestUsername,
@@ -5841,7 +6015,6 @@ export {
5841
6015
  StaticToCouchDBMigrator,
5842
6016
  _resetDataLayer,
5843
6017
  areQuestionRecords,
5844
- cardHistoryPrefix,
5845
6018
  docIsDeleted,
5846
6019
  ensureAppDataDirectory,
5847
6020
  getAppDataDirectory,