@vue-skuilder/db 0.1.11-9 → 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 +358 -87
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +358 -87
- package/dist/core/index.mjs.map +1 -1
- package/dist/{dataLayerProvider-DqtNroSh.d.ts → dataLayerProvider-BiP3kWix.d.mts} +8 -1
- package/dist/{dataLayerProvider-BInqI_RF.d.mts → dataLayerProvider-DSdeyRT3.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 +375 -100
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +374 -99
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.d.mts +23 -8
- package/dist/impl/static/index.d.ts +23 -8
- package/dist/impl/static/index.js +289 -85
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +289 -85
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/{index-CUNnL38E.d.mts → index-Bmll7Xse.d.mts} +1 -1
- package/dist/{index-CLL31bEy.d.ts → index-CD8BZz2k.d.ts} +1 -1
- package/dist/index.d.mts +123 -20
- package/dist/index.d.ts +123 -20
- package/dist/index.js +1133 -343
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1137 -343
- 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-BefDGkKa.d.ts → types-CewsN87z.d.ts} +1 -1
- package/dist/{types-DC-ckZug.d.mts → types-Dbp5DaRR.d.mts} +1 -1
- package/dist/{types-legacy-Birv-Jx6.d.mts → types-legacy-6ettoclI.d.mts} +17 -2
- package/dist/{types-legacy-Birv-Jx6.d.ts → types-legacy-6ettoclI.d.ts} +17 -2
- package/dist/{userDB-DusL7OXe.d.ts → userDB-C4yyAnpp.d.mts} +89 -56
- package/dist/{userDB-C33Hzjgn.d.mts → userDB-CD6s6ZCp.d.ts} +89 -56
- 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/hardcodedOrder.ts +64 -0
- package/src/core/navigators/index.ts +2 -1
- package/src/core/types/contentNavigationStrategy.ts +2 -1
- package/src/core/types/types-legacy.ts +7 -2
- package/src/impl/common/BaseUserDB.ts +60 -14
- 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 +204 -38
- 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 +59 -36
- package/src/impl/static/StaticDataLayerProvider.ts +68 -17
- package/src/impl/static/courseDB.ts +64 -20
- package/src/impl/static/coursesDB.ts +10 -6
- package/src/pouch/index.ts +2 -0
- package/src/study/ItemQueue.ts +58 -0
- package/src/study/SessionController.ts +182 -111
- package/src/study/SpacedRepetition.ts +1 -1
- 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
- package/tsup.config.ts +1 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
|
|
1
2
|
// packages/db/src/impl/static/StaticDataLayerProvider.ts
|
|
2
3
|
|
|
3
4
|
import {
|
|
@@ -7,6 +8,7 @@ import {
|
|
|
7
8
|
CourseDBInterface,
|
|
8
9
|
DataLayerProvider,
|
|
9
10
|
UserDBInterface,
|
|
11
|
+
UserDBReader,
|
|
10
12
|
} from '../../core/interfaces';
|
|
11
13
|
import { logger } from '../../util/logger';
|
|
12
14
|
import { StaticCourseManifest } from '../../util/packer/types';
|
|
@@ -16,39 +18,78 @@ import { StaticCoursesDB } from './coursesDB';
|
|
|
16
18
|
import { BaseUser } from '../common';
|
|
17
19
|
import { NoOpSyncStrategy } from './NoOpSyncStrategy';
|
|
18
20
|
|
|
21
|
+
|
|
22
|
+
interface SkuilderManifest {
|
|
23
|
+
name?: string;
|
|
24
|
+
version?: string;
|
|
25
|
+
description?: string;
|
|
26
|
+
dependencies?: Record<string, string>;
|
|
27
|
+
}
|
|
28
|
+
|
|
19
29
|
interface StaticDataLayerConfig {
|
|
20
|
-
staticContentPath: string;
|
|
21
30
|
localStoragePrefix?: string;
|
|
22
|
-
|
|
31
|
+
rootManifest: SkuilderManifest; // The parsed root skuilder.json object
|
|
32
|
+
rootManifestUrl: string; // The absolute URL where the root manifest was found
|
|
23
33
|
}
|
|
24
34
|
|
|
25
35
|
export class StaticDataLayerProvider implements DataLayerProvider {
|
|
26
36
|
private config: StaticDataLayerConfig;
|
|
27
37
|
private initialized: boolean = false;
|
|
28
38
|
private courseUnpackers: Map<string, StaticDataUnpacker> = new Map();
|
|
39
|
+
private manifests: Record<string, StaticCourseManifest> = {};
|
|
29
40
|
|
|
30
41
|
constructor(config: Partial<StaticDataLayerConfig>) {
|
|
31
42
|
this.config = {
|
|
32
|
-
staticContentPath: config.staticContentPath || '/static-courses',
|
|
33
43
|
localStoragePrefix: config.localStoragePrefix || 'skuilder-static',
|
|
34
|
-
|
|
44
|
+
rootManifest: config.rootManifest || { dependencies: {} },
|
|
45
|
+
rootManifestUrl: config.rootManifestUrl || '/',
|
|
35
46
|
};
|
|
36
47
|
}
|
|
37
48
|
|
|
38
|
-
async
|
|
39
|
-
|
|
49
|
+
private async resolveCourseDependencies(): Promise<void> {
|
|
50
|
+
logger.info('[StaticDataLayerProvider] Starting course dependency resolution...');
|
|
51
|
+
const rootManifest = this.config.rootManifest;
|
|
40
52
|
|
|
41
|
-
|
|
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;
|
|
42
67
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
+
}
|
|
50
84
|
}
|
|
85
|
+
logger.info('[StaticDataLayerProvider] Course dependency resolution complete.');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async initialize(): Promise<void> {
|
|
89
|
+
if (this.initialized) return;
|
|
51
90
|
|
|
91
|
+
logger.info('Initializing static data layer provider');
|
|
92
|
+
await this.resolveCourseDependencies();
|
|
52
93
|
this.initialized = true;
|
|
53
94
|
}
|
|
54
95
|
|
|
@@ -66,14 +107,14 @@ export class StaticDataLayerProvider implements DataLayerProvider {
|
|
|
66
107
|
getCourseDB(courseId: string): CourseDBInterface {
|
|
67
108
|
const unpacker = this.courseUnpackers.get(courseId);
|
|
68
109
|
if (!unpacker) {
|
|
69
|
-
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.`);
|
|
70
111
|
}
|
|
71
|
-
const manifest = this.
|
|
112
|
+
const manifest = this.manifests[courseId];
|
|
72
113
|
return new StaticCourseDB(courseId, unpacker, this.getUserDB(), manifest);
|
|
73
114
|
}
|
|
74
115
|
|
|
75
116
|
getCoursesDB(): CoursesDBInterface {
|
|
76
|
-
return new StaticCoursesDB(this.
|
|
117
|
+
return new StaticCoursesDB(this.manifests);
|
|
77
118
|
}
|
|
78
119
|
|
|
79
120
|
async getClassroomDB(
|
|
@@ -87,6 +128,16 @@ export class StaticDataLayerProvider implements DataLayerProvider {
|
|
|
87
128
|
throw new Error('Admin functions not supported in static mode');
|
|
88
129
|
}
|
|
89
130
|
|
|
131
|
+
async createUserReaderForUser(targetUsername: string): Promise<UserDBReader> {
|
|
132
|
+
logger.warn(`StaticDataLayerProvider: Multi-user access not supported in static mode`);
|
|
133
|
+
logger.warn(`Request: trying to access data for ${targetUsername}`);
|
|
134
|
+
logger.warn(`Returning current user's data instead`);
|
|
135
|
+
|
|
136
|
+
// In static mode, just return the current user's DB as a reader
|
|
137
|
+
// This is safe since static mode is typically for development/testing
|
|
138
|
+
return this.getUserDB() as UserDBReader;
|
|
139
|
+
}
|
|
140
|
+
|
|
90
141
|
isReadOnly(): boolean {
|
|
91
142
|
return true;
|
|
92
143
|
}
|
|
@@ -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[]> {
|
|
@@ -114,22 +132,27 @@ export class StaticCourseDB implements CourseDBInterface {
|
|
|
114
132
|
return { ok: true, id: cardId, rev: '1-static' };
|
|
115
133
|
}
|
|
116
134
|
|
|
117
|
-
async getNewCards(limit
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
+
});
|
|
128
151
|
}
|
|
129
152
|
|
|
130
153
|
async getCardsCenteredAtELO(
|
|
131
154
|
options: { limit: number; elo: 'user' | 'random' | number },
|
|
132
|
-
filter?: (id:
|
|
155
|
+
filter?: (id: QualifiedCardID) => boolean
|
|
133
156
|
): Promise<StudySessionNewItem[]> {
|
|
134
157
|
let targetElo = typeof options.elo === 'number' ? options.elo : 1000;
|
|
135
158
|
|
|
@@ -148,16 +171,21 @@ export class StaticCourseDB implements CourseDBInterface {
|
|
|
148
171
|
targetElo = 800 + Math.random() * 400; // Random between 800-1200
|
|
149
172
|
}
|
|
150
173
|
|
|
151
|
-
let cardIds = await this.unpacker.queryByElo(targetElo, options.limit * 2)
|
|
174
|
+
let cardIds = (await this.unpacker.queryByElo(targetElo, options.limit * 2)).map((c) => {
|
|
175
|
+
return {
|
|
176
|
+
cardID: c,
|
|
177
|
+
courseID: this.courseId,
|
|
178
|
+
};
|
|
179
|
+
});
|
|
152
180
|
|
|
153
181
|
if (filter) {
|
|
154
182
|
cardIds = cardIds.filter(filter);
|
|
155
183
|
}
|
|
156
184
|
|
|
157
|
-
return cardIds.slice(0, options.limit).map((
|
|
185
|
+
return cardIds.slice(0, options.limit).map((card) => ({
|
|
158
186
|
status: 'new' as const,
|
|
159
|
-
qualifiedID: `${this.courseId}-${cardId}`,
|
|
160
|
-
cardID:
|
|
187
|
+
// qualifiedID: `${this.courseId}-${cardId}`,
|
|
188
|
+
cardID: card.cardID,
|
|
161
189
|
contentSourceType: 'course' as const,
|
|
162
190
|
contentSourceID: this.courseId,
|
|
163
191
|
courseID: this.courseId,
|
|
@@ -341,7 +369,7 @@ export class StaticCourseDB implements CourseDBInterface {
|
|
|
341
369
|
// Navigation Strategy Manager implementation
|
|
342
370
|
async getNavigationStrategy(_id: string): Promise<ContentNavigationStrategyData> {
|
|
343
371
|
return {
|
|
344
|
-
|
|
372
|
+
_id: 'NAVIGATION_STRATEGY-ELO',
|
|
345
373
|
docType: DocType.NAVIGATION_STRATEGY,
|
|
346
374
|
name: 'ELO',
|
|
347
375
|
description: 'ELO-based navigation strategy',
|
|
@@ -390,4 +418,20 @@ export class StaticCourseDB implements CourseDBInterface {
|
|
|
390
418
|
async getAttachmentBlob(docId: string, attachmentName: string): Promise<Blob | Buffer | null> {
|
|
391
419
|
return this.unpacker.getAttachmentBlob(docId, attachmentName);
|
|
392
420
|
}
|
|
421
|
+
|
|
422
|
+
// Admin search methods
|
|
423
|
+
async searchCards(_query: string): Promise<any[]> {
|
|
424
|
+
// In static mode, return empty results for now
|
|
425
|
+
// Could be implemented with local search if needed
|
|
426
|
+
return [];
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
async find(_request: PouchDB.Find.FindRequest<any>): Promise<PouchDB.Find.FindResponse<any>> {
|
|
430
|
+
// In static mode, return empty results for now
|
|
431
|
+
// Could be implemented with local search if needed
|
|
432
|
+
return {
|
|
433
|
+
docs: [],
|
|
434
|
+
warning: 'Find operations not supported in static mode',
|
|
435
|
+
} as any;
|
|
436
|
+
}
|
|
393
437
|
}
|
|
@@ -9,14 +9,18 @@ export class StaticCoursesDB implements CoursesDBInterface {
|
|
|
9
9
|
constructor(private manifests: Record<string, StaticCourseManifest>) {}
|
|
10
10
|
|
|
11
11
|
async getCourseConfig(courseId: string): Promise<CourseConfig> {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
logger.warn(`Course ${courseId} not found`);
|
|
15
|
-
|
|
12
|
+
const manifest = this.manifests[courseId];
|
|
13
|
+
if (!manifest) {
|
|
14
|
+
logger.warn(`Course manifest for ${courseId} not found`);
|
|
15
|
+
throw new Error(`Course ${courseId} not found`);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
if (manifest.courseConfig) {
|
|
19
|
+
return manifest.courseConfig;
|
|
20
|
+
} else {
|
|
21
|
+
logger.warn(`Course config not found in manifest for course ${courseId}`);
|
|
22
|
+
throw new Error(`Course config not found for course ${courseId}`);
|
|
23
|
+
}
|
|
20
24
|
}
|
|
21
25
|
|
|
22
26
|
async getCourseList(): Promise<CourseConfig[]> {
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export class ItemQueue<T> {
|
|
2
|
+
private q: T[] = [];
|
|
3
|
+
private seenCardIds: string[] = [];
|
|
4
|
+
private _dequeueCount: number = 0;
|
|
5
|
+
public get dequeueCount(): number {
|
|
6
|
+
return this._dequeueCount;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
public add(item: T, cardId: string) {
|
|
10
|
+
if (this.seenCardIds.find((d) => d === cardId)) {
|
|
11
|
+
return; // do not re-add a card to the same queue
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
this.seenCardIds.push(cardId);
|
|
15
|
+
this.q.push(item);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
public addAll(items: T[], cardIdExtractor: (item: T) => string) {
|
|
19
|
+
items.forEach((i) => this.add(i, cardIdExtractor(i)));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public get length() {
|
|
23
|
+
return this.q.length;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public peek(index: number): T {
|
|
27
|
+
return this.q[index];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public dequeue(cardIdExtractor?: (item: T) => string): T | null {
|
|
31
|
+
if (this.q.length !== 0) {
|
|
32
|
+
this._dequeueCount++;
|
|
33
|
+
const item = this.q.splice(0, 1)[0];
|
|
34
|
+
|
|
35
|
+
// Remove cardId from seenCardIds when dequeuing to allow re-queueing
|
|
36
|
+
if (cardIdExtractor) {
|
|
37
|
+
const cardId = cardIdExtractor(item);
|
|
38
|
+
const index = this.seenCardIds.indexOf(cardId);
|
|
39
|
+
if (index > -1) {
|
|
40
|
+
this.seenCardIds.splice(index, 1);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return item;
|
|
45
|
+
} else {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public get toString(): string {
|
|
51
|
+
return (
|
|
52
|
+
`${typeof this.q[0]}:\n` +
|
|
53
|
+
this.q
|
|
54
|
+
.map((i) => `\t${(i as any).courseID}+${(i as any).cardID}: ${(i as any).status}`)
|
|
55
|
+
.join('\n')
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|