@vue-skuilder/db 0.1.11 → 0.1.12
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 +7 -6
- package/dist/core/index.d.ts +7 -6
- package/dist/core/index.js +146 -37
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +146 -37
- package/dist/core/index.mjs.map +1 -1
- package/dist/{dataLayerProvider-VieuAAkV.d.mts → dataLayerProvider-BiP3kWix.d.mts} +1 -1
- package/dist/{dataLayerProvider-juuqUHOP.d.ts → dataLayerProvider-DSdeyRT3.d.ts} +1 -1
- package/dist/impl/couch/index.d.mts +3 -3
- package/dist/impl/couch/index.d.ts +3 -3
- package/dist/impl/couch/index.js +146 -37
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +146 -37
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.d.mts +14 -6
- package/dist/impl/static/index.d.ts +14 -6
- package/dist/impl/static/index.js +147 -39
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +147 -39
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/{index-DZyxHCcf.d.mts → index-Bmll7Xse.d.mts} +1 -1
- package/dist/{index-CWY6yhkV.d.ts → index-CD8BZz2k.d.ts} +1 -1
- package/dist/index.d.mts +119 -24
- package/dist/index.d.ts +119 -24
- package/dist/index.js +785 -261
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +789 -265
- package/dist/index.mjs.map +1 -1
- package/dist/{types-DtoI27Xh.d.ts → types-CewsN87z.d.ts} +1 -1
- package/dist/{types-Che4wTwA.d.mts → types-Dbp5DaRR.d.mts} +1 -1
- package/dist/{types-legacy-B8ahaCbj.d.mts → types-legacy-6ettoclI.d.mts} +13 -2
- package/dist/{types-legacy-B8ahaCbj.d.ts → types-legacy-6ettoclI.d.ts} +13 -2
- package/dist/{userDB-DJ8HMw83.d.mts → userDB-C4yyAnpp.d.mts} +3 -3
- package/dist/{userDB-B7zTQ123.d.ts → userDB-CD6s6ZCp.d.ts} +3 -3
- 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/navigators/hardcodedOrder.ts +64 -0
- package/src/core/navigators/index.ts +1 -0
- package/src/core/types/contentNavigationStrategy.ts +2 -1
- package/src/core/types/types-legacy.ts +2 -2
- package/src/impl/common/BaseUserDB.ts +15 -11
- package/src/impl/couch/courseDB.ts +74 -27
- package/src/impl/couch/updateQueue.ts +8 -3
- package/src/impl/static/StaticDataLayerProvider.ts +57 -17
- package/src/impl/static/courseDB.ts +17 -12
- package/src/impl/static/coursesDB.ts +10 -6
- package/src/study/ItemQueue.ts +58 -0
- package/src/study/SessionController.ts +132 -178
- package/src/study/services/CardHydrationService.ts +153 -0
- package/src/study/services/EloService.ts +85 -0
- package/src/study/services/ResponseProcessor.ts +224 -0
- package/src/study/services/SrsService.ts +44 -0
|
@@ -78,7 +78,18 @@ interface QuestionData extends SkuilderCourseData {
|
|
|
78
78
|
viewList: string[];
|
|
79
79
|
dataShapeList: PouchDB.Core.DocumentId[];
|
|
80
80
|
}
|
|
81
|
-
declare const DocTypePrefixes:
|
|
81
|
+
declare const DocTypePrefixes: {
|
|
82
|
+
readonly CARD: "c";
|
|
83
|
+
readonly DISPLAYABLE_DATA: "dd";
|
|
84
|
+
readonly TAG: "TAG";
|
|
85
|
+
readonly CARDRECORD: "cardH";
|
|
86
|
+
readonly SCHEDULED_CARD: "card_review_";
|
|
87
|
+
readonly DATASHAPE: "DATASHAPE";
|
|
88
|
+
readonly QUESTION: "QUESTION";
|
|
89
|
+
readonly VIEW: "VIEW";
|
|
90
|
+
readonly PEDAGOGY: "PEDAGOGY";
|
|
91
|
+
readonly NAVIGATION_STRATEGY: "NAVIGATION_STRATEGY";
|
|
92
|
+
};
|
|
82
93
|
interface CardHistory<T extends CardRecord> {
|
|
83
94
|
_id: PouchDB.Core.DocumentId;
|
|
84
95
|
/**
|
|
@@ -140,4 +151,4 @@ interface QuestionRecord extends CardRecord, Evaluation {
|
|
|
140
151
|
priorAttemps: number;
|
|
141
152
|
}
|
|
142
153
|
|
|
143
|
-
export { type
|
|
154
|
+
export { type CardHistory as C, DocType as D, type Field as F, GuestUsername as G, type QualifiedCardID as Q, type SkuilderCourseData as S, type TagStub as T, type Tag as a, DocTypePrefixes as b, type CardRecord as c, type CardData as d, type CourseListData as e, type DisplayableData as f, type DataShapeData as g, type QuestionData as h, type QuestionRecord as i, log as l };
|
|
@@ -78,7 +78,18 @@ interface QuestionData extends SkuilderCourseData {
|
|
|
78
78
|
viewList: string[];
|
|
79
79
|
dataShapeList: PouchDB.Core.DocumentId[];
|
|
80
80
|
}
|
|
81
|
-
declare const DocTypePrefixes:
|
|
81
|
+
declare const DocTypePrefixes: {
|
|
82
|
+
readonly CARD: "c";
|
|
83
|
+
readonly DISPLAYABLE_DATA: "dd";
|
|
84
|
+
readonly TAG: "TAG";
|
|
85
|
+
readonly CARDRECORD: "cardH";
|
|
86
|
+
readonly SCHEDULED_CARD: "card_review_";
|
|
87
|
+
readonly DATASHAPE: "DATASHAPE";
|
|
88
|
+
readonly QUESTION: "QUESTION";
|
|
89
|
+
readonly VIEW: "VIEW";
|
|
90
|
+
readonly PEDAGOGY: "PEDAGOGY";
|
|
91
|
+
readonly NAVIGATION_STRATEGY: "NAVIGATION_STRATEGY";
|
|
92
|
+
};
|
|
82
93
|
interface CardHistory<T extends CardRecord> {
|
|
83
94
|
_id: PouchDB.Core.DocumentId;
|
|
84
95
|
/**
|
|
@@ -140,4 +151,4 @@ interface QuestionRecord extends CardRecord, Evaluation {
|
|
|
140
151
|
priorAttemps: number;
|
|
141
152
|
}
|
|
142
153
|
|
|
143
|
-
export { type
|
|
154
|
+
export { type CardHistory as C, DocType as D, type Field as F, GuestUsername as G, type QualifiedCardID as Q, type SkuilderCourseData as S, type TagStub as T, type Tag as a, DocTypePrefixes as b, type CardRecord as c, type CardData as d, type CourseListData as e, type DisplayableData as f, type DataShapeData as g, type QuestionData as h, type QuestionRecord as i, log as l };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CourseConfig, ClassroomConfig, CourseElo, Status, SkuilderCourseData as SkuilderCourseData$1, DataShape } from '@vue-skuilder/common';
|
|
2
2
|
import { Moment } from 'moment';
|
|
3
|
-
import { S as SkuilderCourseData, D as DocType, Q as QualifiedCardID, T as TagStub, a as Tag,
|
|
3
|
+
import { S as SkuilderCourseData, b as DocTypePrefixes, D as DocType, Q as QualifiedCardID, T as TagStub, a as Tag, C as CardHistory, c as CardRecord } from './types-legacy-6ettoclI.mjs';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Admin functionality
|
|
@@ -199,7 +199,7 @@ interface DataLayerResult {
|
|
|
199
199
|
*
|
|
200
200
|
*/
|
|
201
201
|
interface ContentNavigationStrategyData extends SkuilderCourseData {
|
|
202
|
-
|
|
202
|
+
_id: `${typeof DocTypePrefixes[DocType.NAVIGATION_STRATEGY]}-${string}`;
|
|
203
203
|
docType: DocType.NAVIGATION_STRATEGY;
|
|
204
204
|
name: string;
|
|
205
205
|
description: string;
|
|
@@ -530,4 +530,4 @@ interface ClassroomRegistrationDoc {
|
|
|
530
530
|
registrations: ClassroomRegistration[];
|
|
531
531
|
}
|
|
532
532
|
|
|
533
|
-
export { type AdminDBInterface as A, type
|
|
533
|
+
export { type AdminDBInterface as A, type ClassroomRegistrationDesignation as B, type CourseDBInterface as C, type DataLayerResult as D, type ClassroomRegistration as E, type ClassroomRegistrationDoc as F, type UserConfig as G, type ActivityRecord as H, type CourseRegistration as I, type DocumentUpdater as J, newInterval as K, type StudySessionNewItem as S, type TeacherClassroomDBInterface as T, type UserDBInterface as U, type UserDBReader as a, type CoursesDBInterface as b, type ClassroomDBInterface as c, type CourseInfo as d, type ContentNavigationStrategyData as e, type StudySessionReviewItem as f, type ScheduledCard as g, type AssignedContent as h, type StudyContentSource as i, type StudentClassroomDBInterface as j, type StudySessionItem as k, type StudySessionFailedItem as l, type StudySessionFailedNewItem as m, type StudySessionFailedReviewItem as n, isReview as o, type ContentSourceID as p, getStudySource as q, type CourseRegistrationDoc as r, type AssignedTag as s, type AssignedCourse as t, type AssignedCard as u, type UserDBWriter as v, type UserDBAuthenticator as w, type UserCourseSettings as x, type UserCourseSetting as y, type UsrCrsDataInterface as z };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CourseConfig, ClassroomConfig, CourseElo, Status, SkuilderCourseData as SkuilderCourseData$1, DataShape } from '@vue-skuilder/common';
|
|
2
2
|
import { Moment } from 'moment';
|
|
3
|
-
import { S as SkuilderCourseData, D as DocType, Q as QualifiedCardID, T as TagStub, a as Tag,
|
|
3
|
+
import { S as SkuilderCourseData, b as DocTypePrefixes, D as DocType, Q as QualifiedCardID, T as TagStub, a as Tag, C as CardHistory, c as CardRecord } from './types-legacy-6ettoclI.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Admin functionality
|
|
@@ -199,7 +199,7 @@ interface DataLayerResult {
|
|
|
199
199
|
*
|
|
200
200
|
*/
|
|
201
201
|
interface ContentNavigationStrategyData extends SkuilderCourseData {
|
|
202
|
-
|
|
202
|
+
_id: `${typeof DocTypePrefixes[DocType.NAVIGATION_STRATEGY]}-${string}`;
|
|
203
203
|
docType: DocType.NAVIGATION_STRATEGY;
|
|
204
204
|
name: string;
|
|
205
205
|
description: string;
|
|
@@ -530,4 +530,4 @@ interface ClassroomRegistrationDoc {
|
|
|
530
530
|
registrations: ClassroomRegistration[];
|
|
531
531
|
}
|
|
532
532
|
|
|
533
|
-
export { type AdminDBInterface as A, type
|
|
533
|
+
export { type AdminDBInterface as A, type ClassroomRegistrationDesignation as B, type CourseDBInterface as C, type DataLayerResult as D, type ClassroomRegistration as E, type ClassroomRegistrationDoc as F, type UserConfig as G, type ActivityRecord as H, type CourseRegistration as I, type DocumentUpdater as J, newInterval as K, type StudySessionNewItem as S, type TeacherClassroomDBInterface as T, type UserDBInterface as U, type UserDBReader as a, type CoursesDBInterface as b, type ClassroomDBInterface as c, type CourseInfo as d, type ContentNavigationStrategyData as e, type StudySessionReviewItem as f, type ScheduledCard as g, type AssignedContent as h, type StudyContentSource as i, type StudentClassroomDBInterface as j, type StudySessionItem as k, type StudySessionFailedItem as l, type StudySessionFailedNewItem as m, type StudySessionFailedReviewItem as n, isReview as o, type ContentSourceID as p, getStudySource as q, type CourseRegistrationDoc as r, type AssignedTag as s, type AssignedCourse as t, type AssignedCard as u, type UserDBWriter as v, type UserDBAuthenticator as w, type UserCourseSettings as x, type UserCourseSetting as y, type UsrCrsDataInterface as z };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { A as AttachmentData, C as ChunkMetadata, D as DesignDocument, I as IndexMetadata, a as PackedCourseData, P as PackerConfig, S as StaticCourseManifest } from '../../types-
|
|
2
|
-
export { C as CouchDBToStaticPacker } from '../../index-
|
|
1
|
+
export { A as AttachmentData, C as ChunkMetadata, D as DesignDocument, I as IndexMetadata, a as PackedCourseData, P as PackerConfig, S as StaticCourseManifest } from '../../types-Dbp5DaRR.mjs';
|
|
2
|
+
export { C as CouchDBToStaticPacker } from '../../index-Bmll7Xse.mjs';
|
|
3
3
|
import '@vue-skuilder/common';
|
|
4
|
-
import '../../types-legacy-
|
|
4
|
+
import '../../types-legacy-6ettoclI.mjs';
|
|
5
5
|
import 'moment';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { A as AttachmentData, C as ChunkMetadata, D as DesignDocument, I as IndexMetadata, a as PackedCourseData, P as PackerConfig, S as StaticCourseManifest } from '../../types-
|
|
2
|
-
export { C as CouchDBToStaticPacker } from '../../index-
|
|
1
|
+
export { A as AttachmentData, C as ChunkMetadata, D as DesignDocument, I as IndexMetadata, a as PackedCourseData, P as PackerConfig, S as StaticCourseManifest } from '../../types-CewsN87z.js';
|
|
2
|
+
export { C as CouchDBToStaticPacker } from '../../index-CD8BZz2k.js';
|
|
3
3
|
import '@vue-skuilder/common';
|
|
4
|
-
import '../../types-legacy-
|
|
4
|
+
import '../../types-legacy-6ettoclI.js';
|
|
5
5
|
import 'moment';
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.1.
|
|
6
|
+
"version": "0.1.12",
|
|
7
7
|
"description": "Database layer for vue-skuilder",
|
|
8
8
|
"main": "dist/index.js",
|
|
9
9
|
"module": "dist/index.mjs",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@nilock2/pouchdb-authentication": "^1.0.2",
|
|
48
|
-
"@vue-skuilder/common": "0.1.
|
|
48
|
+
"@vue-skuilder/common": "0.1.12",
|
|
49
49
|
"cross-fetch": "^4.1.0",
|
|
50
50
|
"moment": "^2.29.4",
|
|
51
51
|
"pouchdb": "^9.0.0",
|
|
@@ -57,5 +57,5 @@
|
|
|
57
57
|
"tsup": "^8.0.2",
|
|
58
58
|
"typescript": "~5.7.2"
|
|
59
59
|
},
|
|
60
|
-
"stableVersion": "0.1.
|
|
60
|
+
"stableVersion": "0.1.12"
|
|
61
61
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { CourseDBInterface, QualifiedCardID, StudySessionNewItem, StudySessionReviewItem, UserDBInterface } from '..';
|
|
2
|
+
import { ContentNavigationStrategyData } from '../types/contentNavigationStrategy';
|
|
3
|
+
import { ScheduledCard } from '../types/user';
|
|
4
|
+
import { ContentNavigator } from './index';
|
|
5
|
+
import { logger } from '../../util/logger';
|
|
6
|
+
|
|
7
|
+
export default class HardcodedOrderNavigator extends ContentNavigator {
|
|
8
|
+
private orderedCardIds: string[] = [];
|
|
9
|
+
private user: UserDBInterface;
|
|
10
|
+
private course: CourseDBInterface;
|
|
11
|
+
|
|
12
|
+
constructor(
|
|
13
|
+
user: UserDBInterface,
|
|
14
|
+
course: CourseDBInterface,
|
|
15
|
+
strategyData: ContentNavigationStrategyData
|
|
16
|
+
) {
|
|
17
|
+
super();
|
|
18
|
+
this.user = user;
|
|
19
|
+
this.course = course;
|
|
20
|
+
|
|
21
|
+
if (strategyData.serializedData) {
|
|
22
|
+
try {
|
|
23
|
+
this.orderedCardIds = JSON.parse(strategyData.serializedData);
|
|
24
|
+
} catch (e) {
|
|
25
|
+
logger.error('Failed to parse serializedData for HardcodedOrderNavigator', e);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async getPendingReviews(): Promise<(StudySessionReviewItem & ScheduledCard)[]> {
|
|
31
|
+
const reviews = await this.user.getPendingReviews(this.course.getCourseID());
|
|
32
|
+
return reviews.map((r) => {
|
|
33
|
+
return {
|
|
34
|
+
...r,
|
|
35
|
+
contentSourceType: 'course',
|
|
36
|
+
contentSourceID: this.course.getCourseID(),
|
|
37
|
+
cardID: r.cardId,
|
|
38
|
+
courseID: r.courseId,
|
|
39
|
+
reviewID: r._id,
|
|
40
|
+
status: 'review',
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async getNewCards(limit: number = 99): Promise<StudySessionNewItem[]> {
|
|
46
|
+
const activeCardIds = (await this.user.getActiveCards()).map((c: QualifiedCardID) => c.cardID);
|
|
47
|
+
|
|
48
|
+
const newCardIds = this.orderedCardIds.filter(
|
|
49
|
+
(cardId) => !activeCardIds.includes(cardId)
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const cardsToReturn = newCardIds.slice(0, limit);
|
|
53
|
+
|
|
54
|
+
return cardsToReturn.map((cardId) => {
|
|
55
|
+
return {
|
|
56
|
+
cardID: cardId,
|
|
57
|
+
courseID: this.course.getCourseID(),
|
|
58
|
+
contentSourceType: 'course',
|
|
59
|
+
contentSourceID: this.course.getCourseID(),
|
|
60
|
+
status: 'new',
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { DocType, SkuilderCourseData } from './types-legacy';
|
|
2
|
+
import type { DocTypePrefixes } from './types-legacy';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
*
|
|
5
6
|
*/
|
|
6
7
|
export interface ContentNavigationStrategyData extends SkuilderCourseData {
|
|
7
|
-
|
|
8
|
+
_id: `${typeof DocTypePrefixes[DocType.NAVIGATION_STRATEGY]}-${string}`;
|
|
8
9
|
docType: DocType.NAVIGATION_STRATEGY;
|
|
9
10
|
name: string;
|
|
10
11
|
description: string;
|
|
@@ -91,7 +91,7 @@ export interface QuestionData extends SkuilderCourseData {
|
|
|
91
91
|
dataShapeList: PouchDB.Core.DocumentId[];
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
export const DocTypePrefixes
|
|
94
|
+
export const DocTypePrefixes = {
|
|
95
95
|
[DocType.CARD]: 'c',
|
|
96
96
|
[DocType.DISPLAYABLE_DATA]: 'dd',
|
|
97
97
|
[DocType.TAG]: 'TAG',
|
|
@@ -103,7 +103,7 @@ export const DocTypePrefixes: Record<string, string> = {
|
|
|
103
103
|
[DocType.VIEW]: 'VIEW',
|
|
104
104
|
[DocType.PEDAGOGY]: 'PEDAGOGY',
|
|
105
105
|
[DocType.NAVIGATION_STRATEGY]: 'NAVIGATION_STRATEGY',
|
|
106
|
-
};
|
|
106
|
+
} as const;
|
|
107
107
|
|
|
108
108
|
export interface CardHistory<T extends CardRecord> {
|
|
109
109
|
_id: PouchDB.Core.DocumentId;
|
|
@@ -761,17 +761,21 @@ Currently logged-in as ${this._username}.`
|
|
|
761
761
|
} catch (e) {
|
|
762
762
|
const reason = e as PouchError;
|
|
763
763
|
if (reason.status === 404) {
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
764
|
+
try {
|
|
765
|
+
const initCardHistory: CardHistory<T> = {
|
|
766
|
+
_id: cardHistoryID,
|
|
767
|
+
cardID: record.cardID,
|
|
768
|
+
courseID: record.courseID,
|
|
769
|
+
records: [record],
|
|
770
|
+
lapses: 0,
|
|
771
|
+
streak: 0,
|
|
772
|
+
bestInterval: 0,
|
|
773
|
+
};
|
|
774
|
+
const putResult = await this.writeDB.put<CardHistory<T>>(initCardHistory);
|
|
775
|
+
return { ...initCardHistory, _rev: putResult.rev };
|
|
776
|
+
} catch (creationError) {
|
|
777
|
+
throw new Error(`Failed to create CardHistory for ${cardHistoryID}. Reason: ${creationError}`);
|
|
778
|
+
}
|
|
775
779
|
} else {
|
|
776
780
|
throw new Error(`putCardRecord failed because of:
|
|
777
781
|
name:${reason.name}
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
SkuilderCourseData,
|
|
26
26
|
Tag,
|
|
27
27
|
TagStub,
|
|
28
|
+
DocTypePrefixes,
|
|
28
29
|
} from '../../core/types/types-legacy';
|
|
29
30
|
import { logger } from '../../util/logger';
|
|
30
31
|
import { GET_CACHED } from './clientCache';
|
|
@@ -224,7 +225,29 @@ export class CourseDB implements StudyContentSource, CourseDBInterface {
|
|
|
224
225
|
if (!doc.docType || !(doc.docType === DocType.CARD)) {
|
|
225
226
|
throw new Error(`failed to remove ${id} from course ${this.id}. id does not point to a card`);
|
|
226
227
|
}
|
|
227
|
-
|
|
228
|
+
|
|
229
|
+
// Remove card from all associated tags before deleting the card
|
|
230
|
+
try {
|
|
231
|
+
const appliedTags = await this.getAppliedTags(id);
|
|
232
|
+
const results = await Promise.allSettled(
|
|
233
|
+
appliedTags.rows.map(async (tagRow) => {
|
|
234
|
+
const tagId = tagRow.id;
|
|
235
|
+
await this.removeTagFromCard(id, tagId);
|
|
236
|
+
})
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
// Log any individual tag cleanup failures
|
|
240
|
+
results.forEach((result, index) => {
|
|
241
|
+
if (result.status === 'rejected') {
|
|
242
|
+
const tagId = appliedTags.rows[index].id;
|
|
243
|
+
logger.error(`Failed to remove card ${id} from tag ${tagId}: ${result.reason}`);
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
} catch (error) {
|
|
247
|
+
logger.error(`Error removing card ${id} from tags: ${error}`);
|
|
248
|
+
// Continue with card deletion even if tag cleanup fails
|
|
249
|
+
}
|
|
250
|
+
|
|
228
251
|
return this.db.remove(doc);
|
|
229
252
|
}
|
|
230
253
|
|
|
@@ -456,40 +479,38 @@ above:\n${above.rows.map((r) => `\t${r.id}-${r.key}\n`)}`;
|
|
|
456
479
|
|
|
457
480
|
getNavigationStrategy(id: string): Promise<ContentNavigationStrategyData> {
|
|
458
481
|
logger.debug(`[courseDB] Getting navigation strategy: ${id}`);
|
|
459
|
-
// For now, just return the ELO strategy regardless of the ID
|
|
460
|
-
const strategy: ContentNavigationStrategyData = {
|
|
461
|
-
id: 'ELO',
|
|
462
|
-
docType: DocType.NAVIGATION_STRATEGY,
|
|
463
|
-
name: 'ELO',
|
|
464
|
-
description: 'ELO-based navigation strategy for ordering content by difficulty',
|
|
465
|
-
implementingClass: Navigators.ELO,
|
|
466
|
-
course: this.id,
|
|
467
|
-
serializedData: '', // serde is a noop for ELO navigator.
|
|
468
|
-
};
|
|
469
|
-
return Promise.resolve(strategy);
|
|
470
|
-
}
|
|
471
482
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
{
|
|
476
|
-
id: 'ELO',
|
|
483
|
+
if (id == '') {
|
|
484
|
+
const strategy: ContentNavigationStrategyData = {
|
|
485
|
+
_id: 'NAVIGATION_STRATEGY-ELO',
|
|
477
486
|
docType: DocType.NAVIGATION_STRATEGY,
|
|
478
487
|
name: 'ELO',
|
|
479
488
|
description: 'ELO-based navigation strategy for ordering content by difficulty',
|
|
480
489
|
implementingClass: Navigators.ELO,
|
|
481
490
|
course: this.id,
|
|
482
491
|
serializedData: '', // serde is a noop for ELO navigator.
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
|
|
492
|
+
};
|
|
493
|
+
return Promise.resolve(strategy);
|
|
494
|
+
} else {
|
|
495
|
+
return this.db.get(id);
|
|
496
|
+
}
|
|
486
497
|
}
|
|
487
498
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
499
|
+
async getAllNavigationStrategies(): Promise<ContentNavigationStrategyData[]> {
|
|
500
|
+
const prefix = DocTypePrefixes[DocType.NAVIGATION_STRATEGY];
|
|
501
|
+
const result = await this.db.allDocs<ContentNavigationStrategyData>({
|
|
502
|
+
startkey: prefix,
|
|
503
|
+
endkey: `${prefix}\ufff0`,
|
|
504
|
+
include_docs: true,
|
|
505
|
+
});
|
|
506
|
+
return result.rows.map((row) => row.doc!);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
async addNavigationStrategy(data: ContentNavigationStrategyData): Promise<void> {
|
|
510
|
+
logger.debug(`[courseDB] Adding navigation strategy: ${data._id}`);
|
|
511
|
+
// // For now, just log the data and return success
|
|
512
|
+
// logger.debug(JSON.stringify(data));
|
|
513
|
+
return this.db.put(data).then(() => {});
|
|
493
514
|
}
|
|
494
515
|
updateNavigationStrategy(id: string, data: ContentNavigationStrategyData): Promise<void> {
|
|
495
516
|
logger.debug(`[courseDB] Updating navigation strategy: ${id}`);
|
|
@@ -499,9 +520,35 @@ above:\n${above.rows.map((r) => `\t${r.id}-${r.key}\n`)}`;
|
|
|
499
520
|
}
|
|
500
521
|
|
|
501
522
|
async surfaceNavigationStrategy(): Promise<ContentNavigationStrategyData> {
|
|
523
|
+
try {
|
|
524
|
+
const config = await this.getCourseConfig();
|
|
525
|
+
// @ts-expect-error tmp: defaultNavigationStrategyId property does not yet exist
|
|
526
|
+
if (config.defaultNavigationStrategyId) {
|
|
527
|
+
try {
|
|
528
|
+
// @ts-expect-error tmp: defaultNavigationStrategyId property does not yet exist
|
|
529
|
+
const strategy = await this.getNavigationStrategy(config.defaultNavigationStrategyId);
|
|
530
|
+
if (strategy) {
|
|
531
|
+
logger.debug(`Surfacing strategy ${strategy.name} from course config`);
|
|
532
|
+
return strategy;
|
|
533
|
+
}
|
|
534
|
+
} catch (e) {
|
|
535
|
+
logger.warn(
|
|
536
|
+
// @ts-expect-error tmp: defaultNavigationStrategyId property does not yet exist
|
|
537
|
+
`Failed to load strategy '${config.defaultNavigationStrategyId}' specified in course config. Falling back to ELO.`,
|
|
538
|
+
e
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
} catch (e) {
|
|
543
|
+
logger.warn(
|
|
544
|
+
'Could not retrieve course config to determine navigation strategy. Falling back to ELO.',
|
|
545
|
+
e
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
|
|
502
549
|
logger.warn(`Returning hard-coded default ELO navigator`);
|
|
503
550
|
const ret: ContentNavigationStrategyData = {
|
|
504
|
-
|
|
551
|
+
_id: 'NAVIGATION_STRATEGY-ELO',
|
|
505
552
|
docType: DocType.NAVIGATION_STRATEGY,
|
|
506
553
|
name: 'ELO',
|
|
507
554
|
description: 'ELO-based navigation strategy',
|
|
@@ -44,9 +44,10 @@ export default class UpdateQueue extends Loggable {
|
|
|
44
44
|
): Promise<T & PouchDB.Core.GetMeta & PouchDB.Core.RevisionIdMeta> {
|
|
45
45
|
logger.debug(`Applying updates on doc: ${id}`);
|
|
46
46
|
if (this.inprogressUpdates[id]) {
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
// Poll instead of recursing to avoid infinite recursion
|
|
48
|
+
while (this.inprogressUpdates[id]) {
|
|
49
|
+
await new Promise(resolve => setTimeout(resolve, Math.random() * 50));
|
|
50
|
+
}
|
|
50
51
|
return this.applyUpdates<T>(id);
|
|
51
52
|
} else {
|
|
52
53
|
if (this.pendingUpdates[id] && this.pendingUpdates[id].length > 0) {
|
|
@@ -94,6 +95,10 @@ export default class UpdateQueue extends Loggable {
|
|
|
94
95
|
logger.warn(`Conflict on update for doc ${id}, retry #${i + 1}`);
|
|
95
96
|
await new Promise((res) => setTimeout(res, 50 * Math.random()));
|
|
96
97
|
// continue to next iteration of the loop
|
|
98
|
+
} else if (e.name === 'not_found' && i === 0) {
|
|
99
|
+
// Document not present - throw to caller for initialization
|
|
100
|
+
logger.warn(`Update failed for ${id} - does not exist. Throwing to caller.`);
|
|
101
|
+
throw e; // Let caller handle
|
|
97
102
|
} else {
|
|
98
103
|
// Max retries reached or a non-conflict error
|
|
99
104
|
delete this.inprogressUpdates[id];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
|
|
1
2
|
// packages/db/src/impl/static/StaticDataLayerProvider.ts
|
|
2
3
|
|
|
3
4
|
import {
|
|
@@ -17,39 +18,78 @@ import { StaticCoursesDB } from './coursesDB';
|
|
|
17
18
|
import { BaseUser } from '../common';
|
|
18
19
|
import { NoOpSyncStrategy } from './NoOpSyncStrategy';
|
|
19
20
|
|
|
21
|
+
|
|
22
|
+
interface SkuilderManifest {
|
|
23
|
+
name?: string;
|
|
24
|
+
version?: string;
|
|
25
|
+
description?: string;
|
|
26
|
+
dependencies?: Record<string, string>;
|
|
27
|
+
}
|
|
28
|
+
|
|
20
29
|
interface StaticDataLayerConfig {
|
|
21
|
-
staticContentPath: string;
|
|
22
30
|
localStoragePrefix?: string;
|
|
23
|
-
|
|
31
|
+
rootManifest: SkuilderManifest; // The parsed root skuilder.json object
|
|
32
|
+
rootManifestUrl: string; // The absolute URL where the root manifest was found
|
|
24
33
|
}
|
|
25
34
|
|
|
26
35
|
export class StaticDataLayerProvider implements DataLayerProvider {
|
|
27
36
|
private config: StaticDataLayerConfig;
|
|
28
37
|
private initialized: boolean = false;
|
|
29
38
|
private courseUnpackers: Map<string, StaticDataUnpacker> = new Map();
|
|
39
|
+
private manifests: Record<string, StaticCourseManifest> = {};
|
|
30
40
|
|
|
31
41
|
constructor(config: Partial<StaticDataLayerConfig>) {
|
|
32
42
|
this.config = {
|
|
33
|
-
staticContentPath: config.staticContentPath || '/static-courses',
|
|
34
43
|
localStoragePrefix: config.localStoragePrefix || 'skuilder-static',
|
|
35
|
-
|
|
44
|
+
rootManifest: config.rootManifest || { dependencies: {} },
|
|
45
|
+
rootManifestUrl: config.rootManifestUrl || '/',
|
|
36
46
|
};
|
|
37
47
|
}
|
|
38
48
|
|
|
49
|
+
private async resolveCourseDependencies(): Promise<void> {
|
|
50
|
+
logger.info('[StaticDataLayerProvider] Starting course dependency resolution...');
|
|
51
|
+
const rootManifest = this.config.rootManifest;
|
|
52
|
+
|
|
53
|
+
for (const [courseName, courseUrl] of Object.entries(rootManifest.dependencies || {})) {
|
|
54
|
+
try {
|
|
55
|
+
logger.debug(`[StaticDataLayerProvider] Resolving dependency: ${courseName} from ${courseUrl}`);
|
|
56
|
+
|
|
57
|
+
const courseManifestUrl = new URL(courseUrl as string, this.config.rootManifestUrl).href;
|
|
58
|
+
const courseJsonResponse = await fetch(courseManifestUrl);
|
|
59
|
+
if (!courseJsonResponse.ok) {
|
|
60
|
+
throw new Error(`Failed to fetch course manifest for ${courseName}`);
|
|
61
|
+
}
|
|
62
|
+
const courseJson = await courseJsonResponse.json();
|
|
63
|
+
|
|
64
|
+
if (courseJson.content && courseJson.content.manifest) {
|
|
65
|
+
const baseUrl = new URL('.', courseManifestUrl).href;
|
|
66
|
+
const finalManifestUrl = new URL(courseJson.content.manifest, courseManifestUrl).href;
|
|
67
|
+
|
|
68
|
+
const finalManifestResponse = await fetch(finalManifestUrl);
|
|
69
|
+
if (!finalManifestResponse.ok) {
|
|
70
|
+
throw new Error(`Failed to fetch final content manifest for ${courseName} at ${finalManifestUrl}`);
|
|
71
|
+
}
|
|
72
|
+
const finalManifest = await finalManifestResponse.json();
|
|
73
|
+
|
|
74
|
+
this.manifests[courseName] = finalManifest;
|
|
75
|
+
const unpacker = new StaticDataUnpacker(finalManifest, baseUrl);
|
|
76
|
+
this.courseUnpackers.set(courseName, unpacker);
|
|
77
|
+
|
|
78
|
+
logger.info(`[StaticDataLayerProvider] Successfully resolved and prepared course: ${courseName}`);
|
|
79
|
+
}
|
|
80
|
+
} catch (e) {
|
|
81
|
+
logger.error(`[StaticDataLayerProvider] Failed to resolve dependency ${courseName}:`, e);
|
|
82
|
+
// Continue to next dependency
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
logger.info('[StaticDataLayerProvider] Course dependency resolution complete.');
|
|
86
|
+
}
|
|
87
|
+
|
|
39
88
|
async initialize(): Promise<void> {
|
|
40
89
|
if (this.initialized) return;
|
|
41
90
|
|
|
42
91
|
logger.info('Initializing static data layer provider');
|
|
43
|
-
|
|
44
|
-
// Load manifests for all courses
|
|
45
|
-
for (const [courseId, manifest] of Object.entries(this.config.manifests)) {
|
|
46
|
-
const unpacker = new StaticDataUnpacker(
|
|
47
|
-
manifest,
|
|
48
|
-
`${this.config.staticContentPath}/${courseId}`
|
|
49
|
-
);
|
|
50
|
-
this.courseUnpackers.set(courseId, unpacker);
|
|
51
|
-
}
|
|
52
|
-
|
|
92
|
+
await this.resolveCourseDependencies();
|
|
53
93
|
this.initialized = true;
|
|
54
94
|
}
|
|
55
95
|
|
|
@@ -67,14 +107,14 @@ export class StaticDataLayerProvider implements DataLayerProvider {
|
|
|
67
107
|
getCourseDB(courseId: string): CourseDBInterface {
|
|
68
108
|
const unpacker = this.courseUnpackers.get(courseId);
|
|
69
109
|
if (!unpacker) {
|
|
70
|
-
throw new Error(`Course ${courseId} not found in static data
|
|
110
|
+
throw new Error(`Course ${courseId} not found or failed to initialize in static data layer.`);
|
|
71
111
|
}
|
|
72
|
-
const manifest = this.
|
|
112
|
+
const manifest = this.manifests[courseId];
|
|
73
113
|
return new StaticCourseDB(courseId, unpacker, this.getUserDB(), manifest);
|
|
74
114
|
}
|
|
75
115
|
|
|
76
116
|
getCoursesDB(): CoursesDBInterface {
|
|
77
|
-
return new StaticCoursesDB(this.
|
|
117
|
+
return new StaticCoursesDB(this.manifests);
|
|
78
118
|
}
|
|
79
119
|
|
|
80
120
|
async getClassroomDB(
|
|
@@ -132,17 +132,22 @@ export class StaticCourseDB implements CourseDBInterface {
|
|
|
132
132
|
return { ok: true, id: cardId, rev: '1-static' };
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
async getNewCards(limit
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
135
|
+
async getNewCards(limit: number = 99): Promise<StudySessionNewItem[]> {
|
|
136
|
+
const activeCards = await this.userDB.getActiveCards();
|
|
137
|
+
return (
|
|
138
|
+
await this.getCardsCenteredAtELO({ limit: limit, elo: 'user' }, (c: QualifiedCardID) => {
|
|
139
|
+
if (activeCards.some((ac) => c.cardID === ac.cardID)) {
|
|
140
|
+
return false;
|
|
141
|
+
} else {
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
})
|
|
145
|
+
).map((c) => {
|
|
146
|
+
return {
|
|
147
|
+
...c,
|
|
148
|
+
status: 'new',
|
|
149
|
+
};
|
|
150
|
+
});
|
|
146
151
|
}
|
|
147
152
|
|
|
148
153
|
async getCardsCenteredAtELO(
|
|
@@ -364,7 +369,7 @@ export class StaticCourseDB implements CourseDBInterface {
|
|
|
364
369
|
// Navigation Strategy Manager implementation
|
|
365
370
|
async getNavigationStrategy(_id: string): Promise<ContentNavigationStrategyData> {
|
|
366
371
|
return {
|
|
367
|
-
|
|
372
|
+
_id: 'NAVIGATION_STRATEGY-ELO',
|
|
368
373
|
docType: DocType.NAVIGATION_STRATEGY,
|
|
369
374
|
name: 'ELO',
|
|
370
375
|
description: 'ELO-based navigation strategy',
|