@vue-skuilder/db 0.1.4 → 0.1.6

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 (83) hide show
  1. package/CLAUDE.md +43 -0
  2. package/dist/SyncStrategy-DnJRj-Xp.d.mts +74 -0
  3. package/dist/SyncStrategy-DnJRj-Xp.d.ts +74 -0
  4. package/dist/core/index.d.mts +90 -2
  5. package/dist/core/index.d.ts +90 -2
  6. package/dist/core/index.js +856 -6155
  7. package/dist/core/index.js.map +1 -1
  8. package/dist/core/index.mjs +778 -6097
  9. package/dist/core/index.mjs.map +1 -1
  10. package/dist/dataLayerProvider-BZmLyBVw.d.mts +41 -0
  11. package/dist/dataLayerProvider-BuntXkCs.d.ts +41 -0
  12. package/dist/impl/couch/index.d.mts +292 -0
  13. package/dist/impl/couch/index.d.ts +292 -0
  14. package/dist/impl/couch/index.js +3075 -0
  15. package/dist/impl/couch/index.js.map +1 -0
  16. package/dist/impl/couch/index.mjs +3007 -0
  17. package/dist/impl/couch/index.mjs.map +1 -0
  18. package/dist/impl/static/index.d.mts +188 -0
  19. package/dist/impl/static/index.d.ts +188 -0
  20. package/dist/impl/static/index.js +3055 -0
  21. package/dist/impl/static/index.js.map +1 -0
  22. package/dist/impl/static/index.mjs +3025 -0
  23. package/dist/impl/static/index.mjs.map +1 -0
  24. package/dist/index.d.mts +13 -4
  25. package/dist/index.d.ts +13 -4
  26. package/dist/index.js +2920 -6846
  27. package/dist/index.js.map +1 -1
  28. package/dist/index.mjs +3567 -7513
  29. package/dist/index.mjs.map +1 -1
  30. package/dist/types-D6SnlHPm.d.ts +58 -0
  31. package/dist/types-DPRvCrIk.d.mts +58 -0
  32. package/dist/types-legacy-WPe8CtO-.d.mts +139 -0
  33. package/dist/types-legacy-WPe8CtO-.d.ts +139 -0
  34. package/dist/{index-QMtzQI65.d.mts → userDB-31gsvxyd.d.mts} +11 -252
  35. package/dist/{index-QMtzQI65.d.ts → userDB-D9EuWTp1.d.ts} +11 -252
  36. package/dist/util/packer/index.d.mts +65 -0
  37. package/dist/util/packer/index.d.ts +65 -0
  38. package/dist/util/packer/index.js +512 -0
  39. package/dist/util/packer/index.js.map +1 -0
  40. package/dist/util/packer/index.mjs +485 -0
  41. package/dist/util/packer/index.mjs.map +1 -0
  42. package/package.json +12 -2
  43. package/src/core/interfaces/contentSource.ts +8 -6
  44. package/src/core/interfaces/courseDB.ts +1 -1
  45. package/src/core/interfaces/dataLayerProvider.ts +5 -0
  46. package/src/core/interfaces/userDB.ts +7 -2
  47. package/src/core/types/types-legacy.ts +2 -0
  48. package/src/factory.ts +10 -7
  49. package/src/impl/{pouch/userDB.ts → common/BaseUserDB.ts} +283 -260
  50. package/src/impl/common/SyncStrategy.ts +90 -0
  51. package/src/impl/common/index.ts +23 -0
  52. package/src/impl/common/types.ts +50 -0
  53. package/src/impl/common/userDBHelpers.ts +144 -0
  54. package/src/impl/couch/CouchDBSyncStrategy.ts +209 -0
  55. package/src/impl/{pouch → couch}/PouchDataLayerProvider.ts +16 -7
  56. package/src/impl/{pouch → couch}/adminDB.ts +3 -3
  57. package/src/impl/{pouch → couch}/auth.ts +2 -2
  58. package/src/impl/{pouch → couch}/classroomDB.ts +6 -6
  59. package/src/impl/{pouch → couch}/courseAPI.ts +59 -21
  60. package/src/impl/{pouch → couch}/courseDB.ts +32 -17
  61. package/src/impl/{pouch → couch}/courseLookupDB.ts +1 -1
  62. package/src/impl/{pouch → couch}/index.ts +27 -20
  63. package/src/impl/{pouch → couch}/updateQueue.ts +5 -1
  64. package/src/impl/{pouch → couch}/user-course-relDB.ts +6 -1
  65. package/src/impl/static/NoOpSyncStrategy.ts +70 -0
  66. package/src/impl/static/StaticDataLayerProvider.ts +93 -0
  67. package/src/impl/static/StaticDataUnpacker.ts +549 -0
  68. package/src/impl/static/courseDB.ts +275 -0
  69. package/src/impl/static/coursesDB.ts +37 -0
  70. package/src/impl/static/index.ts +7 -0
  71. package/src/index.ts +1 -1
  72. package/src/study/SessionController.ts +4 -4
  73. package/src/study/SpacedRepetition.ts +3 -3
  74. package/src/study/getCardDataShape.ts +2 -2
  75. package/src/util/index.ts +1 -0
  76. package/src/util/packer/CouchDBToStaticPacker.ts +620 -0
  77. package/src/util/packer/index.ts +4 -0
  78. package/src/util/packer/types.ts +64 -0
  79. package/tsconfig.json +7 -10
  80. package/tsup.config.ts +5 -3
  81. /package/src/impl/{pouch → couch}/clientCache.ts +0 -0
  82. /package/src/impl/{pouch → couch}/pouchdb-setup.ts +0 -0
  83. /package/src/impl/{pouch → couch}/types.ts +0 -0
@@ -0,0 +1,3025 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __glob = (map) => (path) => {
4
+ var fn = map[path];
5
+ if (fn) return fn();
6
+ throw new Error("Module not found in bundle: " + path);
7
+ };
8
+ var __esm = (fn, res) => function __init() {
9
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
+ };
11
+ var __export = (target, all) => {
12
+ for (var name in all)
13
+ __defProp(target, name, { get: all[name], enumerable: true });
14
+ };
15
+
16
+ // src/util/logger.ts
17
+ var isDevelopment, logger;
18
+ var init_logger = __esm({
19
+ "src/util/logger.ts"() {
20
+ "use strict";
21
+ isDevelopment = typeof process !== "undefined" && process.env.NODE_ENV === "development";
22
+ logger = {
23
+ /**
24
+ * Debug-level logging - only shown in development
25
+ */
26
+ debug: (message, ...args) => {
27
+ if (isDevelopment) {
28
+ console.debug(`[DB:DEBUG] ${message}`, ...args);
29
+ }
30
+ },
31
+ /**
32
+ * Info-level logging - general information
33
+ */
34
+ info: (message, ...args) => {
35
+ console.info(`[DB:INFO] ${message}`, ...args);
36
+ },
37
+ /**
38
+ * Warning-level logging - potential issues
39
+ */
40
+ warn: (message, ...args) => {
41
+ console.warn(`[DB:WARN] ${message}`, ...args);
42
+ },
43
+ /**
44
+ * Error-level logging - serious problems
45
+ */
46
+ error: (message, ...args) => {
47
+ console.error(`[DB:ERROR] ${message}`, ...args);
48
+ },
49
+ /**
50
+ * Log function for backward compatibility with existing log() usage
51
+ */
52
+ log: (message, ...args) => {
53
+ if (isDevelopment) {
54
+ console.log(`[DB:LOG] ${message}`, ...args);
55
+ }
56
+ }
57
+ };
58
+ }
59
+ });
60
+
61
+ // src/core/interfaces/adminDB.ts
62
+ var init_adminDB = __esm({
63
+ "src/core/interfaces/adminDB.ts"() {
64
+ "use strict";
65
+ }
66
+ });
67
+
68
+ // src/core/interfaces/classroomDB.ts
69
+ var init_classroomDB = __esm({
70
+ "src/core/interfaces/classroomDB.ts"() {
71
+ "use strict";
72
+ }
73
+ });
74
+
75
+ // src/factory.ts
76
+ var ENV;
77
+ var init_factory = __esm({
78
+ "src/factory.ts"() {
79
+ "use strict";
80
+ init_logger();
81
+ ENV = {
82
+ COUCHDB_SERVER_PROTOCOL: "NOT_SET",
83
+ COUCHDB_SERVER_URL: "NOT_SET"
84
+ };
85
+ }
86
+ });
87
+
88
+ // src/impl/couch/pouchdb-setup.ts
89
+ import PouchDB from "pouchdb";
90
+ import PouchDBFind from "pouchdb-find";
91
+ import PouchDBAuth from "@nilock2/pouchdb-authentication";
92
+ var pouchdb_setup_default;
93
+ var init_pouchdb_setup = __esm({
94
+ "src/impl/couch/pouchdb-setup.ts"() {
95
+ "use strict";
96
+ PouchDB.plugin(PouchDBFind);
97
+ PouchDB.plugin(PouchDBAuth);
98
+ PouchDB.defaults({
99
+ ajax: {
100
+ timeout: 6e4
101
+ }
102
+ });
103
+ pouchdb_setup_default = PouchDB;
104
+ }
105
+ });
106
+
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"() {
111
+ "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";
128
+ }
129
+ });
130
+
131
+ // src/impl/couch/courseLookupDB.ts
132
+ var init_courseLookupDB = __esm({
133
+ "src/impl/couch/courseLookupDB.ts"() {
134
+ "use strict";
135
+ init_pouchdb_setup();
136
+ init_factory();
137
+ init_logger();
138
+ logger.debug(`COURSELOOKUP FILE RUNNING`);
139
+ }
140
+ });
141
+
142
+ // src/impl/couch/adminDB.ts
143
+ var init_adminDB2 = __esm({
144
+ "src/impl/couch/adminDB.ts"() {
145
+ "use strict";
146
+ init_pouchdb_setup();
147
+ init_factory();
148
+ init_couch();
149
+ init_classroomDB2();
150
+ init_courseLookupDB();
151
+ init_logger();
152
+ }
153
+ });
154
+
155
+ // src/util/Loggable.ts
156
+ var Loggable;
157
+ var init_Loggable = __esm({
158
+ "src/util/Loggable.ts"() {
159
+ "use strict";
160
+ Loggable = class {
161
+ log(...args) {
162
+ console.log(`LOG-${this._className}@${/* @__PURE__ */ new Date()}:`, ...args);
163
+ }
164
+ error(...args) {
165
+ console.error(`ERROR-${this._className}@${/* @__PURE__ */ new Date()}:`, ...args);
166
+ }
167
+ };
168
+ }
169
+ });
170
+
171
+ // src/impl/couch/updateQueue.ts
172
+ var UpdateQueue;
173
+ var init_updateQueue = __esm({
174
+ "src/impl/couch/updateQueue.ts"() {
175
+ "use strict";
176
+ init_Loggable();
177
+ init_logger();
178
+ UpdateQueue = class extends Loggable {
179
+ _className = "UpdateQueue";
180
+ pendingUpdates = {};
181
+ inprogressUpdates = {};
182
+ db;
183
+ update(id, update) {
184
+ logger.debug(`Update requested on doc: ${id}`);
185
+ if (this.pendingUpdates[id]) {
186
+ this.pendingUpdates[id].push(update);
187
+ } else {
188
+ this.pendingUpdates[id] = [update];
189
+ }
190
+ return this.applyUpdates(id);
191
+ }
192
+ constructor(db) {
193
+ super();
194
+ this.db = db;
195
+ logger.debug(`UpdateQ initialized...`);
196
+ void this.db.info().then((i) => {
197
+ logger.debug(`db info: ${JSON.stringify(i)}`);
198
+ });
199
+ }
200
+ async applyUpdates(id) {
201
+ logger.debug(`Applying updates on doc: ${id}`);
202
+ if (this.inprogressUpdates[id]) {
203
+ await this.db.info();
204
+ return this.applyUpdates(id);
205
+ } else {
206
+ if (this.pendingUpdates[id] && this.pendingUpdates[id].length > 0) {
207
+ this.inprogressUpdates[id] = true;
208
+ try {
209
+ let doc = await this.db.get(id);
210
+ logger.debug(`Retrieved doc: ${id}`);
211
+ while (this.pendingUpdates[id].length !== 0) {
212
+ const update = this.pendingUpdates[id].splice(0, 1)[0];
213
+ if (typeof update === "function") {
214
+ doc = { ...doc, ...update(doc) };
215
+ } else {
216
+ doc = {
217
+ ...doc,
218
+ ...update
219
+ };
220
+ }
221
+ }
222
+ await this.db.put(doc);
223
+ logger.debug(`Put doc: ${id}`);
224
+ if (this.pendingUpdates[id].length === 0) {
225
+ this.inprogressUpdates[id] = false;
226
+ delete this.inprogressUpdates[id];
227
+ } else {
228
+ return this.applyUpdates(id);
229
+ }
230
+ return doc;
231
+ } catch (e) {
232
+ delete this.inprogressUpdates[id];
233
+ if (this.pendingUpdates[id]) {
234
+ delete this.pendingUpdates[id];
235
+ }
236
+ logger.error(`Error on attemped update: ${JSON.stringify(e)}`);
237
+ throw e;
238
+ }
239
+ } else {
240
+ throw new Error(`Empty Updates Queue Triggered`);
241
+ }
242
+ }
243
+ }
244
+ };
245
+ }
246
+ });
247
+
248
+ // src/impl/couch/clientCache.ts
249
+ async function GET_CACHED(k, f) {
250
+ if (CLIENT_CACHE[k]) {
251
+ return CLIENT_CACHE[k];
252
+ }
253
+ CLIENT_CACHE[k] = f ? await f(k) : await GET_ITEM(k);
254
+ return GET_CACHED(k);
255
+ }
256
+ async function GET_ITEM(k) {
257
+ throw new Error(`No implementation found for GET_CACHED(${k})`);
258
+ }
259
+ var CLIENT_CACHE;
260
+ var init_clientCache = __esm({
261
+ "src/impl/couch/clientCache.ts"() {
262
+ "use strict";
263
+ CLIENT_CACHE = {};
264
+ }
265
+ });
266
+
267
+ // src/core/navigators/elo.ts
268
+ var elo_exports = {};
269
+ __export(elo_exports, {
270
+ default: () => ELONavigator
271
+ });
272
+ var ELONavigator;
273
+ var init_elo = __esm({
274
+ "src/core/navigators/elo.ts"() {
275
+ "use strict";
276
+ init_navigators();
277
+ ELONavigator = class extends ContentNavigator {
278
+ user;
279
+ course;
280
+ constructor(user, course) {
281
+ super();
282
+ this.user = user;
283
+ this.course = course;
284
+ }
285
+ async getPendingReviews() {
286
+ const reviews = await this.user.getPendingReviews(this.course.getCourseID());
287
+ const elo = await this.course.getCardEloData(reviews.map((r) => r.cardId));
288
+ const ratedReviews = reviews.map((r, i) => {
289
+ const ratedR = {
290
+ ...r,
291
+ ...elo[i]
292
+ };
293
+ return ratedR;
294
+ });
295
+ ratedReviews.sort((a, b) => {
296
+ return a.global.score - b.global.score;
297
+ });
298
+ return ratedReviews.map((r) => {
299
+ return {
300
+ ...r,
301
+ contentSourceType: "course",
302
+ contentSourceID: this.course.getCourseID(),
303
+ cardID: r.cardId,
304
+ courseID: r.courseId,
305
+ qualifiedID: `${r.courseId}-${r.cardId}`,
306
+ reviewID: r._id,
307
+ status: "review"
308
+ };
309
+ });
310
+ }
311
+ async getNewCards(limit = 99) {
312
+ const activeCards = await this.user.getActiveCards();
313
+ return (await this.course.getCardsCenteredAtELO({ limit, elo: "user" }, (c) => {
314
+ if (activeCards.some((ac) => c.includes(ac))) {
315
+ return false;
316
+ } else {
317
+ return true;
318
+ }
319
+ })).map((c) => {
320
+ return {
321
+ ...c,
322
+ status: "new"
323
+ };
324
+ });
325
+ }
326
+ };
327
+ }
328
+ });
329
+
330
+ // import("./**/*") in src/core/navigators/index.ts
331
+ var globImport;
332
+ var init_ = __esm({
333
+ 'import("./**/*") in src/core/navigators/index.ts'() {
334
+ globImport = __glob({
335
+ "./elo.ts": () => Promise.resolve().then(() => (init_elo(), elo_exports)),
336
+ "./index.ts": () => Promise.resolve().then(() => (init_navigators(), navigators_exports))
337
+ });
338
+ }
339
+ });
340
+
341
+ // src/core/navigators/index.ts
342
+ var navigators_exports = {};
343
+ __export(navigators_exports, {
344
+ ContentNavigator: () => ContentNavigator,
345
+ Navigators: () => Navigators
346
+ });
347
+ var Navigators, ContentNavigator;
348
+ var init_navigators = __esm({
349
+ "src/core/navigators/index.ts"() {
350
+ "use strict";
351
+ init_logger();
352
+ init_();
353
+ Navigators = /* @__PURE__ */ ((Navigators2) => {
354
+ Navigators2["ELO"] = "elo";
355
+ return Navigators2;
356
+ })(Navigators || {});
357
+ ContentNavigator = class {
358
+ /**
359
+ *
360
+ * @param user
361
+ * @param strategyData
362
+ * @returns the runtime object used to steer a study session.
363
+ */
364
+ static async create(user, course, strategyData) {
365
+ const implementingClass = strategyData.implementingClass;
366
+ let NavigatorImpl;
367
+ const variations = ["", ".js", ".ts"];
368
+ for (const ext of variations) {
369
+ try {
370
+ const module = await globImport(`./${implementingClass}${ext}`);
371
+ NavigatorImpl = module.default;
372
+ break;
373
+ } catch (e) {
374
+ logger.debug(`Failed to load with extension ${ext}:`, e);
375
+ }
376
+ }
377
+ if (!NavigatorImpl) {
378
+ throw new Error(`Could not load navigator implementation for: ${implementingClass}`);
379
+ }
380
+ return new NavigatorImpl(user, course, strategyData);
381
+ }
382
+ };
383
+ }
384
+ });
385
+
386
+ // src/impl/couch/courseDB.ts
387
+ import {
388
+ EloToNumber,
389
+ Status,
390
+ blankCourseElo,
391
+ toCourseElo
392
+ } 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
+ var init_courseDB = __esm({
470
+ "src/impl/couch/courseDB.ts"() {
471
+ "use strict";
472
+ init_couch();
473
+ init_updateQueue();
474
+ init_types_legacy();
475
+ init_logger();
476
+ init_clientCache();
477
+ init_courseAPI();
478
+ init_courseLookupDB();
479
+ 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}:
640
+
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
+ };
891
+ }
892
+ });
893
+
894
+ // src/impl/common/SyncStrategy.ts
895
+ var init_SyncStrategy = __esm({
896
+ "src/impl/common/SyncStrategy.ts"() {
897
+ "use strict";
898
+ }
899
+ });
900
+
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"() {
907
+ "use strict";
908
+ init_types_legacy();
909
+ }
910
+ });
911
+
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"() {
961
+ "use strict";
962
+ init_logger();
963
+ 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
+ };
969
+ }
970
+ });
971
+
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"() {
977
+ "use strict";
978
+ init_couch();
979
+ init_courseDB();
980
+ 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);
1034
+ }
1035
+ };
1036
+ }
1037
+ });
1038
+
1039
+ // src/impl/common/BaseUserDB.ts
1040
+ import { Status as Status2 } from "@vue-skuilder/common";
1041
+ import moment3 from "moment";
1042
+ async function getOrCreateClassroomRegistrationsDoc(user) {
1043
+ let ret;
1044
+ try {
1045
+ ret = await getLocalUserDB(user).get(userClassroomsDoc);
1046
+ } catch (e) {
1047
+ const err = e;
1048
+ if (err.status === 404) {
1049
+ await getLocalUserDB(user).put({
1050
+ _id: userClassroomsDoc,
1051
+ registrations: []
1052
+ });
1053
+ ret = await getOrCreateClassroomRegistrationsDoc(user);
1054
+ } else {
1055
+ const errorDetails = {
1056
+ name: err.name,
1057
+ status: err.status,
1058
+ message: err.message,
1059
+ reason: err.reason,
1060
+ error: err.error
1061
+ };
1062
+ logger.error(
1063
+ "Database error in getOrCreateClassroomRegistrationsDoc (standalone function):",
1064
+ errorDetails
1065
+ );
1066
+ throw new Error(
1067
+ `Database error accessing classroom registrations: ${err.message || err.name || "Unknown error"} (status: ${err.status})`
1068
+ );
1069
+ }
1070
+ }
1071
+ return ret;
1072
+ }
1073
+ async function getOrCreateCourseRegistrationsDoc(user) {
1074
+ let ret;
1075
+ try {
1076
+ ret = await getLocalUserDB(user).get(userCoursesDoc);
1077
+ } catch (e) {
1078
+ const err = e;
1079
+ if (err.status === 404) {
1080
+ await getLocalUserDB(user).put({
1081
+ _id: userCoursesDoc,
1082
+ courses: [],
1083
+ studyWeight: {}
1084
+ });
1085
+ ret = await getOrCreateCourseRegistrationsDoc(user);
1086
+ } else {
1087
+ throw new Error(
1088
+ `Unexpected error ${JSON.stringify(e)} in getOrCreateCourseRegistrationDoc...`
1089
+ );
1090
+ }
1091
+ }
1092
+ return ret;
1093
+ }
1094
+ async function updateUserElo(user, course_id, elo) {
1095
+ const regDoc = await getOrCreateCourseRegistrationsDoc(user);
1096
+ const course = regDoc.courses.find((c) => c.courseID === course_id);
1097
+ course.elo = elo;
1098
+ return getLocalUserDB(user).put(regDoc);
1099
+ }
1100
+ async function registerUserForClassroom(user, classID, registerAs) {
1101
+ log2(`Registering user: ${user} in course: ${classID}`);
1102
+ return getOrCreateClassroomRegistrationsDoc(user).then((doc) => {
1103
+ const regItem = {
1104
+ classID,
1105
+ registeredAs: registerAs
1106
+ };
1107
+ if (doc.registrations.filter((reg) => {
1108
+ return reg.classID === regItem.classID && reg.registeredAs === regItem.registeredAs;
1109
+ }).length === 0) {
1110
+ doc.registrations.push(regItem);
1111
+ } else {
1112
+ log2(`User ${user} is already registered for class ${classID}`);
1113
+ }
1114
+ return getLocalUserDB(user).put(doc);
1115
+ });
1116
+ }
1117
+ async function dropUserFromClassroom(user, classID) {
1118
+ return getOrCreateClassroomRegistrationsDoc(user).then((doc) => {
1119
+ let index = -1;
1120
+ for (let i = 0; i < doc.registrations.length; i++) {
1121
+ if (doc.registrations[i].classID === classID) {
1122
+ index = i;
1123
+ }
1124
+ }
1125
+ if (index !== -1) {
1126
+ doc.registrations.splice(index, 1);
1127
+ }
1128
+ return getLocalUserDB(user).put(doc);
1129
+ });
1130
+ }
1131
+ async function getUserClassrooms(user) {
1132
+ return getOrCreateClassroomRegistrationsDoc(user);
1133
+ }
1134
+ var log2, cardHistoryPrefix2, BaseUser, userCoursesDoc, userClassroomsDoc;
1135
+ var init_BaseUserDB = __esm({
1136
+ "src/impl/common/BaseUserDB.ts"() {
1137
+ "use strict";
1138
+ init_util();
1139
+ init_types_legacy();
1140
+ init_logger();
1141
+ init_userDBHelpers();
1142
+ init_updateQueue();
1143
+ init_user_course_relDB();
1144
+ init_couch();
1145
+ log2 = (s) => {
1146
+ logger.info(s);
1147
+ };
1148
+ cardHistoryPrefix2 = "cardH-";
1149
+ BaseUser = class _BaseUser {
1150
+ static _instance;
1151
+ static _initialized = false;
1152
+ static Dummy(syncStrategy) {
1153
+ return new _BaseUser("Me", syncStrategy);
1154
+ }
1155
+ static DOC_IDS = {
1156
+ CONFIG: "CONFIG",
1157
+ COURSE_REGISTRATIONS: "CourseRegistrations",
1158
+ CLASSROOM_REGISTRATIONS: "ClassroomRegistrations"
1159
+ };
1160
+ // private email: string;
1161
+ _username;
1162
+ syncStrategy;
1163
+ getUsername() {
1164
+ return this._username;
1165
+ }
1166
+ isLoggedIn() {
1167
+ return !this._username.startsWith(GuestUsername);
1168
+ }
1169
+ remoteDB;
1170
+ remote() {
1171
+ return this.remoteDB;
1172
+ }
1173
+ localDB;
1174
+ updateQueue;
1175
+ async createAccount(username, password) {
1176
+ if (!this.syncStrategy.canCreateAccount()) {
1177
+ throw new Error("Account creation not supported by current sync strategy");
1178
+ }
1179
+ if (!this._username.startsWith(GuestUsername)) {
1180
+ throw new Error(
1181
+ `Cannot create a new account while logged in:
1182
+ Currently logged-in as ${this._username}.`
1183
+ );
1184
+ }
1185
+ const result = await this.syncStrategy.createAccount(username, password);
1186
+ if (result.status === Status2.ok) {
1187
+ log2(`Account created successfully, updating username to ${username}`);
1188
+ this._username = username;
1189
+ localStorage.removeItem("dbUUID");
1190
+ await this.init();
1191
+ }
1192
+ return {
1193
+ status: result.status,
1194
+ error: result.error || ""
1195
+ };
1196
+ }
1197
+ async login(username, password) {
1198
+ if (!this.syncStrategy.canAuthenticate()) {
1199
+ throw new Error("Authentication not supported by current sync strategy");
1200
+ }
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}.`);
1204
+ }
1205
+ const loginResult = await this.syncStrategy.authenticate(username, password);
1206
+ if (loginResult.ok) {
1207
+ log2(`Logged in as ${username}`);
1208
+ this._username = username;
1209
+ localStorage.removeItem("dbUUID");
1210
+ await this.init();
1211
+ }
1212
+ return loginResult;
1213
+ }
1214
+ async resetUserData() {
1215
+ if (this.syncStrategy.canAuthenticate()) {
1216
+ return {
1217
+ status: Status2.error,
1218
+ error: "Reset user data is only available for local-only mode. Use logout instead for remote sync."
1219
+ };
1220
+ }
1221
+ try {
1222
+ const localDB = getLocalUserDB(this._username);
1223
+ const allDocs = await localDB.allDocs({ include_docs: false });
1224
+ const docsToDelete = allDocs.rows.filter((row) => {
1225
+ const id = row.id;
1226
+ return id.startsWith(cardHistoryPrefix2) || // Card interaction history
1227
+ id.startsWith(REVIEW_PREFIX) || // Scheduled reviews
1228
+ id === _BaseUser.DOC_IDS.COURSE_REGISTRATIONS || // Course registrations
1229
+ id === _BaseUser.DOC_IDS.CLASSROOM_REGISTRATIONS || // Classroom registrations
1230
+ id === _BaseUser.DOC_IDS.CONFIG;
1231
+ }).map((row) => ({ _id: row.id, _rev: row.value.rev, _deleted: true }));
1232
+ if (docsToDelete.length > 0) {
1233
+ await localDB.bulkDocs(docsToDelete);
1234
+ }
1235
+ await this.init();
1236
+ return { status: Status2.ok };
1237
+ } catch (error) {
1238
+ logger.error("Failed to reset user data:", error);
1239
+ return {
1240
+ status: Status2.error,
1241
+ error: error instanceof Error ? error.message : "Unknown error during reset"
1242
+ };
1243
+ }
1244
+ }
1245
+ async logout() {
1246
+ if (!this.syncStrategy.canAuthenticate()) {
1247
+ this._username = await this.syncStrategy.getCurrentUsername();
1248
+ await this.init();
1249
+ return { ok: true };
1250
+ }
1251
+ const ret = await this.syncStrategy.logout();
1252
+ this._username = await this.syncStrategy.getCurrentUsername();
1253
+ await this.init();
1254
+ return ret;
1255
+ }
1256
+ update(id, update) {
1257
+ return this.updateQueue.update(id, update);
1258
+ }
1259
+ async getCourseRegistrationsDoc() {
1260
+ logger.debug(`Fetching courseRegistrations for ${this.getUsername()}`);
1261
+ let ret;
1262
+ try {
1263
+ const regDoc = await this.localDB.get(
1264
+ _BaseUser.DOC_IDS.COURSE_REGISTRATIONS
1265
+ );
1266
+ return regDoc;
1267
+ } catch (e) {
1268
+ const err = e;
1269
+ if (err.status === 404) {
1270
+ await this.localDB.put({
1271
+ _id: _BaseUser.DOC_IDS.COURSE_REGISTRATIONS,
1272
+ courses: [],
1273
+ studyWeight: {}
1274
+ });
1275
+ ret = await this.getCourseRegistrationsDoc();
1276
+ } else {
1277
+ throw new Error(
1278
+ `Unexpected error ${JSON.stringify(e)} in getOrCreateCourseRegistrationDoc...`
1279
+ );
1280
+ }
1281
+ }
1282
+ return ret;
1283
+ }
1284
+ async getActiveCourses() {
1285
+ const reg = await this.getCourseRegistrationsDoc();
1286
+ return reg.courses.filter((c) => {
1287
+ return c.status === void 0 || c.status === "active";
1288
+ });
1289
+ }
1290
+ /**
1291
+ * Returns a promise of the card IDs that the user has
1292
+ * a scheduled review for.
1293
+ *
1294
+ */
1295
+ async getActiveCards() {
1296
+ const keys = getStartAndEndKeys2(REVIEW_PREFIX);
1297
+ const reviews = await this.remoteDB.allDocs({
1298
+ startkey: keys.startkey,
1299
+ endkey: keys.endkey,
1300
+ include_docs: true
1301
+ });
1302
+ return reviews.rows.map((r) => `${r.doc.courseId}-${r.doc.cardId}`);
1303
+ }
1304
+ async getActivityRecords() {
1305
+ try {
1306
+ const hist = await this.getHistory();
1307
+ const allRecords = [];
1308
+ if (!Array.isArray(hist)) {
1309
+ logger.error("getHistory did not return an array:", hist);
1310
+ return allRecords;
1311
+ }
1312
+ let sampleCount = 0;
1313
+ for (let i = 0; i < hist.length; i++) {
1314
+ try {
1315
+ if (hist[i] && Array.isArray(hist[i].records)) {
1316
+ hist[i].records.forEach((record) => {
1317
+ try {
1318
+ if (!record.timeStamp) {
1319
+ return;
1320
+ }
1321
+ let timeStamp;
1322
+ if (typeof record.timeStamp === "object") {
1323
+ if (typeof record.timeStamp.toDate === "function") {
1324
+ timeStamp = record.timeStamp.toISOString();
1325
+ } else if (record.timeStamp instanceof Date) {
1326
+ timeStamp = record.timeStamp.toISOString();
1327
+ } else {
1328
+ if (sampleCount < 3) {
1329
+ logger.warn("Unknown timestamp object type:", record.timeStamp);
1330
+ sampleCount++;
1331
+ }
1332
+ return;
1333
+ }
1334
+ } else if (typeof record.timeStamp === "string") {
1335
+ const date = new Date(record.timeStamp);
1336
+ if (isNaN(date.getTime())) {
1337
+ return;
1338
+ }
1339
+ timeStamp = record.timeStamp;
1340
+ } else if (typeof record.timeStamp === "number") {
1341
+ timeStamp = new Date(record.timeStamp).toISOString();
1342
+ } else {
1343
+ return;
1344
+ }
1345
+ allRecords.push({
1346
+ timeStamp,
1347
+ courseID: record.courseID || "unknown",
1348
+ cardID: record.cardID || "unknown",
1349
+ timeSpent: record.timeSpent || 0,
1350
+ type: "card_view"
1351
+ });
1352
+ } catch (err) {
1353
+ }
1354
+ });
1355
+ }
1356
+ } catch (err) {
1357
+ logger.error("Error processing history item:", err);
1358
+ }
1359
+ }
1360
+ logger.debug(`Found ${allRecords.length} activity records`);
1361
+ return allRecords;
1362
+ } catch (err) {
1363
+ logger.error("Error in getActivityRecords:", err);
1364
+ return [];
1365
+ }
1366
+ }
1367
+ async getReviewstoDate(targetDate, course_id) {
1368
+ const keys = getStartAndEndKeys2(REVIEW_PREFIX);
1369
+ const reviews = await this.remoteDB.allDocs({
1370
+ startkey: keys.startkey,
1371
+ endkey: keys.endkey,
1372
+ include_docs: true
1373
+ });
1374
+ log2(
1375
+ `Fetching ${this._username}'s scheduled reviews${course_id ? ` for course ${course_id}` : ""}.`
1376
+ );
1377
+ 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);
1380
+ if (targetDate.isAfter(date)) {
1381
+ if (course_id === void 0 || r.doc.courseId === course_id) {
1382
+ return true;
1383
+ }
1384
+ }
1385
+ }
1386
+ }).map((r) => r.doc);
1387
+ }
1388
+ async getReviewsForcast(daysCount) {
1389
+ const time = moment3.utc().add(daysCount, "days");
1390
+ return this.getReviewstoDate(time);
1391
+ }
1392
+ async getPendingReviews(course_id) {
1393
+ const now = moment3.utc();
1394
+ return this.getReviewstoDate(now, course_id);
1395
+ }
1396
+ async getScheduledReviewCount(course_id) {
1397
+ return (await this.getPendingReviews(course_id)).length;
1398
+ }
1399
+ async getRegisteredCourses() {
1400
+ const regDoc = await this.getCourseRegistrationsDoc();
1401
+ return regDoc.courses.filter((c) => {
1402
+ return !c.status || c.status === "active" || c.status === "maintenance-mode";
1403
+ });
1404
+ }
1405
+ async getCourseRegDoc(courseID) {
1406
+ const regDocs = await this.getCourseRegistrationsDoc();
1407
+ const ret = regDocs.courses.find((c) => c.courseID === courseID);
1408
+ if (ret) {
1409
+ return ret;
1410
+ } else {
1411
+ throw new Error(`Course registration not found for course ID: ${courseID}`);
1412
+ }
1413
+ }
1414
+ async registerForCourse(course_id, previewMode = false) {
1415
+ return this.getCourseRegistrationsDoc().then((doc) => {
1416
+ const status = previewMode ? "preview" : "active";
1417
+ logger.debug(`Registering for ${course_id} with status: ${status}`);
1418
+ const regItem = {
1419
+ status,
1420
+ courseID: course_id,
1421
+ user: true,
1422
+ admin: false,
1423
+ moderator: false,
1424
+ elo: {
1425
+ global: {
1426
+ score: 1e3,
1427
+ count: 0
1428
+ },
1429
+ tags: {},
1430
+ misc: {}
1431
+ }
1432
+ };
1433
+ if (doc.courses.filter((course) => {
1434
+ return course.courseID === regItem.courseID;
1435
+ }).length === 0) {
1436
+ log2(`It's a new course registration!`);
1437
+ doc.courses.push(regItem);
1438
+ doc.studyWeight[course_id] = 1;
1439
+ } else {
1440
+ doc.courses.forEach((c) => {
1441
+ log2(`Found the previously registered course!`);
1442
+ if (c.courseID === course_id) {
1443
+ c.status = status;
1444
+ }
1445
+ });
1446
+ }
1447
+ return this.localDB.put(doc);
1448
+ }).catch((e) => {
1449
+ log2(`Registration failed because of: ${JSON.stringify(e)}`);
1450
+ throw e;
1451
+ });
1452
+ }
1453
+ async dropCourse(course_id, dropStatus = "dropped") {
1454
+ return this.getCourseRegistrationsDoc().then((doc) => {
1455
+ let index = -1;
1456
+ for (let i = 0; i < doc.courses.length; i++) {
1457
+ if (doc.courses[i].courseID === course_id) {
1458
+ index = i;
1459
+ }
1460
+ }
1461
+ if (index !== -1) {
1462
+ delete doc.studyWeight[course_id];
1463
+ doc.courses[index].status = dropStatus;
1464
+ } else {
1465
+ throw new Error(
1466
+ `User ${this.getUsername()} is not currently registered for course ${course_id}`
1467
+ );
1468
+ }
1469
+ return this.localDB.put(doc);
1470
+ });
1471
+ }
1472
+ async getCourseInterface(courseId) {
1473
+ return new UsrCrsData(this, courseId);
1474
+ }
1475
+ async getUserEditableCourses() {
1476
+ let courseIDs = [];
1477
+ const registeredCourses = await this.getCourseRegistrationsDoc();
1478
+ courseIDs = courseIDs.concat(
1479
+ registeredCourses.courses.map((course) => {
1480
+ return course.courseID;
1481
+ })
1482
+ );
1483
+ const cfgs = await Promise.all(
1484
+ courseIDs.map(async (id) => {
1485
+ return await getCredentialledCourseConfig(id);
1486
+ })
1487
+ );
1488
+ return cfgs;
1489
+ }
1490
+ async getConfig() {
1491
+ const defaultConfig = {
1492
+ _id: _BaseUser.DOC_IDS.CONFIG,
1493
+ darkMode: false,
1494
+ likesConfetti: false
1495
+ };
1496
+ try {
1497
+ const cfg = await this.localDB.get(_BaseUser.DOC_IDS.CONFIG);
1498
+ logger.debug("Raw config from DB:", cfg);
1499
+ return cfg;
1500
+ } catch (e) {
1501
+ const err = e;
1502
+ if (err.name && err.name === "not_found") {
1503
+ await this.localDB.put(defaultConfig);
1504
+ return this.getConfig();
1505
+ } else {
1506
+ logger.error(`Error setting user default config:`, e);
1507
+ throw new Error(`Error returning the user's configuration: ${JSON.stringify(e)}`);
1508
+ }
1509
+ }
1510
+ }
1511
+ async setConfig(items) {
1512
+ logger.debug(`Setting Config items ${JSON.stringify(items)}`);
1513
+ const c = await this.getConfig();
1514
+ const put = await this.localDB.put({
1515
+ ...c,
1516
+ ...items
1517
+ });
1518
+ if (put.ok) {
1519
+ logger.debug(`Config items set: ${JSON.stringify(items)}`);
1520
+ } else {
1521
+ logger.error(`Error setting config items: ${JSON.stringify(put)}`);
1522
+ }
1523
+ }
1524
+ /**
1525
+ *
1526
+ * This function should be called *only* by the pouchdb datalayer provider
1527
+ * auth store.
1528
+ *
1529
+ *
1530
+ * Anyone else seeking the current user should use the auth store's
1531
+ * exported `getCurrentUser` method.
1532
+ *
1533
+ */
1534
+ static async instance(syncStrategy, username) {
1535
+ if (username) {
1536
+ _BaseUser._instance = new _BaseUser(username, syncStrategy);
1537
+ await _BaseUser._instance.init();
1538
+ return _BaseUser._instance;
1539
+ } else if (_BaseUser._instance && _BaseUser._initialized) {
1540
+ return _BaseUser._instance;
1541
+ } else if (_BaseUser._instance) {
1542
+ return new Promise((resolve) => {
1543
+ (function waitForUser() {
1544
+ if (_BaseUser._initialized) {
1545
+ return resolve(_BaseUser._instance);
1546
+ } else {
1547
+ setTimeout(waitForUser, 50);
1548
+ }
1549
+ })();
1550
+ });
1551
+ } else {
1552
+ const guestUsername = await syncStrategy.getCurrentUsername();
1553
+ _BaseUser._instance = new _BaseUser(guestUsername, syncStrategy);
1554
+ await _BaseUser._instance.init();
1555
+ return _BaseUser._instance;
1556
+ }
1557
+ }
1558
+ constructor(username, syncStrategy) {
1559
+ _BaseUser._initialized = false;
1560
+ this._username = username;
1561
+ this.syncStrategy = syncStrategy;
1562
+ this.setDBandQ();
1563
+ }
1564
+ setDBandQ() {
1565
+ this.localDB = getLocalUserDB(this._username);
1566
+ this.remoteDB = this.syncStrategy.setupRemoteDB(this._username);
1567
+ this.updateQueue = new UpdateQueue(this.localDB);
1568
+ }
1569
+ async init() {
1570
+ _BaseUser._initialized = false;
1571
+ this.setDBandQ();
1572
+ this.syncStrategy.startSync(this.localDB, this.remoteDB);
1573
+ void this.applyDesignDocs();
1574
+ void this.deduplicateReviews();
1575
+ _BaseUser._initialized = true;
1576
+ }
1577
+ static designDocs = [
1578
+ {
1579
+ _id: "_design/reviewCards",
1580
+ views: {
1581
+ reviewCards: {
1582
+ map: `function (doc) {
1583
+ if (doc._id && doc._id.indexOf('card_review') === 0 && doc.courseId && doc.cardId) {
1584
+ emit(doc._id, doc.courseId + '-' + doc.cardId);
1585
+ }
1586
+ }`
1587
+ }
1588
+ }
1589
+ }
1590
+ ];
1591
+ async applyDesignDocs() {
1592
+ for (const doc of _BaseUser.designDocs) {
1593
+ try {
1594
+ try {
1595
+ const existingDoc = await this.remoteDB.get(doc._id);
1596
+ await this.remoteDB.put({
1597
+ ...doc,
1598
+ _rev: existingDoc._rev
1599
+ });
1600
+ } catch (e) {
1601
+ if (e instanceof Error && e.name === "not_found") {
1602
+ await this.remoteDB.put(doc);
1603
+ } else {
1604
+ throw e;
1605
+ }
1606
+ }
1607
+ } catch (error) {
1608
+ if (error instanceof Error && error.name === "conflict") {
1609
+ logger.warn(`Design doc ${doc._id} update conflict - will retry`);
1610
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
1611
+ await this.applyDesignDoc(doc);
1612
+ } else {
1613
+ logger.error(`Failed to apply design doc ${doc._id}:`, error);
1614
+ throw error;
1615
+ }
1616
+ }
1617
+ }
1618
+ }
1619
+ // Helper method for single doc update with retry
1620
+ async applyDesignDoc(doc, retries = 3) {
1621
+ try {
1622
+ const existingDoc = await this.remoteDB.get(doc._id);
1623
+ await this.remoteDB.put({
1624
+ ...doc,
1625
+ _rev: existingDoc._rev
1626
+ });
1627
+ } catch (e) {
1628
+ if (e instanceof Error && e.name === "conflict" && retries > 0) {
1629
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
1630
+ return this.applyDesignDoc(doc, retries - 1);
1631
+ }
1632
+ throw e;
1633
+ }
1634
+ }
1635
+ /**
1636
+ * Logs a record of the user's interaction with the card and returns the card's
1637
+ * up-to-date history
1638
+ *
1639
+ * // [ ] #db-refactor extract to a smaller scope - eg, UserStudySession
1640
+ *
1641
+ * @param record the recent recorded interaction between user and card
1642
+ * @returns The updated state of the card's CardHistory data
1643
+ */
1644
+ async putCardRecord(record) {
1645
+ const cardHistoryID = getCardHistoryID(record.courseID, record.cardID);
1646
+ record.timeStamp = moment3.utc(record.timeStamp).toString();
1647
+ try {
1648
+ const cardHistory = await this.update(
1649
+ cardHistoryID,
1650
+ function(h) {
1651
+ h.records.push(record);
1652
+ h.bestInterval = h.bestInterval || 0;
1653
+ h.lapses = h.lapses || 0;
1654
+ h.streak = h.streak || 0;
1655
+ return h;
1656
+ }
1657
+ );
1658
+ cardHistory.records = cardHistory.records.map((record2) => {
1659
+ const ret = {
1660
+ ...record2
1661
+ };
1662
+ ret.timeStamp = moment3.utc(record2.timeStamp);
1663
+ return ret;
1664
+ });
1665
+ return cardHistory;
1666
+ } catch (e) {
1667
+ const reason = e;
1668
+ if (reason.status === 404) {
1669
+ const initCardHistory = {
1670
+ _id: cardHistoryID,
1671
+ cardID: record.cardID,
1672
+ courseID: record.courseID,
1673
+ records: [record],
1674
+ lapses: 0,
1675
+ streak: 0,
1676
+ bestInterval: 0
1677
+ };
1678
+ void this.remoteDB.put(initCardHistory);
1679
+ return initCardHistory;
1680
+ } else {
1681
+ throw new Error(`putCardRecord failed because of:
1682
+ name:${reason.name}
1683
+ error: ${reason.error}
1684
+ message: ${reason.message}`);
1685
+ }
1686
+ }
1687
+ }
1688
+ async deduplicateReviews() {
1689
+ try {
1690
+ log2("Starting deduplication of scheduled reviews...");
1691
+ const reviewsMap = {};
1692
+ const duplicateDocIds = [];
1693
+ const scheduledReviews = await this.remoteDB.query("reviewCards/reviewCards");
1694
+ log2(`Found ${scheduledReviews.rows.length} scheduled reviews to process`);
1695
+ scheduledReviews.rows.forEach((r) => {
1696
+ const qualifiedCardId = r.value;
1697
+ const docId = r.key;
1698
+ if (reviewsMap[qualifiedCardId]) {
1699
+ log2(`Found duplicate scheduled review for card: ${qualifiedCardId}`);
1700
+ log2(
1701
+ `Marking earlier review ${reviewsMap[qualifiedCardId]} for deletion, keeping ${docId}`
1702
+ );
1703
+ duplicateDocIds.push(reviewsMap[qualifiedCardId]);
1704
+ reviewsMap[qualifiedCardId] = docId;
1705
+ } else {
1706
+ reviewsMap[qualifiedCardId] = docId;
1707
+ }
1708
+ });
1709
+ if (duplicateDocIds.length > 0) {
1710
+ log2(`Removing ${duplicateDocIds.length} duplicate reviews...`);
1711
+ const deletePromises = duplicateDocIds.map(async (docId) => {
1712
+ try {
1713
+ const doc = await this.remoteDB.get(docId);
1714
+ await this.remoteDB.remove(doc);
1715
+ log2(`Successfully removed duplicate review: ${docId}`);
1716
+ } catch (error) {
1717
+ log2(`Failed to remove duplicate review ${docId}: ${error}`);
1718
+ }
1719
+ });
1720
+ await Promise.all(deletePromises);
1721
+ log2(`Deduplication complete. Processed ${duplicateDocIds.length} duplicates`);
1722
+ } else {
1723
+ log2("No duplicate reviews found");
1724
+ }
1725
+ } catch (error) {
1726
+ log2(`Error during review deduplication: ${error}`);
1727
+ }
1728
+ }
1729
+ /**
1730
+ * Returns a promise of the card IDs that the user has
1731
+ * encountered in the past.
1732
+ *
1733
+ * @param course_id optional specification of individual course
1734
+ */
1735
+ async getSeenCards(course_id) {
1736
+ let prefix = cardHistoryPrefix2;
1737
+ if (course_id) {
1738
+ prefix += course_id;
1739
+ }
1740
+ const docs = await filterAllDocsByPrefix2(this.localDB, prefix, {
1741
+ include_docs: false
1742
+ });
1743
+ const ret = [];
1744
+ docs.rows.forEach((row) => {
1745
+ if (row.id.startsWith(cardHistoryPrefix2)) {
1746
+ ret.push(row.id.substr(cardHistoryPrefix2.length));
1747
+ }
1748
+ });
1749
+ return ret;
1750
+ }
1751
+ /**
1752
+ *
1753
+ * @returns A promise of the cards that the user has seen in the past.
1754
+ */
1755
+ async getHistory() {
1756
+ const cards = await filterAllDocsByPrefix2(
1757
+ this.remoteDB,
1758
+ cardHistoryPrefix2,
1759
+ {
1760
+ include_docs: true,
1761
+ attachments: false
1762
+ }
1763
+ );
1764
+ return cards.rows.map((r) => r.doc);
1765
+ }
1766
+ async updateCourseSettings(course_id, settings) {
1767
+ void this.getCourseRegistrationsDoc().then((doc) => {
1768
+ const crs = doc.courses.find((c) => c.courseID === course_id);
1769
+ if (crs) {
1770
+ if (crs.settings === null || crs.settings === void 0) {
1771
+ crs.settings = {};
1772
+ }
1773
+ settings.forEach((setting) => {
1774
+ crs.settings[setting.key] = setting.value;
1775
+ });
1776
+ }
1777
+ return this.localDB.put(doc);
1778
+ });
1779
+ }
1780
+ async getCourseSettings(course_id) {
1781
+ const regDoc = await this.getCourseRegistrationsDoc();
1782
+ const crsDoc = regDoc.courses.find((c) => c.courseID === course_id);
1783
+ if (crsDoc) {
1784
+ return crsDoc.settings;
1785
+ } else {
1786
+ throw new Error(`getCourseSettings Failed:
1787
+ User is not registered for course ${course_id}`);
1788
+ }
1789
+ }
1790
+ async getOrCreateClassroomRegistrationsDoc() {
1791
+ let ret;
1792
+ try {
1793
+ ret = await this.remoteDB.get(
1794
+ _BaseUser.DOC_IDS.CLASSROOM_REGISTRATIONS
1795
+ );
1796
+ } catch (e) {
1797
+ const err = e;
1798
+ if (err.status === 404) {
1799
+ await this.remoteDB.put({
1800
+ _id: _BaseUser.DOC_IDS.CLASSROOM_REGISTRATIONS,
1801
+ registrations: []
1802
+ });
1803
+ ret = await this.getOrCreateClassroomRegistrationsDoc();
1804
+ } else {
1805
+ const errorDetails = {
1806
+ name: err.name,
1807
+ status: err.status,
1808
+ message: err.message,
1809
+ reason: err.reason,
1810
+ error: err.error
1811
+ };
1812
+ logger.error(
1813
+ "Database error in getOrCreateClassroomRegistrationsDoc (private method):",
1814
+ errorDetails
1815
+ );
1816
+ throw new Error(
1817
+ `Database error accessing classroom registrations: ${err.message || err.name || "Unknown error"} (status: ${err.status})`
1818
+ );
1819
+ }
1820
+ }
1821
+ logger.debug(`Returning classroom registrations doc: ${JSON.stringify(ret)}`);
1822
+ return ret;
1823
+ }
1824
+ /**
1825
+ * Retrieves the list of active classroom IDs where the user is registered as a student.
1826
+ *
1827
+ * @returns Promise<string[]> - Array of classroom IDs, or empty array if classroom
1828
+ * registration document is unavailable due to database errors
1829
+ *
1830
+ * @description This method gracefully handles database connectivity issues by returning
1831
+ * an empty array when the classroom registrations document cannot be accessed.
1832
+ * This ensures that users can still access other application features even
1833
+ * when classroom functionality is temporarily unavailable.
1834
+ */
1835
+ async getActiveClasses() {
1836
+ try {
1837
+ return (await this.getOrCreateClassroomRegistrationsDoc()).registrations.filter((c) => c.registeredAs === "student").map((c) => c.classID);
1838
+ } catch (error) {
1839
+ logger.warn(
1840
+ "Failed to load classroom registrations, continuing without classroom data:",
1841
+ error
1842
+ );
1843
+ return [];
1844
+ }
1845
+ }
1846
+ async scheduleCardReview(review) {
1847
+ return scheduleCardReviewLocal(this.remoteDB, review);
1848
+ }
1849
+ async removeScheduledCardReview(reviewId) {
1850
+ return removeScheduledCardReviewLocal(this.remoteDB, reviewId);
1851
+ }
1852
+ async registerForClassroom(_classId, _registerAs) {
1853
+ return registerUserForClassroom(this._username, _classId, _registerAs);
1854
+ }
1855
+ async dropFromClassroom(classId) {
1856
+ return dropUserFromClassroom(this._username, classId);
1857
+ }
1858
+ async getUserClassrooms() {
1859
+ return getUserClassrooms(this._username);
1860
+ }
1861
+ async updateUserElo(courseId, elo) {
1862
+ return updateUserElo(this._username, courseId, elo);
1863
+ }
1864
+ };
1865
+ userCoursesDoc = "CourseRegistrations";
1866
+ userClassroomsDoc = "ClassroomRegistrations";
1867
+ }
1868
+ });
1869
+
1870
+ // src/impl/common/index.ts
1871
+ var init_common = __esm({
1872
+ "src/impl/common/index.ts"() {
1873
+ "use strict";
1874
+ init_SyncStrategy();
1875
+ init_BaseUserDB();
1876
+ init_userDBHelpers();
1877
+ }
1878
+ });
1879
+
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"() {
2085
+ "use strict";
2086
+ init_factory();
2087
+ init_types_legacy();
2088
+ init_logger();
2089
+ 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
+ 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
+ }
2156
+ };
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
+ }
2173
+ });
2174
+
2175
+ // src/core/interfaces/contentSource.ts
2176
+ var init_contentSource = __esm({
2177
+ "src/core/interfaces/contentSource.ts"() {
2178
+ "use strict";
2179
+ init_factory();
2180
+ init_classroomDB2();
2181
+ }
2182
+ });
2183
+
2184
+ // src/core/interfaces/courseDB.ts
2185
+ var init_courseDB2 = __esm({
2186
+ "src/core/interfaces/courseDB.ts"() {
2187
+ "use strict";
2188
+ }
2189
+ });
2190
+
2191
+ // src/core/interfaces/dataLayerProvider.ts
2192
+ var init_dataLayerProvider = __esm({
2193
+ "src/core/interfaces/dataLayerProvider.ts"() {
2194
+ "use strict";
2195
+ }
2196
+ });
2197
+
2198
+ // src/core/interfaces/userDB.ts
2199
+ var init_userDB = __esm({
2200
+ "src/core/interfaces/userDB.ts"() {
2201
+ "use strict";
2202
+ }
2203
+ });
2204
+
2205
+ // src/core/interfaces/index.ts
2206
+ var init_interfaces = __esm({
2207
+ "src/core/interfaces/index.ts"() {
2208
+ "use strict";
2209
+ init_adminDB();
2210
+ init_classroomDB();
2211
+ init_contentSource();
2212
+ init_courseDB2();
2213
+ init_dataLayerProvider();
2214
+ init_userDB();
2215
+ }
2216
+ });
2217
+
2218
+ // src/core/types/user.ts
2219
+ var init_user = __esm({
2220
+ "src/core/types/user.ts"() {
2221
+ "use strict";
2222
+ }
2223
+ });
2224
+
2225
+ // src/core/bulkImport/cardProcessor.ts
2226
+ import { Status as Status4 } from "@vue-skuilder/common";
2227
+ var init_cardProcessor = __esm({
2228
+ "src/core/bulkImport/cardProcessor.ts"() {
2229
+ "use strict";
2230
+ init_logger();
2231
+ }
2232
+ });
2233
+
2234
+ // src/core/bulkImport/types.ts
2235
+ var init_types = __esm({
2236
+ "src/core/bulkImport/types.ts"() {
2237
+ "use strict";
2238
+ }
2239
+ });
2240
+
2241
+ // src/core/bulkImport/index.ts
2242
+ var init_bulkImport = __esm({
2243
+ "src/core/bulkImport/index.ts"() {
2244
+ "use strict";
2245
+ init_cardProcessor();
2246
+ init_types();
2247
+ }
2248
+ });
2249
+
2250
+ // src/core/index.ts
2251
+ var init_core = __esm({
2252
+ "src/core/index.ts"() {
2253
+ "use strict";
2254
+ init_interfaces();
2255
+ init_types_legacy();
2256
+ init_user();
2257
+ init_Loggable();
2258
+ init_util();
2259
+ init_navigators();
2260
+ init_bulkImport();
2261
+ }
2262
+ });
2263
+
2264
+ // src/impl/static/StaticDataUnpacker.ts
2265
+ var pathUtils, nodeFS, StaticDataUnpacker;
2266
+ var init_StaticDataUnpacker = __esm({
2267
+ "src/impl/static/StaticDataUnpacker.ts"() {
2268
+ "use strict";
2269
+ init_logger();
2270
+ init_core();
2271
+ pathUtils = {
2272
+ isAbsolute: (path) => {
2273
+ if (/^[a-zA-Z]:[\\/]/.test(path) || /^\\\\/.test(path)) {
2274
+ return true;
2275
+ }
2276
+ if (path.startsWith("/")) {
2277
+ return true;
2278
+ }
2279
+ return false;
2280
+ }
2281
+ };
2282
+ nodeFS = null;
2283
+ try {
2284
+ if (typeof window === "undefined" && typeof process !== "undefined" && process.versions?.node) {
2285
+ nodeFS = eval("require")("fs");
2286
+ }
2287
+ } catch {
2288
+ }
2289
+ StaticDataUnpacker = class {
2290
+ manifest;
2291
+ basePath;
2292
+ documentCache = /* @__PURE__ */ new Map();
2293
+ chunkCache = /* @__PURE__ */ new Map();
2294
+ indexCache = /* @__PURE__ */ new Map();
2295
+ constructor(manifest, basePath) {
2296
+ this.manifest = manifest;
2297
+ this.basePath = basePath;
2298
+ }
2299
+ /**
2300
+ * Get a document by ID, loading from appropriate chunk if needed
2301
+ */
2302
+ async getDocument(id) {
2303
+ if (this.documentCache.has(id)) {
2304
+ const doc = this.documentCache.get(id);
2305
+ return await this.hydrateAttachments(doc);
2306
+ }
2307
+ const chunk = await this.findChunkForDocument(id);
2308
+ if (!chunk) {
2309
+ logger.error(
2310
+ `Document ${id} not found in any chunk. Available chunks:`,
2311
+ this.manifest.chunks.map((c) => `${c.id} (${c.docType}): ${c.startKey} - ${c.endKey}`)
2312
+ );
2313
+ throw new Error(`Document ${id} not found in any chunk`);
2314
+ }
2315
+ await this.loadChunk(chunk.id);
2316
+ if (this.documentCache.has(id)) {
2317
+ const doc = this.documentCache.get(id);
2318
+ return await this.hydrateAttachments(doc);
2319
+ }
2320
+ logger.error(`Document ${id} not found in chunk ${chunk.id}`);
2321
+ throw new Error(`Document ${id} not found in chunk ${chunk.id}`);
2322
+ }
2323
+ /**
2324
+ * Query cards by ELO score, returning card IDs sorted by ELO
2325
+ */
2326
+ async queryByElo(targetElo, limit = 25) {
2327
+ const eloIndex = await this.loadIndex("elo");
2328
+ if (!eloIndex || !eloIndex.sorted) {
2329
+ logger.warn("ELO index not found or malformed, returning empty results");
2330
+ return [];
2331
+ }
2332
+ const sorted = eloIndex.sorted;
2333
+ let startIndex = 0;
2334
+ let left = 0;
2335
+ let right = sorted.length - 1;
2336
+ while (left <= right) {
2337
+ const mid = Math.floor((left + right) / 2);
2338
+ if (sorted[mid].elo < targetElo) {
2339
+ left = mid + 1;
2340
+ } else {
2341
+ right = mid - 1;
2342
+ }
2343
+ }
2344
+ startIndex = left;
2345
+ const result = [];
2346
+ const halfLimit = Math.floor(limit / 2);
2347
+ for (let i = Math.max(0, startIndex - halfLimit); i < startIndex && result.length < limit; i++) {
2348
+ result.push(sorted[i].cardId);
2349
+ }
2350
+ for (let i = startIndex; i < sorted.length && result.length < limit; i++) {
2351
+ result.push(sorted[i].cardId);
2352
+ }
2353
+ return result;
2354
+ }
2355
+ /**
2356
+ * Get all tag names mapped to their card arrays
2357
+ */
2358
+ async getTagsIndex() {
2359
+ return await this.loadIndex("tags");
2360
+ }
2361
+ /**
2362
+ * Find which chunk contains a specific document ID
2363
+ */
2364
+ 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) {
2373
+ const typeChunks = this.manifest.chunks.filter((c) => c.docType === expectedDocType);
2374
+ for (const chunk of typeChunks) {
2375
+ if (docId >= chunk.startKey && docId <= chunk.endKey) {
2376
+ const exists = await this.verifyDocumentInChunk(docId, chunk);
2377
+ if (exists) {
2378
+ return chunk;
2379
+ }
2380
+ }
2381
+ }
2382
+ return void 0;
2383
+ } 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) {
2397
+ if (docId >= chunk.startKey && docId <= chunk.endKey) {
2398
+ const exists = await this.verifyDocumentInChunk(docId, chunk);
2399
+ if (exists) {
2400
+ return chunk;
2401
+ }
2402
+ }
2403
+ }
2404
+ const otherChunks = this.manifest.chunks.filter(
2405
+ (c) => c.docType !== "CARD" && c.docType !== "DISPLAYABLE_DATA" && c.docType !== "TAG"
2406
+ );
2407
+ for (const chunk of otherChunks) {
2408
+ if (docId >= chunk.startKey && docId <= chunk.endKey) {
2409
+ const exists = await this.verifyDocumentInChunk(docId, chunk);
2410
+ if (exists) {
2411
+ return chunk;
2412
+ }
2413
+ }
2414
+ }
2415
+ return void 0;
2416
+ }
2417
+ }
2418
+ /**
2419
+ * Verify that a document actually exists in a specific chunk by loading and checking it
2420
+ */
2421
+ async verifyDocumentInChunk(docId, chunk) {
2422
+ try {
2423
+ await this.loadChunk(chunk.id);
2424
+ return this.documentCache.has(docId);
2425
+ } catch {
2426
+ return false;
2427
+ }
2428
+ }
2429
+ /**
2430
+ * Load a chunk file and cache its documents
2431
+ */
2432
+ async loadChunk(chunkId) {
2433
+ if (this.chunkCache.has(chunkId)) {
2434
+ return;
2435
+ }
2436
+ const chunk = this.manifest.chunks.find((c) => c.id === chunkId);
2437
+ if (!chunk) {
2438
+ throw new Error(`Chunk ${chunkId} not found in manifest`);
2439
+ }
2440
+ try {
2441
+ const chunkPath = `${this.basePath}/${chunk.path}`;
2442
+ logger.debug(`Loading chunk from ${chunkPath}`);
2443
+ let documents;
2444
+ if (this.isLocalPath(chunkPath) && nodeFS) {
2445
+ const fileContent = await nodeFS.promises.readFile(chunkPath, "utf8");
2446
+ documents = JSON.parse(fileContent);
2447
+ } else {
2448
+ const response = await fetch(chunkPath);
2449
+ if (!response.ok) {
2450
+ throw new Error(
2451
+ `Failed to fetch chunk ${chunkId}: ${response.status} ${response.statusText}`
2452
+ );
2453
+ }
2454
+ documents = await response.json();
2455
+ }
2456
+ this.chunkCache.set(chunkId, documents);
2457
+ for (const doc of documents) {
2458
+ if (doc._id) {
2459
+ this.documentCache.set(doc._id, doc);
2460
+ }
2461
+ }
2462
+ logger.debug(`Loaded ${documents.length} documents from chunk ${chunkId}`);
2463
+ } catch (error) {
2464
+ logger.error(`Failed to load chunk ${chunkId}:`, error);
2465
+ throw error;
2466
+ }
2467
+ }
2468
+ /**
2469
+ * Load an index file and cache it
2470
+ */
2471
+ async loadIndex(indexName) {
2472
+ if (this.indexCache.has(indexName)) {
2473
+ return this.indexCache.get(indexName);
2474
+ }
2475
+ const indexMeta = this.manifest.indices.find((idx) => idx.name === indexName);
2476
+ if (!indexMeta) {
2477
+ throw new Error(`Index ${indexName} not found in manifest`);
2478
+ }
2479
+ try {
2480
+ const indexPath = `${this.basePath}/${indexMeta.path}`;
2481
+ logger.debug(`Loading index from ${indexPath}`);
2482
+ let indexData;
2483
+ if (this.isLocalPath(indexPath) && nodeFS) {
2484
+ const fileContent = await nodeFS.promises.readFile(indexPath, "utf8");
2485
+ indexData = JSON.parse(fileContent);
2486
+ } else {
2487
+ const response = await fetch(indexPath);
2488
+ if (!response.ok) {
2489
+ throw new Error(
2490
+ `Failed to fetch index ${indexName}: ${response.status} ${response.statusText}`
2491
+ );
2492
+ }
2493
+ indexData = await response.json();
2494
+ }
2495
+ this.indexCache.set(indexName, indexData);
2496
+ logger.debug(`Loaded index ${indexName}`);
2497
+ return indexData;
2498
+ } catch (error) {
2499
+ logger.error(`Failed to load index ${indexName}:`, error);
2500
+ throw error;
2501
+ }
2502
+ }
2503
+ /**
2504
+ * Get a document by ID without hydration (raw document access)
2505
+ * Used internally to avoid recursion during attachment hydration
2506
+ */
2507
+ async getRawDocument(id) {
2508
+ if (this.documentCache.has(id)) {
2509
+ return this.documentCache.get(id);
2510
+ }
2511
+ const chunk = await this.findChunkForDocument(id);
2512
+ if (!chunk) {
2513
+ logger.error(
2514
+ `Document ${id} not found in any chunk. Available chunks:`,
2515
+ this.manifest.chunks.map((c) => `${c.id} (${c.docType}): ${c.startKey} - ${c.endKey}`)
2516
+ );
2517
+ throw new Error(`Document ${id} not found in any chunk`);
2518
+ }
2519
+ await this.loadChunk(chunk.id);
2520
+ if (this.documentCache.has(id)) {
2521
+ return this.documentCache.get(id);
2522
+ }
2523
+ logger.error(`Document ${id} not found in chunk ${chunk.id}`);
2524
+ throw new Error(`Document ${id} not found in chunk ${chunk.id}`);
2525
+ }
2526
+ /**
2527
+ * Get attachment URL for a given document and attachment name
2528
+ */
2529
+ getAttachmentUrl(docId, attachmentName) {
2530
+ return `${this.basePath}/attachments/${docId}/${attachmentName}`;
2531
+ }
2532
+ /**
2533
+ * Load attachment data from the document and get the correct file path
2534
+ * Uses raw document access to avoid recursion with hydration
2535
+ */
2536
+ async getAttachmentPath(docId, attachmentName) {
2537
+ try {
2538
+ const doc = await this.getRawDocument(docId);
2539
+ if (doc._attachments && doc._attachments[attachmentName]) {
2540
+ const attachment = doc._attachments[attachmentName];
2541
+ if (attachment.path) {
2542
+ return `${this.basePath}/${attachment.path}`;
2543
+ }
2544
+ }
2545
+ return null;
2546
+ } catch {
2547
+ return null;
2548
+ }
2549
+ }
2550
+ /**
2551
+ * Load attachment as a blob (for browser) or buffer (for Node.js)
2552
+ */
2553
+ async getAttachmentBlob(docId, attachmentName) {
2554
+ const attachmentPath = await this.getAttachmentPath(docId, attachmentName);
2555
+ if (!attachmentPath) {
2556
+ return null;
2557
+ }
2558
+ try {
2559
+ if (this.isLocalPath(attachmentPath) && nodeFS) {
2560
+ const buffer = await nodeFS.promises.readFile(attachmentPath);
2561
+ return buffer;
2562
+ } else {
2563
+ const response = await fetch(attachmentPath);
2564
+ if (!response.ok) {
2565
+ throw new Error(
2566
+ `Failed to fetch attachment ${docId}/${attachmentName}: ${response.status} ${response.statusText}`
2567
+ );
2568
+ }
2569
+ return await response.blob();
2570
+ }
2571
+ } catch (error) {
2572
+ logger.error(`Failed to load attachment ${docId}/${attachmentName}:`, error);
2573
+ return null;
2574
+ }
2575
+ }
2576
+ /**
2577
+ * Hydrate document attachments by converting file paths to blob URLs
2578
+ */
2579
+ async hydrateAttachments(doc) {
2580
+ const typedDoc = doc;
2581
+ if (!typedDoc._attachments) {
2582
+ return doc;
2583
+ }
2584
+ const hydratedDoc = JSON.parse(JSON.stringify(doc));
2585
+ for (const [attachmentName, attachment] of Object.entries(typedDoc._attachments)) {
2586
+ const attachmentData = attachment;
2587
+ if (attachmentData.path) {
2588
+ try {
2589
+ const blob = await this.getAttachmentBlob(typedDoc._id, attachmentName);
2590
+ if (blob) {
2591
+ if (typeof window !== "undefined" && window.URL) {
2592
+ hydratedDoc._attachments[attachmentName] = {
2593
+ ...attachmentData,
2594
+ data: blob,
2595
+ stub: false
2596
+ // Indicates this contains actual data, not just metadata
2597
+ };
2598
+ } else {
2599
+ hydratedDoc._attachments[attachmentName] = {
2600
+ ...attachmentData,
2601
+ buffer: blob
2602
+ // Attach buffer for Node.js use
2603
+ };
2604
+ }
2605
+ } else {
2606
+ logger.warn(
2607
+ `[hydrateAttachments] getAttachmentBlob returned null for ${typedDoc._id}/${attachmentName}. Skipping hydration for this attachment.`
2608
+ );
2609
+ }
2610
+ } catch (error) {
2611
+ logger.warn(
2612
+ `[hydrateAttachments] Failed to hydrate attachment ${typedDoc._id}/${attachmentName}:`,
2613
+ error
2614
+ );
2615
+ }
2616
+ } else {
2617
+ logger.debug(
2618
+ `[hydrateAttachments] Attachment ${attachmentName} for doc ${typedDoc._id} has no path. Skipping blob conversion.`
2619
+ );
2620
+ }
2621
+ }
2622
+ return hydratedDoc;
2623
+ }
2624
+ /**
2625
+ * Clear all caches (useful for testing or memory management)
2626
+ */
2627
+ clearCaches() {
2628
+ this.documentCache.clear();
2629
+ this.chunkCache.clear();
2630
+ this.indexCache.clear();
2631
+ }
2632
+ /**
2633
+ * Get cache statistics for debugging
2634
+ */
2635
+ getCacheStats() {
2636
+ return {
2637
+ documents: this.documentCache.size,
2638
+ chunks: this.chunkCache.size,
2639
+ indices: this.indexCache.size
2640
+ };
2641
+ }
2642
+ /**
2643
+ * Check if a path is a local file path (vs URL)
2644
+ */
2645
+ isLocalPath(filePath) {
2646
+ return !filePath.startsWith("http://") && !filePath.startsWith("https://") && (pathUtils.isAbsolute(filePath) || filePath.startsWith("./") || filePath.startsWith("../"));
2647
+ }
2648
+ };
2649
+ }
2650
+ });
2651
+
2652
+ // src/impl/static/courseDB.ts
2653
+ import { Status as Status5 } from "@vue-skuilder/common";
2654
+ var StaticCourseDB;
2655
+ var init_courseDB3 = __esm({
2656
+ "src/impl/static/courseDB.ts"() {
2657
+ "use strict";
2658
+ init_types_legacy();
2659
+ init_navigators();
2660
+ StaticCourseDB = class {
2661
+ constructor(courseId, unpacker, userDB, manifest) {
2662
+ this.courseId = courseId;
2663
+ this.unpacker = unpacker;
2664
+ this.userDB = userDB;
2665
+ this.manifest = manifest;
2666
+ }
2667
+ getCourseID() {
2668
+ return this.courseId;
2669
+ }
2670
+ async getCourseConfig() {
2671
+ if (this.manifest.courseConfig != null) {
2672
+ return this.manifest.courseConfig;
2673
+ } else {
2674
+ throw new Error(`Course config not found for course ${this.courseId}`);
2675
+ }
2676
+ }
2677
+ async updateCourseConfig(_cfg) {
2678
+ throw new Error("Cannot update course config in static mode");
2679
+ }
2680
+ async getCourseInfo() {
2681
+ return {
2682
+ cardCount: 0,
2683
+ // Would come from manifest
2684
+ registeredUsers: 0
2685
+ };
2686
+ }
2687
+ async getCourseDoc(id, _options) {
2688
+ return this.unpacker.getDocument(id);
2689
+ }
2690
+ async getCourseDocs(ids, _options) {
2691
+ const rows = await Promise.all(
2692
+ ids.map(async (id) => {
2693
+ try {
2694
+ const doc = await this.unpacker.getDocument(id);
2695
+ return {
2696
+ id,
2697
+ key: id,
2698
+ value: { rev: "1-static" },
2699
+ doc
2700
+ };
2701
+ } catch {
2702
+ return {
2703
+ key: id,
2704
+ error: "not_found"
2705
+ };
2706
+ }
2707
+ })
2708
+ );
2709
+ return {
2710
+ total_rows: ids.length,
2711
+ offset: 0,
2712
+ rows
2713
+ };
2714
+ }
2715
+ async getCardsByELO(elo, limit) {
2716
+ return this.unpacker.queryByElo(elo, limit || 25);
2717
+ }
2718
+ async getCardEloData(cardIds) {
2719
+ const results = await Promise.all(
2720
+ cardIds.map(async (id) => {
2721
+ try {
2722
+ const card = await this.unpacker.getDocument(id);
2723
+ return card.elo || { global: { score: 1e3, count: 0 }, tags: {}, misc: {} };
2724
+ } catch {
2725
+ return { global: { score: 1e3, count: 0 }, tags: {}, misc: {} };
2726
+ }
2727
+ })
2728
+ );
2729
+ return results;
2730
+ }
2731
+ async updateCardElo(cardId, _elo) {
2732
+ return { ok: true, id: cardId, rev: "1-static" };
2733
+ }
2734
+ async getNewCards(limit) {
2735
+ const cardIds = await this.unpacker.queryByElo(1e3, limit || 10);
2736
+ return cardIds.map((cardId) => ({
2737
+ status: "new",
2738
+ qualifiedID: `${this.courseId}-${cardId}`,
2739
+ cardID: cardId,
2740
+ contentSourceType: "course",
2741
+ contentSourceID: this.courseId,
2742
+ courseID: this.courseId
2743
+ }));
2744
+ }
2745
+ async getCardsCenteredAtELO(options, filter) {
2746
+ let targetElo = typeof options.elo === "number" ? options.elo : 1e3;
2747
+ if (options.elo === "user") {
2748
+ try {
2749
+ const regDoc = await this.userDB.getCourseRegistrationsDoc();
2750
+ const courseReg = regDoc.courses.find((c) => c.courseID === this.courseId);
2751
+ if (courseReg && typeof courseReg.elo === "object") {
2752
+ targetElo = courseReg.elo.global.score;
2753
+ }
2754
+ } catch {
2755
+ targetElo = 1e3;
2756
+ }
2757
+ } else if (options.elo === "random") {
2758
+ targetElo = 800 + Math.random() * 400;
2759
+ }
2760
+ let cardIds = await this.unpacker.queryByElo(targetElo, options.limit * 2);
2761
+ if (filter) {
2762
+ cardIds = cardIds.filter(filter);
2763
+ }
2764
+ return cardIds.slice(0, options.limit).map((cardId) => ({
2765
+ status: "new",
2766
+ qualifiedID: `${this.courseId}-${cardId}`,
2767
+ cardID: cardId,
2768
+ contentSourceType: "course",
2769
+ contentSourceID: this.courseId,
2770
+ courseID: this.courseId
2771
+ }));
2772
+ }
2773
+ async getAppliedTags(_cardId) {
2774
+ return {
2775
+ total_rows: 0,
2776
+ offset: 0,
2777
+ rows: []
2778
+ };
2779
+ }
2780
+ async addTagToCard(_cardId, _tagId) {
2781
+ throw new Error("Cannot modify tags in static mode");
2782
+ }
2783
+ async removeTagFromCard(_cardId, _tagId) {
2784
+ throw new Error("Cannot modify tags in static mode");
2785
+ }
2786
+ async createTag(_tagName) {
2787
+ throw new Error("Cannot create tags in static mode");
2788
+ }
2789
+ async getTag(tagName) {
2790
+ return this.unpacker.getDocument(`${"TAG" /* TAG */}-${tagName}`);
2791
+ }
2792
+ async updateTag(_tag) {
2793
+ throw new Error("Cannot update tags in static mode");
2794
+ }
2795
+ async getCourseTagStubs() {
2796
+ return {
2797
+ total_rows: 0,
2798
+ offset: 0,
2799
+ rows: []
2800
+ };
2801
+ }
2802
+ async addNote(_codeCourse, _shape, _data, _author, _tags, _uploads, _elo) {
2803
+ return {
2804
+ status: Status5.error,
2805
+ message: "Cannot add notes in static mode"
2806
+ };
2807
+ }
2808
+ async removeCard(_cardId) {
2809
+ throw new Error("Cannot remove cards in static mode");
2810
+ }
2811
+ async getInexperiencedCards() {
2812
+ return [];
2813
+ }
2814
+ // Navigation Strategy Manager implementation
2815
+ async getNavigationStrategy(_id) {
2816
+ return {
2817
+ id: "ELO",
2818
+ docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
2819
+ name: "ELO",
2820
+ description: "ELO-based navigation strategy",
2821
+ implementingClass: "elo" /* ELO */,
2822
+ course: this.courseId,
2823
+ serializedData: ""
2824
+ };
2825
+ }
2826
+ async getAllNavigationStrategies() {
2827
+ return [await this.getNavigationStrategy("ELO")];
2828
+ }
2829
+ async addNavigationStrategy(_data) {
2830
+ throw new Error("Cannot add navigation strategies in static mode");
2831
+ }
2832
+ async updateNavigationStrategy(_id, _data) {
2833
+ throw new Error("Cannot update navigation strategies in static mode");
2834
+ }
2835
+ async surfaceNavigationStrategy() {
2836
+ return this.getNavigationStrategy("ELO");
2837
+ }
2838
+ // Study Content Source implementation
2839
+ async getPendingReviews() {
2840
+ return [];
2841
+ }
2842
+ // Attachment helper methods (internal use, not part of interface)
2843
+ /**
2844
+ * Get attachment URL for a document and attachment name
2845
+ * Internal helper method for static attachment serving
2846
+ */
2847
+ getAttachmentUrl(docId, attachmentName) {
2848
+ return this.unpacker.getAttachmentUrl(docId, attachmentName);
2849
+ }
2850
+ /**
2851
+ * Load attachment as blob/buffer
2852
+ * Internal helper method for static attachment serving
2853
+ */
2854
+ async getAttachmentBlob(docId, attachmentName) {
2855
+ return this.unpacker.getAttachmentBlob(docId, attachmentName);
2856
+ }
2857
+ };
2858
+ }
2859
+ });
2860
+
2861
+ // src/impl/static/coursesDB.ts
2862
+ var StaticCoursesDB;
2863
+ var init_coursesDB = __esm({
2864
+ "src/impl/static/coursesDB.ts"() {
2865
+ "use strict";
2866
+ init_logger();
2867
+ StaticCoursesDB = class {
2868
+ constructor(manifests) {
2869
+ this.manifests = manifests;
2870
+ }
2871
+ async getCourseConfig(courseId) {
2872
+ if (!this.manifests[courseId]) {
2873
+ logger.warn(`Course ${courseId} not found`);
2874
+ return {};
2875
+ }
2876
+ return {};
2877
+ }
2878
+ async getCourseList() {
2879
+ return Object.keys(this.manifests).map(
2880
+ (courseId) => ({
2881
+ courseID: courseId,
2882
+ name: this.manifests[courseId].courseName
2883
+ // ... other config fields
2884
+ })
2885
+ );
2886
+ }
2887
+ async disambiguateCourse(_courseId, _disambiguator) {
2888
+ logger.warn("Cannot disambiguate courses in static mode");
2889
+ }
2890
+ };
2891
+ }
2892
+ });
2893
+
2894
+ // src/impl/static/NoOpSyncStrategy.ts
2895
+ var NoOpSyncStrategy;
2896
+ var init_NoOpSyncStrategy = __esm({
2897
+ "src/impl/static/NoOpSyncStrategy.ts"() {
2898
+ "use strict";
2899
+ init_types_legacy();
2900
+ init_common();
2901
+ NoOpSyncStrategy = class {
2902
+ currentUsername = GuestUsername;
2903
+ setupRemoteDB(username) {
2904
+ return getLocalUserDB(username);
2905
+ }
2906
+ startSync(_localDB, _remoteDB) {
2907
+ }
2908
+ stopSync() {
2909
+ }
2910
+ canCreateAccount() {
2911
+ return false;
2912
+ }
2913
+ canAuthenticate() {
2914
+ return false;
2915
+ }
2916
+ async createAccount(_username, _password) {
2917
+ throw new Error(
2918
+ "Account creation not supported in static mode. Use local account switching instead."
2919
+ );
2920
+ }
2921
+ async authenticate(_username, _password) {
2922
+ throw new Error(
2923
+ "Remote authentication not supported in static mode. Use local account switching instead."
2924
+ );
2925
+ }
2926
+ async logout() {
2927
+ this.currentUsername = GuestUsername;
2928
+ return {
2929
+ ok: true
2930
+ };
2931
+ }
2932
+ async getCurrentUsername() {
2933
+ return this.currentUsername;
2934
+ }
2935
+ /**
2936
+ * Set the current username (for local account switching)
2937
+ * This method is specific to NoOpSyncStrategy and not part of the base interface
2938
+ */
2939
+ setCurrentUsername(username) {
2940
+ this.currentUsername = username;
2941
+ }
2942
+ };
2943
+ }
2944
+ });
2945
+
2946
+ // src/impl/static/StaticDataLayerProvider.ts
2947
+ var StaticDataLayerProvider;
2948
+ var init_StaticDataLayerProvider = __esm({
2949
+ "src/impl/static/StaticDataLayerProvider.ts"() {
2950
+ "use strict";
2951
+ init_logger();
2952
+ init_StaticDataUnpacker();
2953
+ init_courseDB3();
2954
+ init_coursesDB();
2955
+ init_common();
2956
+ init_NoOpSyncStrategy();
2957
+ StaticDataLayerProvider = class {
2958
+ config;
2959
+ initialized = false;
2960
+ courseUnpackers = /* @__PURE__ */ new Map();
2961
+ constructor(config) {
2962
+ this.config = {
2963
+ staticContentPath: config.staticContentPath || "/static-courses",
2964
+ localStoragePrefix: config.localStoragePrefix || "skuilder-static",
2965
+ manifests: config.manifests || {}
2966
+ };
2967
+ }
2968
+ async initialize() {
2969
+ if (this.initialized) return;
2970
+ logger.info("Initializing static data layer provider");
2971
+ for (const [courseId, manifest] of Object.entries(this.config.manifests)) {
2972
+ const unpacker = new StaticDataUnpacker(
2973
+ manifest,
2974
+ `${this.config.staticContentPath}/${courseId}`
2975
+ );
2976
+ this.courseUnpackers.set(courseId, unpacker);
2977
+ }
2978
+ this.initialized = true;
2979
+ }
2980
+ async teardown() {
2981
+ this.courseUnpackers.clear();
2982
+ this.initialized = false;
2983
+ }
2984
+ getUserDB() {
2985
+ const syncStrategy = new NoOpSyncStrategy();
2986
+ return BaseUser.Dummy(syncStrategy);
2987
+ }
2988
+ getCourseDB(courseId) {
2989
+ const unpacker = this.courseUnpackers.get(courseId);
2990
+ if (!unpacker) {
2991
+ throw new Error(`Course ${courseId} not found in static data`);
2992
+ }
2993
+ const manifest = this.config.manifests[courseId];
2994
+ return new StaticCourseDB(courseId, unpacker, this.getUserDB(), manifest);
2995
+ }
2996
+ getCoursesDB() {
2997
+ return new StaticCoursesDB(this.config.manifests);
2998
+ }
2999
+ async getClassroomDB(_classId, _type) {
3000
+ throw new Error("Classrooms not supported in static mode");
3001
+ }
3002
+ getAdminDB() {
3003
+ throw new Error("Admin functions not supported in static mode");
3004
+ }
3005
+ isReadOnly() {
3006
+ return true;
3007
+ }
3008
+ };
3009
+ }
3010
+ });
3011
+
3012
+ // src/impl/static/index.ts
3013
+ init_StaticDataLayerProvider();
3014
+ init_StaticDataUnpacker();
3015
+ init_courseDB3();
3016
+ init_coursesDB();
3017
+ init_NoOpSyncStrategy();
3018
+ export {
3019
+ NoOpSyncStrategy,
3020
+ StaticCourseDB,
3021
+ StaticCoursesDB,
3022
+ StaticDataLayerProvider,
3023
+ StaticDataUnpacker
3024
+ };
3025
+ //# sourceMappingURL=index.mjs.map