@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.
Files changed (76) hide show
  1. package/dist/core/index.d.mts +7 -6
  2. package/dist/core/index.d.ts +7 -6
  3. package/dist/core/index.js +358 -87
  4. package/dist/core/index.js.map +1 -1
  5. package/dist/core/index.mjs +358 -87
  6. package/dist/core/index.mjs.map +1 -1
  7. package/dist/{dataLayerProvider-DqtNroSh.d.ts → dataLayerProvider-BiP3kWix.d.mts} +8 -1
  8. package/dist/{dataLayerProvider-BInqI_RF.d.mts → dataLayerProvider-DSdeyRT3.d.ts} +8 -1
  9. package/dist/impl/couch/index.d.mts +19 -7
  10. package/dist/impl/couch/index.d.ts +19 -7
  11. package/dist/impl/couch/index.js +375 -100
  12. package/dist/impl/couch/index.js.map +1 -1
  13. package/dist/impl/couch/index.mjs +374 -99
  14. package/dist/impl/couch/index.mjs.map +1 -1
  15. package/dist/impl/static/index.d.mts +23 -8
  16. package/dist/impl/static/index.d.ts +23 -8
  17. package/dist/impl/static/index.js +289 -85
  18. package/dist/impl/static/index.js.map +1 -1
  19. package/dist/impl/static/index.mjs +289 -85
  20. package/dist/impl/static/index.mjs.map +1 -1
  21. package/dist/{index-CUNnL38E.d.mts → index-Bmll7Xse.d.mts} +1 -1
  22. package/dist/{index-CLL31bEy.d.ts → index-CD8BZz2k.d.ts} +1 -1
  23. package/dist/index.d.mts +123 -20
  24. package/dist/index.d.ts +123 -20
  25. package/dist/index.js +1133 -343
  26. package/dist/index.js.map +1 -1
  27. package/dist/index.mjs +1137 -343
  28. package/dist/index.mjs.map +1 -1
  29. package/dist/pouch/index.d.mts +1 -0
  30. package/dist/pouch/index.d.ts +1 -0
  31. package/dist/pouch/index.js +49 -0
  32. package/dist/pouch/index.js.map +1 -0
  33. package/dist/pouch/index.mjs +16 -0
  34. package/dist/pouch/index.mjs.map +1 -0
  35. package/dist/{types-BefDGkKa.d.ts → types-CewsN87z.d.ts} +1 -1
  36. package/dist/{types-DC-ckZug.d.mts → types-Dbp5DaRR.d.mts} +1 -1
  37. package/dist/{types-legacy-Birv-Jx6.d.mts → types-legacy-6ettoclI.d.mts} +17 -2
  38. package/dist/{types-legacy-Birv-Jx6.d.ts → types-legacy-6ettoclI.d.ts} +17 -2
  39. package/dist/{userDB-DusL7OXe.d.ts → userDB-C4yyAnpp.d.mts} +89 -56
  40. package/dist/{userDB-C33Hzjgn.d.mts → userDB-CD6s6ZCp.d.ts} +89 -56
  41. package/dist/util/packer/index.d.mts +3 -3
  42. package/dist/util/packer/index.d.ts +3 -3
  43. package/package.json +3 -3
  44. package/src/core/interfaces/contentSource.ts +3 -2
  45. package/src/core/interfaces/courseDB.ts +26 -3
  46. package/src/core/interfaces/dataLayerProvider.ts +9 -1
  47. package/src/core/interfaces/userDB.ts +80 -64
  48. package/src/core/navigators/elo.ts +10 -7
  49. package/src/core/navigators/hardcodedOrder.ts +64 -0
  50. package/src/core/navigators/index.ts +2 -1
  51. package/src/core/types/contentNavigationStrategy.ts +2 -1
  52. package/src/core/types/types-legacy.ts +7 -2
  53. package/src/impl/common/BaseUserDB.ts +60 -14
  54. package/src/impl/couch/CouchDBSyncStrategy.ts +2 -2
  55. package/src/impl/couch/PouchDataLayerProvider.ts +21 -0
  56. package/src/impl/couch/adminDB.ts +2 -2
  57. package/src/impl/couch/auth.ts +13 -4
  58. package/src/impl/couch/classroomDB.ts +10 -12
  59. package/src/impl/couch/courseAPI.ts +2 -2
  60. package/src/impl/couch/courseDB.ts +204 -38
  61. package/src/impl/couch/courseLookupDB.ts +4 -3
  62. package/src/impl/couch/index.ts +36 -4
  63. package/src/impl/couch/pouchdb-setup.ts +3 -3
  64. package/src/impl/couch/updateQueue.ts +59 -36
  65. package/src/impl/static/StaticDataLayerProvider.ts +68 -17
  66. package/src/impl/static/courseDB.ts +64 -20
  67. package/src/impl/static/coursesDB.ts +10 -6
  68. package/src/pouch/index.ts +2 -0
  69. package/src/study/ItemQueue.ts +58 -0
  70. package/src/study/SessionController.ts +182 -111
  71. package/src/study/SpacedRepetition.ts +1 -1
  72. package/src/study/services/CardHydrationService.ts +153 -0
  73. package/src/study/services/EloService.ts +85 -0
  74. package/src/study/services/ResponseProcessor.ts +224 -0
  75. package/src/study/services/SrsService.ts +44 -0
  76. 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
- manifests: Record<string, StaticCourseManifest>; // courseId -> manifest
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
- manifests: config.manifests || {},
44
+ rootManifest: config.rootManifest || { dependencies: {} },
45
+ rootManifestUrl: config.rootManifestUrl || '/',
35
46
  };
36
47
  }
37
48
 
38
- async initialize(): Promise<void> {
39
- if (this.initialized) return;
49
+ private async resolveCourseDependencies(): Promise<void> {
50
+ logger.info('[StaticDataLayerProvider] Starting course dependency resolution...');
51
+ const rootManifest = this.config.rootManifest;
40
52
 
41
- logger.info('Initializing static data layer provider');
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
- // Load manifests for all courses
44
- for (const [courseId, manifest] of Object.entries(this.config.manifests)) {
45
- const unpacker = new StaticDataUnpacker(
46
- manifest,
47
- `${this.config.staticContentPath}/${courseId}`
48
- );
49
- this.courseUnpackers.set(courseId, unpacker);
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.config.manifests[courseId];
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.config.manifests);
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 { Tag, TagStub, DocType, SkuilderCourseData } from '../../core/types/types-legacy';
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(elo: number, limit?: number): Promise<string[]> {
95
- return this.unpacker.queryByElo(elo, limit || 25);
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?: number): Promise<StudySessionNewItem[]> {
118
- // Simplified implementation - would need proper navigation strategy
119
- const cardIds = await this.unpacker.queryByElo(1000, limit || 10);
120
- return cardIds.map((cardId) => ({
121
- status: 'new' as const,
122
- qualifiedID: `${this.courseId}-${cardId}`,
123
- cardID: cardId,
124
- contentSourceType: 'course' as const,
125
- contentSourceID: this.courseId,
126
- courseID: this.courseId,
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: string) => boolean
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((cardId) => ({
185
+ return cardIds.slice(0, options.limit).map((card) => ({
158
186
  status: 'new' as const,
159
- qualifiedID: `${this.courseId}-${cardId}`,
160
- cardID: 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
- id: 'ELO',
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
- if (!this.manifests[courseId]) {
13
- // throw new Error(`Course ${courseId} not found`);
14
- logger.warn(`Course ${courseId} not found`);
15
- return {} as CourseConfig; // Return empty config if course not found
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
- // Would need to fetch the course config from static files
19
- return {} as CourseConfig;
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,2 @@
1
+ // Export configured PouchDB instance
2
+ export { default } from '../impl/couch/pouchdb-setup.js';
@@ -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
+ }