@vue-skuilder/db 0.1.14-2 → 0.1.15
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.js +40 -4
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +40 -4
- package/dist/core/index.mjs.map +1 -1
- package/dist/impl/couch/index.js +40 -4
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +40 -4
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.d.mts +3 -1
- package/dist/impl/static/index.d.ts +3 -1
- package/dist/impl/static/index.js +71 -13
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +71 -13
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/index.d.mts +38 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.js +149 -36
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +149 -36
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/impl/common/BaseUserDB.ts +12 -2
- package/src/impl/couch/updateQueue.ts +28 -2
- package/src/impl/static/StaticDataLayerProvider.ts +32 -8
- package/src/impl/static/coursesDB.ts +15 -2
- package/src/study/SessionController.ts +50 -0
- package/src/study/services/CardHydrationService.ts +7 -0
- package/src/study/services/ResponseProcessor.ts +64 -25
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.15",
|
|
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.15",
|
|
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.15"
|
|
61
61
|
}
|
|
@@ -722,12 +722,22 @@ Currently logged-in as ${this._username}.`
|
|
|
722
722
|
|
|
723
723
|
/**
|
|
724
724
|
* Logs a record of the user's interaction with the card and returns the card's
|
|
725
|
-
* up-to-date history
|
|
725
|
+
* up-to-date history.
|
|
726
|
+
*
|
|
727
|
+
* **Automatic Initialization:**
|
|
728
|
+
* If this is the user's first interaction with the card (CardHistory doesn't exist),
|
|
729
|
+
* this method automatically creates the CardHistory document with initial values
|
|
730
|
+
* (lapses: 0, streak: 0, bestInterval: 0).
|
|
731
|
+
*
|
|
732
|
+
* **Error Handling:**
|
|
733
|
+
* - Handles 404 errors by creating initial CardHistory document
|
|
734
|
+
* - Re-throws all other errors from UpdateQueue
|
|
726
735
|
*
|
|
727
736
|
* // [ ] #db-refactor extract to a smaller scope - eg, UserStudySession
|
|
728
737
|
*
|
|
729
|
-
* @param record
|
|
738
|
+
* @param record - The recent recorded interaction between user and card
|
|
730
739
|
* @returns The updated state of the card's CardHistory data
|
|
740
|
+
* @throws Error if document creation fails or non-404 database error occurs
|
|
731
741
|
*/
|
|
732
742
|
|
|
733
743
|
public async putCardRecord<T extends CardRecord>(
|
|
@@ -15,6 +15,33 @@ export default class UpdateQueue extends Loggable {
|
|
|
15
15
|
private readDB: PouchDB.Database; // Database for read operations
|
|
16
16
|
private writeDB: PouchDB.Database; // Database for write operations (local-first)
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Queues an update for a document and applies it with conflict resolution.
|
|
20
|
+
*
|
|
21
|
+
* @param id - Document ID to update
|
|
22
|
+
* @param update - Partial object or function that transforms the document
|
|
23
|
+
* @returns Promise resolving to the updated document
|
|
24
|
+
*
|
|
25
|
+
* @throws {PouchError} with status 404 if document doesn't exist
|
|
26
|
+
*
|
|
27
|
+
* @remarks
|
|
28
|
+
* **Error Handling Pattern:**
|
|
29
|
+
* - This method does NOT create documents if they don't exist
|
|
30
|
+
* - Callers are responsible for handling 404 errors and creating documents
|
|
31
|
+
* - This design maintains separation of concerns (UpdateQueue handles conflicts, callers handle lifecycle)
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```typescript
|
|
35
|
+
* try {
|
|
36
|
+
* await updateQueue.update(docId, (doc) => ({ ...doc, field: newValue }));
|
|
37
|
+
* } catch (e) {
|
|
38
|
+
* if ((e as PouchError).status === 404) {
|
|
39
|
+
* // Create the document with initial values
|
|
40
|
+
* await db.put({ _id: docId, field: newValue, ...initialFields });
|
|
41
|
+
* }
|
|
42
|
+
* }
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
18
45
|
public update<T extends PouchDB.Core.Document<object>>(
|
|
19
46
|
id: PouchDB.Core.DocumentId,
|
|
20
47
|
update: Update<T>
|
|
@@ -57,7 +84,6 @@ export default class UpdateQueue extends Loggable {
|
|
|
57
84
|
for (let i = 0; i < MAX_RETRIES; i++) {
|
|
58
85
|
try {
|
|
59
86
|
const doc = await this.readDB.get<T>(id);
|
|
60
|
-
logger.debug(`Retrieved doc: ${id}`);
|
|
61
87
|
|
|
62
88
|
// Create a new doc object to apply updates to for this attempt
|
|
63
89
|
let updatedDoc = { ...doc };
|
|
@@ -77,7 +103,6 @@ export default class UpdateQueue extends Loggable {
|
|
|
77
103
|
}
|
|
78
104
|
|
|
79
105
|
await this.writeDB.put<T>(updatedDoc);
|
|
80
|
-
logger.debug(`Put doc: ${id}`);
|
|
81
106
|
|
|
82
107
|
// Success! Remove the updates we just applied.
|
|
83
108
|
this.pendingUpdates[id].splice(0, updatesToApply.length);
|
|
@@ -98,6 +123,7 @@ export default class UpdateQueue extends Loggable {
|
|
|
98
123
|
} else if (e.name === 'not_found' && i === 0) {
|
|
99
124
|
// Document not present - throw to caller for initialization
|
|
100
125
|
logger.warn(`Update failed for ${id} - does not exist. Throwing to caller.`);
|
|
126
|
+
delete this.inprogressUpdates[id];
|
|
101
127
|
throw e; // Let caller handle
|
|
102
128
|
} else {
|
|
103
129
|
// Max retries reached or a non-conflict error
|
|
@@ -37,6 +37,8 @@ export class StaticDataLayerProvider implements DataLayerProvider {
|
|
|
37
37
|
private initialized: boolean = false;
|
|
38
38
|
private courseUnpackers: Map<string, StaticDataUnpacker> = new Map();
|
|
39
39
|
private manifests: Record<string, StaticCourseManifest> = {};
|
|
40
|
+
// Mapping from dependency name to actual courseId for backwards compatibility
|
|
41
|
+
private dependencyNameToCourseId: Map<string, string> = new Map();
|
|
40
42
|
|
|
41
43
|
constructor(config: Partial<StaticDataLayerConfig>) {
|
|
42
44
|
this.config = {
|
|
@@ -70,12 +72,22 @@ export class StaticDataLayerProvider implements DataLayerProvider {
|
|
|
70
72
|
throw new Error(`Failed to fetch final content manifest for ${courseName} at ${finalManifestUrl}`);
|
|
71
73
|
}
|
|
72
74
|
const finalManifest = await finalManifestResponse.json();
|
|
73
|
-
|
|
74
|
-
|
|
75
|
+
|
|
76
|
+
// Extract courseId from the manifest to use as the lookup key
|
|
77
|
+
const courseId = finalManifest.courseId || finalManifest.courseConfig?.courseID;
|
|
78
|
+
if (!courseId) {
|
|
79
|
+
throw new Error(`Course manifest for ${courseName} missing courseId`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
this.manifests[courseId] = finalManifest;
|
|
75
83
|
const unpacker = new StaticDataUnpacker(finalManifest, baseUrl);
|
|
76
|
-
this.courseUnpackers.set(
|
|
84
|
+
this.courseUnpackers.set(courseId, unpacker);
|
|
77
85
|
|
|
78
|
-
|
|
86
|
+
// Also store mapping from dependency name to courseId for backwards compatibility
|
|
87
|
+
// This allows lookup by either dependency name or courseId
|
|
88
|
+
this.dependencyNameToCourseId.set(courseName, courseId);
|
|
89
|
+
|
|
90
|
+
logger.info(`[StaticDataLayerProvider] Successfully resolved and prepared course: ${courseName} (courseId: ${courseId})`);
|
|
79
91
|
}
|
|
80
92
|
} catch (e) {
|
|
81
93
|
logger.error(`[StaticDataLayerProvider] Failed to resolve dependency ${courseName}:`, e);
|
|
@@ -105,16 +117,28 @@ export class StaticDataLayerProvider implements DataLayerProvider {
|
|
|
105
117
|
}
|
|
106
118
|
|
|
107
119
|
getCourseDB(courseId: string): CourseDBInterface {
|
|
108
|
-
|
|
120
|
+
// Try direct lookup by courseId first
|
|
121
|
+
let unpacker = this.courseUnpackers.get(courseId);
|
|
122
|
+
let actualCourseId = courseId;
|
|
123
|
+
|
|
124
|
+
// If not found, try lookup by dependency name (backwards compatibility)
|
|
125
|
+
if (!unpacker) {
|
|
126
|
+
const mappedCourseId = this.dependencyNameToCourseId.get(courseId);
|
|
127
|
+
if (mappedCourseId) {
|
|
128
|
+
unpacker = this.courseUnpackers.get(mappedCourseId);
|
|
129
|
+
actualCourseId = mappedCourseId;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
109
133
|
if (!unpacker) {
|
|
110
134
|
throw new Error(`Course ${courseId} not found or failed to initialize in static data layer.`);
|
|
111
135
|
}
|
|
112
|
-
const manifest = this.manifests[
|
|
113
|
-
return new StaticCourseDB(
|
|
136
|
+
const manifest = this.manifests[actualCourseId];
|
|
137
|
+
return new StaticCourseDB(actualCourseId, unpacker, this.getUserDB(), manifest);
|
|
114
138
|
}
|
|
115
139
|
|
|
116
140
|
getCoursesDB(): CoursesDBInterface {
|
|
117
|
-
return new StaticCoursesDB(this.manifests);
|
|
141
|
+
return new StaticCoursesDB(this.manifests, this.dependencyNameToCourseId);
|
|
118
142
|
}
|
|
119
143
|
|
|
120
144
|
async getClassroomDB(
|
|
@@ -6,10 +6,23 @@ import { StaticCourseManifest } from '../../util/packer/types';
|
|
|
6
6
|
import { logger } from '../../util/logger';
|
|
7
7
|
|
|
8
8
|
export class StaticCoursesDB implements CoursesDBInterface {
|
|
9
|
-
constructor(
|
|
9
|
+
constructor(
|
|
10
|
+
private manifests: Record<string, StaticCourseManifest>,
|
|
11
|
+
private dependencyNameToCourseId?: Map<string, string>
|
|
12
|
+
) {}
|
|
10
13
|
|
|
11
14
|
async getCourseConfig(courseId: string): Promise<CourseConfig> {
|
|
12
|
-
|
|
15
|
+
// Try direct lookup by courseId first
|
|
16
|
+
let manifest = this.manifests[courseId];
|
|
17
|
+
|
|
18
|
+
// If not found, try lookup by dependency name (backwards compatibility)
|
|
19
|
+
if (!manifest && this.dependencyNameToCourseId) {
|
|
20
|
+
const mappedCourseId = this.dependencyNameToCourseId.get(courseId);
|
|
21
|
+
if (mappedCourseId) {
|
|
22
|
+
manifest = this.manifests[mappedCourseId];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
13
26
|
if (!manifest) {
|
|
14
27
|
logger.warn(`Course manifest for ${courseId} not found`);
|
|
15
28
|
throw new Error(`Course ${courseId} not found`);
|
|
@@ -208,6 +208,56 @@ export class SessionController<TView = unknown> extends Loggable {
|
|
|
208
208
|
return `${this.reviewQ.dequeueCount} Reviews, ${this.newQ.dequeueCount} New, ${this.failedQ.dequeueCount} failed`;
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
+
/**
|
|
212
|
+
* Returns debug information about the current session state.
|
|
213
|
+
* Used by SessionControllerDebug component for runtime inspection.
|
|
214
|
+
*/
|
|
215
|
+
public getDebugInfo() {
|
|
216
|
+
const extractQueueItems = (queue: ItemQueue<any>, limit: number = 10) => {
|
|
217
|
+
const items = [];
|
|
218
|
+
for (let i = 0; i < Math.min(queue.length, limit); i++) {
|
|
219
|
+
const item = queue.peek(i);
|
|
220
|
+
items.push({
|
|
221
|
+
courseID: item.courseID || 'unknown',
|
|
222
|
+
cardID: item.cardID || 'unknown',
|
|
223
|
+
status: item.status || 'unknown',
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
return items;
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const extractHydratedItems = () => {
|
|
230
|
+
// We can't easily iterate the hydrated queue without dequeuing,
|
|
231
|
+
// so we'll just report the count via hydratedCache.count below
|
|
232
|
+
|
|
233
|
+
const items: any[] = [];
|
|
234
|
+
return items;
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
reviewQueue: {
|
|
239
|
+
length: this.reviewQ.length,
|
|
240
|
+
dequeueCount: this.reviewQ.dequeueCount,
|
|
241
|
+
items: extractQueueItems(this.reviewQ),
|
|
242
|
+
},
|
|
243
|
+
newQueue: {
|
|
244
|
+
length: this.newQ.length,
|
|
245
|
+
dequeueCount: this.newQ.dequeueCount,
|
|
246
|
+
items: extractQueueItems(this.newQ),
|
|
247
|
+
},
|
|
248
|
+
failedQueue: {
|
|
249
|
+
length: this.failedQ.length,
|
|
250
|
+
dequeueCount: this.failedQ.dequeueCount,
|
|
251
|
+
items: extractQueueItems(this.failedQ),
|
|
252
|
+
},
|
|
253
|
+
hydratedCache: {
|
|
254
|
+
count: this.hydrationService.hydratedCount,
|
|
255
|
+
failedCacheSize: this.hydrationService.failedCacheSize,
|
|
256
|
+
items: extractHydratedItems(),
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
211
261
|
private async getScheduledReviews() {
|
|
212
262
|
const reviews = await Promise.all(
|
|
213
263
|
this.sources.map((c) =>
|
|
@@ -79,6 +79,13 @@ export class CardHydrationService<TView = unknown> {
|
|
|
79
79
|
return this.hydratedQ.length;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Get current failed card cache size.
|
|
84
|
+
*/
|
|
85
|
+
public get failedCacheSize(): number {
|
|
86
|
+
return this.failedCardCache.size;
|
|
87
|
+
}
|
|
88
|
+
|
|
82
89
|
/**
|
|
83
90
|
* Fill the hydrated queue up to BUFFER_SIZE with pre-fetched cards.
|
|
84
91
|
*/
|
|
@@ -60,32 +60,71 @@ export class ResponseProcessor {
|
|
|
60
60
|
};
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
// Debug logging for response processing
|
|
64
|
+
// logger.debug('[ResponseProcessor] Processing response', {
|
|
65
|
+
// cardId,
|
|
66
|
+
// courseId,
|
|
67
|
+
// isCorrect: cardRecord.isCorrect,
|
|
68
|
+
// performance: cardRecord.performance,
|
|
69
|
+
// priorAttempts: cardRecord.priorAttemps,
|
|
70
|
+
// currentSessionViews: sessionViews,
|
|
71
|
+
// maxSessionViews,
|
|
72
|
+
// maxAttemptsPerView,
|
|
73
|
+
// currentCardRecordsLength: currentCard.records.length,
|
|
74
|
+
// studySessionSourceType: studySessionItem.contentSourceType,
|
|
75
|
+
// studySessionSourceID: studySessionItem.contentSourceID,
|
|
76
|
+
// studySessionItemId: studySessionItem.cardID,
|
|
77
|
+
// studySessionItemType: studySessionItem.contentSourceType,
|
|
64
78
|
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
79
|
+
// cardRecordTimestamp: cardRecord.timeStamp,
|
|
80
|
+
// cardRecordResponseTime: cardRecord.timeSpent,
|
|
81
|
+
// });
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const history = await cardHistory;
|
|
85
|
+
|
|
86
|
+
// Debug logging for card history
|
|
87
|
+
// logger.debug('[ResponseProcessor] History loaded:', {
|
|
88
|
+
// cardId,
|
|
89
|
+
// historyRecordsCount: history.records.length,
|
|
90
|
+
// historyRecords: history.records.map((record) => ({
|
|
91
|
+
// timeStamp: record.timeStamp,
|
|
92
|
+
// isCorrect: 'isCorrect' in record ? record.isCorrect : 'N/A',
|
|
93
|
+
// performance: 'performance' in record ? record.performance : 'N/A',
|
|
94
|
+
// priorAttempts: 'priorAttemps' in record ? record.priorAttemps : 'N/A',
|
|
95
|
+
// })),
|
|
96
|
+
// firstInteraction: history.records.length === 1,
|
|
97
|
+
// lastRecord: history.records[history.records.length - 1],
|
|
98
|
+
// });
|
|
99
|
+
|
|
100
|
+
// Handle correct responses
|
|
101
|
+
if (cardRecord.isCorrect) {
|
|
102
|
+
return this.processCorrectResponse(
|
|
103
|
+
cardRecord,
|
|
104
|
+
history,
|
|
105
|
+
studySessionItem,
|
|
106
|
+
courseRegistrationDoc,
|
|
107
|
+
currentCard,
|
|
108
|
+
courseId,
|
|
109
|
+
cardId
|
|
110
|
+
);
|
|
111
|
+
} else {
|
|
112
|
+
// Handle incorrect responses
|
|
113
|
+
return this.processIncorrectResponse(
|
|
114
|
+
cardRecord,
|
|
115
|
+
history,
|
|
116
|
+
courseRegistrationDoc,
|
|
117
|
+
currentCard,
|
|
118
|
+
courseId,
|
|
119
|
+
cardId,
|
|
120
|
+
maxAttemptsPerView,
|
|
121
|
+
maxSessionViews,
|
|
122
|
+
sessionViews
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
} catch (e: unknown) {
|
|
126
|
+
logger.error('[ResponseProcessor] Failed to load card history', { e, cardId });
|
|
127
|
+
throw e;
|
|
89
128
|
}
|
|
90
129
|
}
|
|
91
130
|
|