@vue-skuilder/db 0.1.11-7 → 0.1.11
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.
- package/dist/core/index.d.mts +5 -5
- package/dist/core/index.d.ts +5 -5
- package/dist/core/index.js +212 -50
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +212 -50
- package/dist/core/index.mjs.map +1 -1
- package/dist/{dataLayerProvider-DqtNroSh.d.ts → dataLayerProvider-VieuAAkV.d.mts} +8 -1
- package/dist/{dataLayerProvider-BInqI_RF.d.mts → dataLayerProvider-juuqUHOP.d.ts} +8 -1
- package/dist/impl/couch/index.d.mts +19 -7
- package/dist/impl/couch/index.d.ts +19 -7
- package/dist/impl/couch/index.js +229 -63
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +228 -62
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.d.mts +13 -6
- package/dist/impl/static/index.d.ts +13 -6
- package/dist/impl/static/index.js +142 -46
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +142 -46
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/{index-CLL31bEy.d.ts → index-CWY6yhkV.d.ts} +1 -1
- package/dist/{index-CUNnL38E.d.mts → index-DZyxHCcf.d.mts} +1 -1
- package/dist/index.d.mts +28 -20
- package/dist/index.d.ts +28 -20
- package/dist/index.js +374 -108
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +378 -108
- package/dist/index.mjs.map +1 -1
- package/dist/pouch/index.d.mts +1 -0
- package/dist/pouch/index.d.ts +1 -0
- package/dist/pouch/index.js +49 -0
- package/dist/pouch/index.js.map +1 -0
- package/dist/pouch/index.mjs +16 -0
- package/dist/pouch/index.mjs.map +1 -0
- package/dist/{types-DC-ckZug.d.mts → types-Che4wTwA.d.mts} +1 -1
- package/dist/{types-BefDGkKa.d.ts → types-DtoI27Xh.d.ts} +1 -1
- package/dist/{types-legacy-Birv-Jx6.d.mts → types-legacy-B8ahaCbj.d.mts} +5 -1
- package/dist/{types-legacy-Birv-Jx6.d.ts → types-legacy-B8ahaCbj.d.ts} +5 -1
- package/dist/{userDB-C33Hzjgn.d.mts → userDB-B7zTQ123.d.ts} +88 -55
- package/dist/{userDB-DusL7OXe.d.ts → userDB-DJ8HMw83.d.mts} +88 -55
- package/dist/util/packer/index.d.mts +3 -3
- package/dist/util/packer/index.d.ts +3 -3
- package/package.json +3 -3
- package/src/core/interfaces/contentSource.ts +3 -2
- package/src/core/interfaces/courseDB.ts +26 -3
- package/src/core/interfaces/dataLayerProvider.ts +9 -1
- package/src/core/interfaces/userDB.ts +80 -64
- package/src/core/navigators/elo.ts +10 -7
- package/src/core/navigators/index.ts +1 -1
- package/src/core/types/types-legacy.ts +5 -0
- package/src/impl/common/BaseUserDB.ts +45 -3
- package/src/impl/couch/CouchDBSyncStrategy.ts +2 -2
- package/src/impl/couch/PouchDataLayerProvider.ts +21 -0
- package/src/impl/couch/adminDB.ts +2 -2
- package/src/impl/couch/auth.ts +13 -4
- package/src/impl/couch/classroomDB.ts +10 -12
- package/src/impl/couch/courseAPI.ts +2 -2
- package/src/impl/couch/courseDB.ts +130 -11
- package/src/impl/couch/courseLookupDB.ts +4 -3
- package/src/impl/couch/index.ts +36 -4
- package/src/impl/couch/pouchdb-setup.ts +3 -3
- package/src/impl/couch/updateQueue.ts +51 -33
- package/src/impl/static/StaticDataLayerProvider.ts +11 -0
- package/src/impl/static/courseDB.ts +47 -8
- package/src/pouch/index.ts +2 -0
- package/src/study/SessionController.ts +168 -51
- package/src/study/SpacedRepetition.ts +1 -1
- package/tsup.config.ts +1 -0
|
@@ -18,7 +18,14 @@ import {
|
|
|
18
18
|
StudySessionNewItem,
|
|
19
19
|
StudySessionReviewItem,
|
|
20
20
|
} from '../../core/interfaces/contentSource';
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
CardData,
|
|
23
|
+
DocType,
|
|
24
|
+
QualifiedCardID,
|
|
25
|
+
SkuilderCourseData,
|
|
26
|
+
Tag,
|
|
27
|
+
TagStub,
|
|
28
|
+
} from '../../core/types/types-legacy';
|
|
22
29
|
import { logger } from '../../util/logger';
|
|
23
30
|
import { GET_CACHED } from './clientCache';
|
|
24
31
|
import { addNote55, addTagToCard, getCredentialledCourseConfig, getTagID } from './courseAPI';
|
|
@@ -263,7 +270,7 @@ export class CourseDB implements StudyContentSource, CourseDBInterface {
|
|
|
263
270
|
limit: aboveLimit,
|
|
264
271
|
startkey: elo + 1,
|
|
265
272
|
});
|
|
266
|
-
//
|
|
273
|
+
// logger.log(JSON.stringify(below));
|
|
267
274
|
|
|
268
275
|
let cards = below.rows;
|
|
269
276
|
cards = cards.concat(above.rows);
|
|
@@ -277,7 +284,13 @@ export class CourseDB implements StudyContentSource, CourseDBInterface {
|
|
|
277
284
|
return s;
|
|
278
285
|
}
|
|
279
286
|
})
|
|
280
|
-
.map((c) =>
|
|
287
|
+
.map((c) => {
|
|
288
|
+
return {
|
|
289
|
+
courseID: this.id,
|
|
290
|
+
cardID: c.id,
|
|
291
|
+
elo: c.key,
|
|
292
|
+
};
|
|
293
|
+
});
|
|
281
294
|
|
|
282
295
|
const str = `below:\n${below.rows.map((r) => `\t${r.id}-${r.key}\n`)}
|
|
283
296
|
|
|
@@ -342,7 +355,13 @@ above:\n${above.rows.map((r) => `\t${r.id}-${r.key}\n`)}`;
|
|
|
342
355
|
tagId: string,
|
|
343
356
|
updateELO?: boolean
|
|
344
357
|
): Promise<PouchDB.Core.Response> {
|
|
345
|
-
return await addTagToCard(
|
|
358
|
+
return await addTagToCard(
|
|
359
|
+
this.id,
|
|
360
|
+
cardId,
|
|
361
|
+
tagId,
|
|
362
|
+
(await this._getCurrentUser()).getUsername(),
|
|
363
|
+
updateELO
|
|
364
|
+
);
|
|
346
365
|
}
|
|
347
366
|
|
|
348
367
|
async removeTagFromCard(cardId: string, tagId: string): Promise<PouchDB.Core.Response> {
|
|
@@ -535,7 +554,7 @@ above:\n${above.rows.map((r) => `\t${r.id}-${r.key}\n`)}`;
|
|
|
535
554
|
limit: 99,
|
|
536
555
|
elo: 'user',
|
|
537
556
|
},
|
|
538
|
-
filter?: (a:
|
|
557
|
+
filter?: (a: QualifiedCardID) => boolean
|
|
539
558
|
): Promise<StudySessionItem[]> {
|
|
540
559
|
let targetElo: number;
|
|
541
560
|
|
|
@@ -554,12 +573,12 @@ above:\n${above.rows.map((r) => `\t${r.id}-${r.key}\n`)}`;
|
|
|
554
573
|
} else if (options.elo === 'random') {
|
|
555
574
|
const bounds = await GET_CACHED(`elo-bounds-${this.id}`, () => this.getELOBounds());
|
|
556
575
|
targetElo = Math.round(bounds.low + Math.random() * (bounds.high - bounds.low));
|
|
557
|
-
//
|
|
576
|
+
// logger.log(`Picked ${targetElo} from [${bounds.low}, ${bounds.high}]`);
|
|
558
577
|
} else {
|
|
559
578
|
targetElo = options.elo;
|
|
560
579
|
}
|
|
561
580
|
|
|
562
|
-
let cards:
|
|
581
|
+
let cards: (QualifiedCardID & { elo?: number })[] = [];
|
|
563
582
|
let mult: number = 4;
|
|
564
583
|
let previousCount: number = -1;
|
|
565
584
|
let newCount: number = 0;
|
|
@@ -579,7 +598,11 @@ above:\n${above.rows.map((r) => `\t${r.id}-${r.key}\n`)}`;
|
|
|
579
598
|
mult *= 2;
|
|
580
599
|
}
|
|
581
600
|
|
|
582
|
-
const selectedCards:
|
|
601
|
+
const selectedCards: {
|
|
602
|
+
courseID: string;
|
|
603
|
+
cardID: string;
|
|
604
|
+
elo?: number;
|
|
605
|
+
}[] = [];
|
|
583
606
|
|
|
584
607
|
while (selectedCards.length < options.limit && cards.length > 0) {
|
|
585
608
|
const index = randIntWeightedTowardZero(cards.length);
|
|
@@ -588,17 +611,113 @@ above:\n${above.rows.map((r) => `\t${r.id}-${r.key}\n`)}`;
|
|
|
588
611
|
}
|
|
589
612
|
|
|
590
613
|
return selectedCards.map((c) => {
|
|
591
|
-
const split = c.split('-');
|
|
592
614
|
return {
|
|
593
615
|
courseID: this.id,
|
|
594
|
-
cardID:
|
|
595
|
-
qualifiedID: `${split[0]}-${split[1]}`,
|
|
616
|
+
cardID: c.cardID,
|
|
596
617
|
contentSourceType: 'course',
|
|
597
618
|
contentSourceID: this.id,
|
|
619
|
+
elo: c.elo,
|
|
598
620
|
status: 'new',
|
|
599
621
|
};
|
|
600
622
|
});
|
|
601
623
|
}
|
|
624
|
+
|
|
625
|
+
// Admin search methods
|
|
626
|
+
public async searchCards(query: string): Promise<any[]> {
|
|
627
|
+
logger.log(`[CourseDB ${this.id}] Searching for: "${query}"`);
|
|
628
|
+
|
|
629
|
+
// Try multiple search approaches
|
|
630
|
+
let displayableData;
|
|
631
|
+
|
|
632
|
+
try {
|
|
633
|
+
// Try regex search on the correct data structure: data[0].data
|
|
634
|
+
displayableData = await this.db.find({
|
|
635
|
+
selector: {
|
|
636
|
+
docType: 'DISPLAYABLE_DATA',
|
|
637
|
+
'data.0.data': { $regex: `.*${query}.*` },
|
|
638
|
+
},
|
|
639
|
+
});
|
|
640
|
+
logger.log(`[CourseDB ${this.id}] Regex search on data[0].data successful`);
|
|
641
|
+
} catch (regexError) {
|
|
642
|
+
logger.log(
|
|
643
|
+
`[CourseDB ${this.id}] Regex search failed, falling back to manual search:`,
|
|
644
|
+
regexError
|
|
645
|
+
);
|
|
646
|
+
|
|
647
|
+
// Fallback: get all displayable data and filter manually
|
|
648
|
+
const allDisplayable = await this.db.find({
|
|
649
|
+
selector: {
|
|
650
|
+
docType: 'DISPLAYABLE_DATA',
|
|
651
|
+
},
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
logger.log(
|
|
655
|
+
`[CourseDB ${this.id}] Retrieved ${allDisplayable.docs.length} documents for manual filtering`
|
|
656
|
+
);
|
|
657
|
+
|
|
658
|
+
displayableData = {
|
|
659
|
+
docs: allDisplayable.docs.filter((doc) => {
|
|
660
|
+
// Search entire document as JSON string - inclusive approach for admin tool
|
|
661
|
+
const docString = JSON.stringify(doc).toLowerCase();
|
|
662
|
+
const match = docString.includes(query.toLowerCase());
|
|
663
|
+
if (match) {
|
|
664
|
+
logger.log(`[CourseDB ${this.id}] Manual match found in document: ${doc._id}`);
|
|
665
|
+
}
|
|
666
|
+
return match;
|
|
667
|
+
}),
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
logger.log(
|
|
672
|
+
`[CourseDB ${this.id}] Found ${displayableData.docs.length} displayable data documents`
|
|
673
|
+
);
|
|
674
|
+
|
|
675
|
+
if (displayableData.docs.length === 0) {
|
|
676
|
+
// Debug: Let's see what displayable data exists
|
|
677
|
+
const allDisplayableData = await this.db.find({
|
|
678
|
+
selector: {
|
|
679
|
+
docType: 'DISPLAYABLE_DATA',
|
|
680
|
+
},
|
|
681
|
+
limit: 5, // Just sample a few
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
logger.log(
|
|
685
|
+
`[CourseDB ${this.id}] Sample displayable data:`,
|
|
686
|
+
allDisplayableData.docs.map((d) => ({
|
|
687
|
+
id: d._id,
|
|
688
|
+
docType: (d as any).docType,
|
|
689
|
+
dataStructure: (d as any).data ? Object.keys((d as any).data) : 'no data field',
|
|
690
|
+
dataContent: (d as any).data,
|
|
691
|
+
fullDoc: d,
|
|
692
|
+
}))
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
const allResults: any[] = [];
|
|
697
|
+
|
|
698
|
+
for (const dd of displayableData.docs) {
|
|
699
|
+
const cards = await this.db.find({
|
|
700
|
+
selector: {
|
|
701
|
+
docType: 'CARD',
|
|
702
|
+
id_displayable_data: { $in: [dd._id] },
|
|
703
|
+
},
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
logger.log(
|
|
707
|
+
`[CourseDB ${this.id}] Displayable data ${dd._id} linked to ${cards.docs.length} cards`
|
|
708
|
+
);
|
|
709
|
+
allResults.push(...cards.docs);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
logger.log(`[CourseDB ${this.id}] Total cards found: ${allResults.length}`);
|
|
713
|
+
return allResults;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
public async find(
|
|
717
|
+
request: PouchDB.Find.FindRequest<any>
|
|
718
|
+
): Promise<PouchDB.Find.FindResponse<any>> {
|
|
719
|
+
return this.db.find(request);
|
|
720
|
+
}
|
|
602
721
|
}
|
|
603
722
|
|
|
604
723
|
/**
|
|
@@ -105,15 +105,15 @@ export default class CourseLookup {
|
|
|
105
105
|
* @returns Promise<void>
|
|
106
106
|
*/
|
|
107
107
|
static async addWithId(
|
|
108
|
-
courseId: string,
|
|
109
|
-
courseName: string,
|
|
108
|
+
courseId: string,
|
|
109
|
+
courseName: string,
|
|
110
110
|
disambiguator?: string
|
|
111
111
|
): Promise<void> {
|
|
112
112
|
const doc: Omit<CourseLookupDoc, '_rev'> = {
|
|
113
113
|
_id: courseId,
|
|
114
114
|
name: courseName,
|
|
115
115
|
};
|
|
116
|
-
|
|
116
|
+
|
|
117
117
|
if (disambiguator) {
|
|
118
118
|
doc.disambiguator = disambiguator;
|
|
119
119
|
}
|
|
@@ -130,6 +130,7 @@ export default class CourseLookup {
|
|
|
130
130
|
return await CourseLookup._db.remove(doc);
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
// [ ] rename to allCourses()
|
|
133
134
|
static async allCourseWare(): Promise<CourseLookupDoc[]> {
|
|
134
135
|
const resp = await CourseLookup._db.allDocs<CourseLookupDoc>({
|
|
135
136
|
include_docs: true,
|
package/src/impl/couch/index.ts
CHANGED
|
@@ -38,7 +38,7 @@ export function hexEncode(str: string): string {
|
|
|
38
38
|
|
|
39
39
|
return returnStr;
|
|
40
40
|
}
|
|
41
|
-
|
|
41
|
+
const pouchDBincludeCredentialsConfig: PouchDB.Configuration.RemoteDatabaseConfiguration = {
|
|
42
42
|
fetch(url: string | Request, opts: RequestInit): Promise<Response> {
|
|
43
43
|
opts.credentials = 'include';
|
|
44
44
|
|
|
@@ -46,10 +46,42 @@ export const pouchDBincludeCredentialsConfig: PouchDB.Configuration.RemoteDataba
|
|
|
46
46
|
},
|
|
47
47
|
} as PouchDB.Configuration.RemoteDatabaseConfiguration;
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Creates PouchDB configuration with appropriate authentication method
|
|
51
|
+
* - Uses HTTP Basic Auth when credentials are available (Node.js/MCP)
|
|
52
|
+
* - Falls back to cookie auth for browser environments
|
|
53
|
+
*/
|
|
54
|
+
export function createPouchDBConfig(): PouchDB.Configuration.RemoteDatabaseConfiguration {
|
|
55
|
+
// Check if running in Node.js with explicit credentials
|
|
56
|
+
const hasExplicitCredentials = ENV.COUCHDB_USERNAME && ENV.COUCHDB_PASSWORD;
|
|
57
|
+
const isNodeEnvironment = typeof window === 'undefined';
|
|
58
|
+
|
|
59
|
+
if (hasExplicitCredentials && isNodeEnvironment) {
|
|
60
|
+
// Use HTTP Basic Auth for Node.js environments (MCP server)
|
|
61
|
+
return {
|
|
62
|
+
fetch(url: string | Request, opts: RequestInit = {}): Promise<Response> {
|
|
63
|
+
const basicAuth = btoa(`${ENV.COUCHDB_USERNAME}:${ENV.COUCHDB_PASSWORD}`);
|
|
64
|
+
const headers = new Headers(opts.headers || {});
|
|
65
|
+
headers.set('Authorization', `Basic ${basicAuth}`);
|
|
66
|
+
|
|
67
|
+
const newOpts = {
|
|
68
|
+
...opts,
|
|
69
|
+
headers: headers
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return (pouch as any).fetch(url, newOpts);
|
|
73
|
+
}
|
|
74
|
+
} as PouchDB.Configuration.RemoteDatabaseConfiguration;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Use cookie-based auth for browser environments or when no explicit credentials
|
|
78
|
+
return pouchDBincludeCredentialsConfig;
|
|
79
|
+
}
|
|
80
|
+
|
|
49
81
|
function getCouchDB(dbName: string): PouchDB.Database {
|
|
50
82
|
return new pouch(
|
|
51
83
|
ENV.COUCHDB_SERVER_PROTOCOL + '://' + ENV.COUCHDB_SERVER_URL + dbName,
|
|
52
|
-
|
|
84
|
+
createPouchDBConfig()
|
|
53
85
|
);
|
|
54
86
|
}
|
|
55
87
|
|
|
@@ -57,7 +89,7 @@ export function getCourseDB(courseID: string): PouchDB.Database {
|
|
|
57
89
|
// todo: keep a cache of opened courseDBs? need to benchmark this somehow
|
|
58
90
|
return new pouch(
|
|
59
91
|
ENV.COUCHDB_SERVER_PROTOCOL + '://' + ENV.COUCHDB_SERVER_URL + 'coursedb-' + courseID,
|
|
60
|
-
|
|
92
|
+
createPouchDBConfig()
|
|
61
93
|
);
|
|
62
94
|
}
|
|
63
95
|
|
|
@@ -188,7 +220,7 @@ export function getCouchUserDB(username: string): PouchDB.Database {
|
|
|
188
220
|
// see: https://github.com/pouchdb-community/pouchdb-authentication/issues/239
|
|
189
221
|
const ret = new pouch(
|
|
190
222
|
ENV.COUCHDB_SERVER_PROTOCOL + '://' + ENV.COUCHDB_SERVER_URL + dbName,
|
|
191
|
-
|
|
223
|
+
createPouchDBConfig()
|
|
192
224
|
);
|
|
193
225
|
if (guestAccount) {
|
|
194
226
|
updateGuestAccountExpirationDate(ret);
|
|
@@ -52,43 +52,61 @@ export default class UpdateQueue extends Loggable {
|
|
|
52
52
|
if (this.pendingUpdates[id] && this.pendingUpdates[id].length > 0) {
|
|
53
53
|
this.inprogressUpdates[id] = true;
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
55
|
+
const MAX_RETRIES = 5;
|
|
56
|
+
for (let i = 0; i < MAX_RETRIES; i++) {
|
|
57
|
+
try {
|
|
58
|
+
const doc = await this.readDB.get<T>(id);
|
|
59
|
+
logger.debug(`Retrieved doc: ${id}`);
|
|
60
|
+
|
|
61
|
+
// Create a new doc object to apply updates to for this attempt
|
|
62
|
+
let updatedDoc = { ...doc };
|
|
63
|
+
|
|
64
|
+
// Note: This loop is not fully safe if updates are functions that depend on a specific doc state
|
|
65
|
+
// that might change between retries. But for simple object merges, it's okay.
|
|
66
|
+
const updatesToApply = [...this.pendingUpdates[id]];
|
|
67
|
+
for (const update of updatesToApply) {
|
|
68
|
+
if (typeof update === 'function') {
|
|
69
|
+
updatedDoc = { ...updatedDoc, ...update(updatedDoc) };
|
|
70
|
+
} else {
|
|
71
|
+
updatedDoc = {
|
|
72
|
+
...updatedDoc,
|
|
73
|
+
...update,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
67
76
|
}
|
|
68
|
-
}
|
|
69
|
-
// for (const k in doc) {
|
|
70
|
-
// console.log(`${k}: ${typeof k}`);
|
|
71
|
-
// }
|
|
72
|
-
// console.log(`Applied updates to doc: ${JSON.stringify(doc)}`);
|
|
73
|
-
await this.writeDB.put<T>(doc);
|
|
74
|
-
logger.debug(`Put doc: ${id}`);
|
|
75
77
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
78
|
+
await this.writeDB.put<T>(updatedDoc);
|
|
79
|
+
logger.debug(`Put doc: ${id}`);
|
|
80
|
+
|
|
81
|
+
// Success! Remove the updates we just applied.
|
|
82
|
+
this.pendingUpdates[id].splice(0, updatesToApply.length);
|
|
83
|
+
|
|
84
|
+
if (this.pendingUpdates[id].length === 0) {
|
|
85
|
+
this.inprogressUpdates[id] = false;
|
|
86
|
+
delete this.inprogressUpdates[id];
|
|
87
|
+
} else {
|
|
88
|
+
// More updates came in, run again.
|
|
89
|
+
return this.applyUpdates<T>(id);
|
|
90
|
+
}
|
|
91
|
+
return updatedDoc as any; // success, exit loop and function
|
|
92
|
+
} catch (e: any) {
|
|
93
|
+
if (e.name === 'conflict' && i < MAX_RETRIES - 1) {
|
|
94
|
+
logger.warn(`Conflict on update for doc ${id}, retry #${i + 1}`);
|
|
95
|
+
await new Promise((res) => setTimeout(res, 50 * Math.random()));
|
|
96
|
+
// continue to next iteration of the loop
|
|
97
|
+
} else {
|
|
98
|
+
// Max retries reached or a non-conflict error
|
|
99
|
+
delete this.inprogressUpdates[id];
|
|
100
|
+
if (this.pendingUpdates[id]) {
|
|
101
|
+
delete this.pendingUpdates[id];
|
|
102
|
+
}
|
|
103
|
+
logger.error(`Error on attemped update (retry ${i}): ${JSON.stringify(e)}`);
|
|
104
|
+
throw e; // Let caller handle
|
|
105
|
+
}
|
|
88
106
|
}
|
|
89
|
-
logger.error(`Error on attemped update: ${JSON.stringify(e)}`);
|
|
90
|
-
throw e; // Let caller handle (e.g., putCardRecord's 404 handling)
|
|
91
107
|
}
|
|
108
|
+
// This should be unreachable, but it satisfies the compiler that a value is always returned or an error thrown.
|
|
109
|
+
throw new Error(`UpdateQueue failed for doc ${id} after ${MAX_RETRIES} retries.`);
|
|
92
110
|
} else {
|
|
93
111
|
throw new Error(`Empty Updates Queue Triggered`);
|
|
94
112
|
}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
CourseDBInterface,
|
|
8
8
|
DataLayerProvider,
|
|
9
9
|
UserDBInterface,
|
|
10
|
+
UserDBReader,
|
|
10
11
|
} from '../../core/interfaces';
|
|
11
12
|
import { logger } from '../../util/logger';
|
|
12
13
|
import { StaticCourseManifest } from '../../util/packer/types';
|
|
@@ -87,6 +88,16 @@ export class StaticDataLayerProvider implements DataLayerProvider {
|
|
|
87
88
|
throw new Error('Admin functions not supported in static mode');
|
|
88
89
|
}
|
|
89
90
|
|
|
91
|
+
async createUserReaderForUser(targetUsername: string): Promise<UserDBReader> {
|
|
92
|
+
logger.warn(`StaticDataLayerProvider: Multi-user access not supported in static mode`);
|
|
93
|
+
logger.warn(`Request: trying to access data for ${targetUsername}`);
|
|
94
|
+
logger.warn(`Returning current user's data instead`);
|
|
95
|
+
|
|
96
|
+
// In static mode, just return the current user's DB as a reader
|
|
97
|
+
// This is safe since static mode is typically for development/testing
|
|
98
|
+
return this.getUserDB() as UserDBReader;
|
|
99
|
+
}
|
|
100
|
+
|
|
90
101
|
isReadOnly(): boolean {
|
|
91
102
|
return true;
|
|
92
103
|
}
|
|
@@ -10,7 +10,13 @@ import {
|
|
|
10
10
|
import { StaticDataUnpacker } from './StaticDataUnpacker';
|
|
11
11
|
import { StaticCourseManifest } from '../../util/packer/types';
|
|
12
12
|
import { CourseConfig, CourseElo, DataShape, Status } from '@vue-skuilder/common';
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
Tag,
|
|
15
|
+
TagStub,
|
|
16
|
+
DocType,
|
|
17
|
+
SkuilderCourseData,
|
|
18
|
+
QualifiedCardID,
|
|
19
|
+
} from '../../core/types/types-legacy';
|
|
14
20
|
import { DataLayerResult } from '../../core/types/db';
|
|
15
21
|
import { ContentNavigationStrategyData } from '../../core/types/contentNavigationStrategy';
|
|
16
22
|
import { ScheduledCard } from '../../core/types/user';
|
|
@@ -91,8 +97,20 @@ export class StaticCourseDB implements CourseDBInterface {
|
|
|
91
97
|
};
|
|
92
98
|
}
|
|
93
99
|
|
|
94
|
-
async getCardsByELO(
|
|
95
|
-
|
|
100
|
+
async getCardsByELO(
|
|
101
|
+
elo: number,
|
|
102
|
+
limit?: number
|
|
103
|
+
): Promise<
|
|
104
|
+
{
|
|
105
|
+
courseID: string;
|
|
106
|
+
cardID: string;
|
|
107
|
+
elo?: number;
|
|
108
|
+
}[]
|
|
109
|
+
> {
|
|
110
|
+
return (await this.unpacker.queryByElo(elo, limit || 25)).map((card) => {
|
|
111
|
+
const [courseID, cardID, elo] = card.split('-');
|
|
112
|
+
return { courseID, cardID, elo: elo ? parseInt(elo) : undefined };
|
|
113
|
+
});
|
|
96
114
|
}
|
|
97
115
|
|
|
98
116
|
async getCardEloData(cardIds: string[]): Promise<CourseElo[]> {
|
|
@@ -129,7 +147,7 @@ export class StaticCourseDB implements CourseDBInterface {
|
|
|
129
147
|
|
|
130
148
|
async getCardsCenteredAtELO(
|
|
131
149
|
options: { limit: number; elo: 'user' | 'random' | number },
|
|
132
|
-
filter?: (id:
|
|
150
|
+
filter?: (id: QualifiedCardID) => boolean
|
|
133
151
|
): Promise<StudySessionNewItem[]> {
|
|
134
152
|
let targetElo = typeof options.elo === 'number' ? options.elo : 1000;
|
|
135
153
|
|
|
@@ -148,16 +166,21 @@ export class StaticCourseDB implements CourseDBInterface {
|
|
|
148
166
|
targetElo = 800 + Math.random() * 400; // Random between 800-1200
|
|
149
167
|
}
|
|
150
168
|
|
|
151
|
-
let cardIds = await this.unpacker.queryByElo(targetElo, options.limit * 2)
|
|
169
|
+
let cardIds = (await this.unpacker.queryByElo(targetElo, options.limit * 2)).map((c) => {
|
|
170
|
+
return {
|
|
171
|
+
cardID: c,
|
|
172
|
+
courseID: this.courseId,
|
|
173
|
+
};
|
|
174
|
+
});
|
|
152
175
|
|
|
153
176
|
if (filter) {
|
|
154
177
|
cardIds = cardIds.filter(filter);
|
|
155
178
|
}
|
|
156
179
|
|
|
157
|
-
return cardIds.slice(0, options.limit).map((
|
|
180
|
+
return cardIds.slice(0, options.limit).map((card) => ({
|
|
158
181
|
status: 'new' as const,
|
|
159
|
-
qualifiedID: `${this.courseId}-${cardId}`,
|
|
160
|
-
cardID:
|
|
182
|
+
// qualifiedID: `${this.courseId}-${cardId}`,
|
|
183
|
+
cardID: card.cardID,
|
|
161
184
|
contentSourceType: 'course' as const,
|
|
162
185
|
contentSourceID: this.courseId,
|
|
163
186
|
courseID: this.courseId,
|
|
@@ -390,4 +413,20 @@ export class StaticCourseDB implements CourseDBInterface {
|
|
|
390
413
|
async getAttachmentBlob(docId: string, attachmentName: string): Promise<Blob | Buffer | null> {
|
|
391
414
|
return this.unpacker.getAttachmentBlob(docId, attachmentName);
|
|
392
415
|
}
|
|
416
|
+
|
|
417
|
+
// Admin search methods
|
|
418
|
+
async searchCards(_query: string): Promise<any[]> {
|
|
419
|
+
// In static mode, return empty results for now
|
|
420
|
+
// Could be implemented with local search if needed
|
|
421
|
+
return [];
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
async find(_request: PouchDB.Find.FindRequest<any>): Promise<PouchDB.Find.FindResponse<any>> {
|
|
425
|
+
// In static mode, return empty results for now
|
|
426
|
+
// Could be implemented with local search if needed
|
|
427
|
+
return {
|
|
428
|
+
docs: [],
|
|
429
|
+
warning: 'Find operations not supported in static mode',
|
|
430
|
+
} as any;
|
|
431
|
+
}
|
|
393
432
|
}
|