@vue-skuilder/db 0.1.31-a → 0.1.31

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 (50) hide show
  1. package/dist/{contentSource-BmnmvH8C.d.ts → contentSource-Bdwkvqa8.d.ts} +35 -4
  2. package/dist/{contentSource-DfBbaLA-.d.cts → contentSource-DF1nUbPQ.d.cts} +35 -4
  3. package/dist/core/index.d.cts +48 -3
  4. package/dist/core/index.d.ts +48 -3
  5. package/dist/core/index.js +587 -56
  6. package/dist/core/index.js.map +1 -1
  7. package/dist/core/index.mjs +586 -56
  8. package/dist/core/index.mjs.map +1 -1
  9. package/dist/{dataLayerProvider-BeRXVMs5.d.cts → dataLayerProvider-BKmVoyJR.d.ts} +20 -1
  10. package/dist/{dataLayerProvider-CG9GfaAY.d.ts → dataLayerProvider-BQdfJuBN.d.cts} +20 -1
  11. package/dist/impl/couch/index.d.cts +156 -4
  12. package/dist/impl/couch/index.d.ts +156 -4
  13. package/dist/impl/couch/index.js +805 -47
  14. package/dist/impl/couch/index.js.map +1 -1
  15. package/dist/impl/couch/index.mjs +804 -47
  16. package/dist/impl/couch/index.mjs.map +1 -1
  17. package/dist/impl/static/index.d.cts +3 -2
  18. package/dist/impl/static/index.d.ts +3 -2
  19. package/dist/impl/static/index.js +542 -37
  20. package/dist/impl/static/index.js.map +1 -1
  21. package/dist/impl/static/index.mjs +542 -37
  22. package/dist/impl/static/index.mjs.map +1 -1
  23. package/dist/index.d.cts +64 -3
  24. package/dist/index.d.ts +64 -3
  25. package/dist/index.js +1040 -90
  26. package/dist/index.js.map +1 -1
  27. package/dist/index.mjs +1030 -81
  28. package/dist/index.mjs.map +1 -1
  29. package/docs/navigators-architecture.md +64 -5
  30. package/package.json +3 -3
  31. package/src/core/interfaces/contentSource.ts +6 -0
  32. package/src/core/interfaces/courseDB.ts +6 -0
  33. package/src/core/interfaces/dataLayerProvider.ts +20 -0
  34. package/src/core/navigators/Pipeline.ts +414 -9
  35. package/src/core/navigators/PipelineAssembler.ts +23 -18
  36. package/src/core/navigators/PipelineDebugger.ts +115 -1
  37. package/src/core/navigators/filters/hierarchyDefinition.ts +78 -8
  38. package/src/core/navigators/generators/prescribed.ts +95 -0
  39. package/src/core/navigators/index.ts +55 -10
  40. package/src/impl/common/BaseUserDB.ts +4 -1
  41. package/src/impl/couch/CourseSyncService.ts +356 -0
  42. package/src/impl/couch/PouchDataLayerProvider.ts +21 -1
  43. package/src/impl/couch/courseDB.ts +60 -13
  44. package/src/impl/couch/index.ts +1 -0
  45. package/src/impl/static/courseDB.ts +5 -0
  46. package/src/study/ItemQueue.ts +42 -0
  47. package/src/study/SessionController.ts +195 -22
  48. package/src/study/SpacedRepetition.ts +7 -2
  49. package/tests/core/navigators/Pipeline.test.ts +1 -1
  50. package/tests/core/navigators/PipelineAssembler.test.ts +15 -14
@@ -1,4 +1,4 @@
1
- import { U as UserDBInterface, a as UserDBReader, C as CourseDBInterface, b as CoursesDBInterface, c as ClassroomDBInterface, A as AdminDBInterface } from './contentSource-DfBbaLA-.cjs';
1
+ import { U as UserDBInterface, a as UserDBReader, C as CourseDBInterface, b as CoursesDBInterface, c as ClassroomDBInterface, A as AdminDBInterface } from './contentSource-Bdwkvqa8.js';
2
2
 
3
3
  /**
4
4
  * Main factory interface for data access
@@ -43,6 +43,25 @@ interface DataLayerProvider {
43
43
  * Check if this data layer is read-only
44
44
  */
45
45
  isReadOnly(): boolean;
46
+ /**
47
+ * Trigger local replication of a course database.
48
+ *
49
+ * When a course opts in via `CourseConfig.localSync.enabled`, this method
50
+ * replicates the remote course DB to a local PouchDB instance. Subsequent
51
+ * `getCourseDB()` calls for that course will return a CourseDB that reads
52
+ * from the local replica (fast, no network) and writes to the remote
53
+ * (ELO updates, admin ops).
54
+ *
55
+ * Safe to call multiple times — concurrent calls coalesce. Returns when
56
+ * sync is complete (or immediately if already synced / disabled).
57
+ *
58
+ * Implementations that don't support local sync may no-op.
59
+ *
60
+ * @param courseId - The course to sync locally
61
+ * @param forceEnabled - Skip CourseConfig check and sync regardless.
62
+ * Use when the caller already knows local sync is desired.
63
+ */
64
+ ensureCourseSynced?(courseId: string, forceEnabled?: boolean): Promise<void>;
46
65
  }
47
66
 
48
67
  export type { DataLayerProvider as D };
@@ -1,4 +1,4 @@
1
- import { U as UserDBInterface, a as UserDBReader, C as CourseDBInterface, b as CoursesDBInterface, c as ClassroomDBInterface, A as AdminDBInterface } from './contentSource-BmnmvH8C.js';
1
+ import { U as UserDBInterface, a as UserDBReader, C as CourseDBInterface, b as CoursesDBInterface, c as ClassroomDBInterface, A as AdminDBInterface } from './contentSource-DF1nUbPQ.cjs';
2
2
 
3
3
  /**
4
4
  * Main factory interface for data access
@@ -43,6 +43,25 @@ interface DataLayerProvider {
43
43
  * Check if this data layer is read-only
44
44
  */
45
45
  isReadOnly(): boolean;
46
+ /**
47
+ * Trigger local replication of a course database.
48
+ *
49
+ * When a course opts in via `CourseConfig.localSync.enabled`, this method
50
+ * replicates the remote course DB to a local PouchDB instance. Subsequent
51
+ * `getCourseDB()` calls for that course will return a CourseDB that reads
52
+ * from the local replica (fast, no network) and writes to the remote
53
+ * (ELO updates, admin ops).
54
+ *
55
+ * Safe to call multiple times — concurrent calls coalesce. Returns when
56
+ * sync is complete (or immediately if already synced / disabled).
57
+ *
58
+ * Implementations that don't support local sync may no-op.
59
+ *
60
+ * @param courseId - The course to sync locally
61
+ * @param forceEnabled - Skip CourseConfig check and sync regardless.
62
+ * Use when the caller already knows local sync is desired.
63
+ */
64
+ ensureCourseSynced?(courseId: string, forceEnabled?: boolean): Promise<void>;
46
65
  }
47
66
 
48
67
  export type { DataLayerProvider as D };
@@ -1,7 +1,7 @@
1
1
  import { T as TagStub, a as Tag, S as SkuilderCourseData, Q as QualifiedCardID } from '../../types-legacy-JXDxinpU.cjs';
2
2
  import { Moment } from 'moment';
3
- import { A as AdminDBInterface, g as AssignedContent, h as StudyContentSource, i as StudentClassroomDBInterface, U as UserDBInterface, W as WeightedCard, T as TeacherClassroomDBInterface, b as CoursesDBInterface, C as CourseDBInterface, d as CourseInfo, D as DataLayerResult, e as ContentNavigationStrategyData, f as ContentNavigator, S as StudySessionItem, j as ScheduledCard } from '../../contentSource-DfBbaLA-.cjs';
4
- export { q as ContentSourceID, k as StudySessionFailedItem, l as StudySessionFailedNewItem, m as StudySessionFailedReviewItem, n as StudySessionNewItem, o as StudySessionReviewItem, r as getStudySource, p as isReview } from '../../contentSource-DfBbaLA-.cjs';
3
+ import { A as AdminDBInterface, g as AssignedContent, h as StudyContentSource, i as StudentClassroomDBInterface, U as UserDBInterface, W as WeightedCard, T as TeacherClassroomDBInterface, b as CoursesDBInterface, C as CourseDBInterface, d as CourseInfo, D as DataLayerResult, e as ContentNavigationStrategyData, f as ContentNavigator, S as StudySessionItem, j as ScheduledCard } from '../../contentSource-DF1nUbPQ.cjs';
4
+ export { q as ContentSourceID, k as StudySessionFailedItem, l as StudySessionFailedNewItem, m as StudySessionFailedReviewItem, n as StudySessionNewItem, o as StudySessionReviewItem, r as getStudySource, p as isReview } from '../../contentSource-DF1nUbPQ.cjs';
5
5
  import * as _vue_skuilder_common from '@vue-skuilder/common';
6
6
  import { ClassroomConfig, DataShape, CourseElo, CourseConfig as CourseConfig$1 } from '@vue-skuilder/common';
7
7
  import { S as SyncStrategy, A as AccountCreationResult, a as AuthenticationResult } from '../../SyncStrategy-CyATpyLQ.cjs';
@@ -45,6 +45,27 @@ interface CourseConfig {
45
45
  questionTypes: QuestionType55[];
46
46
  disambiguator?: string;
47
47
  orchestration?: CourseOrchestrationConfig;
48
+ /**
49
+ * Opt-in client-side replication of the course database.
50
+ *
51
+ * When enabled, the client replicates the course DB to a local PouchDB
52
+ * instance on first visit (full one-shot sync) and performs incremental
53
+ * sync on subsequent visits. Pipeline scoring, tag hydration, and card
54
+ * lookup then run against the local replica — eliminating network round
55
+ * trips from the study-session hot path.
56
+ *
57
+ * **Read/write split:** The local DB is a read-only snapshot. All writes
58
+ * (card ELO updates, tag mutations, etc.) continue to target the remote
59
+ * CouchDB. This avoids propagating per-interaction ELO noise to every
60
+ * syncing client — the remote DB aggregates writes from all users, and
61
+ * each client's local snapshot is refreshed on the next page load.
62
+ *
63
+ * Defaults to `undefined` (disabled). Courses with small, relatively
64
+ * static content databases (e.g. < 50 MB) are good candidates.
65
+ */
66
+ localSync?: {
67
+ enabled: boolean;
68
+ };
48
69
  }
49
70
 
50
71
  declare class AdminDB implements AdminDBInterface {
@@ -156,11 +177,36 @@ declare class CoursesDB implements CoursesDBInterface {
156
177
  disambiguateCourse(courseId: string, disambiguator: string): Promise<void>;
157
178
  }
158
179
  declare class CourseDB implements CourseDBInterface {
180
+ /**
181
+ * Primary database handle used for all **read** operations (queries, gets).
182
+ *
183
+ * When local sync is active, this points to the local PouchDB replica for
184
+ * fast, network-free reads. Otherwise it points to the remote CouchDB.
185
+ */
159
186
  private db;
187
+ /**
188
+ * Remote database handle used for all **write** operations.
189
+ *
190
+ * Always points to the remote CouchDB so that writes (ELO updates, tag
191
+ * mutations, admin operations) aggregate on the server. The local replica
192
+ * is a read-only snapshot that refreshes on the next page load.
193
+ *
194
+ * When local sync is NOT active, this is the same instance as `this.db`.
195
+ */
196
+ private remoteDB;
160
197
  private id;
161
198
  private _getCurrentUser;
162
199
  private updateQueue;
163
- constructor(id: string, userLookup: () => Promise<UserDBInterface>);
200
+ /**
201
+ * @param id - Course ID
202
+ * @param userLookup - Async function returning the current user DB
203
+ * @param localDB - Optional local PouchDB replica for reads. When provided,
204
+ * `this.db` uses the local replica and `this.remoteDB` stays remote.
205
+ * The UpdateQueue reads from remote and writes to remote (local `_rev`
206
+ * values may be stale, so read-modify-write cycles must go through
207
+ * the remote DB to avoid conflicts).
208
+ */
209
+ constructor(id: string, userLookup: () => Promise<UserDBInterface>, localDB?: PouchDB.Database);
164
210
  getCourseID(): string;
165
211
  getCourseInfo(): Promise<CourseInfo>;
166
212
  getInexperiencedCards(limit?: number): Promise<{
@@ -197,6 +243,7 @@ declare class CourseDB implements CourseDBInterface {
197
243
  updateCardElo(cardId: string, elo: CourseElo): Promise<PouchDB.Core.Response>;
198
244
  getAppliedTags(cardId: string): Promise<PouchDB.Query.Response<TagStub>>;
199
245
  getAppliedTagsBatch(cardIds: string[]): Promise<Map<string, string[]>>;
246
+ getAllCardIds(): Promise<string[]>;
200
247
  addTagToCard(cardId: string, tagId: string, updateELO?: boolean): Promise<PouchDB.Core.Response>;
201
248
  removeTagFromCard(cardId: string, tagId: string): Promise<PouchDB.Core.Response>;
202
249
  createTag(name: string, author: string): Promise<PouchDB.Core.Response>;
@@ -268,6 +315,111 @@ declare function getAppliedTags(id_course: string, id_card: string): Promise<Pou
268
315
  declare function updateCardElo(courseID: string, cardID: string, elo: CourseElo): Promise<PouchDB.Core.Response | undefined>;
269
316
  declare function updateCredentialledCourseConfig(courseID: string, config: CourseConfig$1): Promise<PouchDB.Core.Response>;
270
317
 
318
+ /**
319
+ * Sync state for a single course database.
320
+ */
321
+ type CourseSyncState = 'not-started' | 'checking-config' | 'syncing' | 'warming-views' | 'ready' | 'disabled' | 'error';
322
+ /**
323
+ * Detailed sync status for observability.
324
+ */
325
+ interface CourseSyncStatus {
326
+ state: CourseSyncState;
327
+ /** Number of documents replicated (set after sync completes) */
328
+ docsReplicated?: number;
329
+ /** Total replication time in ms */
330
+ syncTimeMs?: number;
331
+ /** View warming time in ms */
332
+ viewWarmTimeMs?: number;
333
+ /** Error message if state is 'error' */
334
+ error?: string;
335
+ }
336
+ /**
337
+ * Service that manages local PouchDB replicas of course databases.
338
+ *
339
+ * Usage:
340
+ * ```typescript
341
+ * const syncService = CourseSyncService.getInstance();
342
+ *
343
+ * // Trigger sync (typically on app load / pre-session)
344
+ * await syncService.ensureSynced(courseId);
345
+ *
346
+ * // Get local DB for reads (returns null if sync not ready/enabled)
347
+ * const localDB = syncService.getLocalDB(courseId);
348
+ * ```
349
+ *
350
+ * The service is a singleton — course sync state is shared across the app.
351
+ */
352
+ declare class CourseSyncService {
353
+ private static instance;
354
+ private entries;
355
+ private constructor();
356
+ static getInstance(): CourseSyncService;
357
+ /**
358
+ * Reset the singleton (for testing).
359
+ */
360
+ static resetInstance(): void;
361
+ /**
362
+ * Ensure a course's local replica is synced.
363
+ *
364
+ * On first call for a course:
365
+ * 1. Fetches CourseConfig from remote to check localSync.enabled
366
+ * 2. If enabled, performs one-shot replication remote → local
367
+ * 3. Pre-warms PouchDB view indices (elo, getTags)
368
+ *
369
+ * On subsequent calls: returns immediately if already synced, or awaits
370
+ * the in-flight sync if one is in progress.
371
+ *
372
+ * Safe to call multiple times — concurrent calls coalesce to one sync.
373
+ *
374
+ * @param courseId - The course to sync
375
+ * @param forceEnabled - Skip the CourseConfig check and sync regardless.
376
+ * Useful when the caller already knows local sync is desired (e.g.,
377
+ * LettersPractice hardcodes this).
378
+ */
379
+ ensureSynced(courseId: string, forceEnabled?: boolean): Promise<void>;
380
+ /**
381
+ * Get the local PouchDB for a course, or null if not available.
382
+ *
383
+ * Returns null when:
384
+ * - Local sync is not enabled for this course
385
+ * - Sync has not been triggered yet
386
+ * - Sync is still in progress
387
+ * - Sync failed
388
+ */
389
+ getLocalDB(courseId: string): PouchDB.Database | null;
390
+ /**
391
+ * Check whether a course has a ready local replica.
392
+ */
393
+ isReady(courseId: string): boolean;
394
+ /**
395
+ * Get detailed sync status for a course.
396
+ */
397
+ getStatus(courseId: string): CourseSyncStatus;
398
+ private performSync;
399
+ /**
400
+ * Check CourseConfig.localSync.enabled on the remote DB.
401
+ */
402
+ private checkLocalSyncEnabled;
403
+ /**
404
+ * One-shot replication from remote to local.
405
+ */
406
+ private replicate;
407
+ /**
408
+ * Pre-warm PouchDB view indices by running a minimal query against each
409
+ * design doc. This forces PouchDB to build the MapReduce index now
410
+ * (during a loading phase) rather than on first pipeline query.
411
+ */
412
+ private warmViewIndices;
413
+ /**
414
+ * Get a remote PouchDB handle for a course.
415
+ */
416
+ private getRemoteDB;
417
+ /**
418
+ * Local DB naming convention.
419
+ */
420
+ private localDBName;
421
+ }
422
+
271
423
  /**
272
424
  * Sync strategy that implements full CouchDB remote synchronization
273
425
  * Handles account creation, authentication, and live sync with remote CouchDB server
@@ -339,4 +491,4 @@ declare function getStartAndEndKeys(key: string): {
339
491
  endkey: string;
340
492
  };
341
493
 
342
- export { AdminDB, CLASSROOM_CONFIG, ClassroomLookupDB, type ClassroomMessage, CouchDBSyncStrategy, CourseDB, CoursesDB, REVIEW_TIME_FORMAT, StudentClassroomDB, StudyContentSource, StudySessionItem, TeacherClassroomDB, addNote55, addTagToCard, createPouchDBConfig, createTag, deleteTag, filterAllDocsByPrefix, getAncestorTagIDs, getAppliedTags, getChildTagStubs, getClassroomConfig, getClassroomDB, getCouchUserDB, getCourseDB, getCourseDataShapes, getCourseDoc, getCourseDocs, getCourseQuestionTypes, getCourseTagStubs, getCredentialledCourseConfig, getCredentialledDataShapes, getLatestVersion, getRandomCards, getStartAndEndKeys, getTag, getTagID, hexEncode, localUserDB, removeTagFromCard, scheduleCardReview, updateCardElo, updateCredentialledCourseConfig, updateGuestAccountExpirationDate, updateTag, usernameIsAvailable };
494
+ export { AdminDB, CLASSROOM_CONFIG, ClassroomLookupDB, type ClassroomMessage, CouchDBSyncStrategy, CourseDB, CourseSyncService, type CourseSyncState, type CourseSyncStatus, CoursesDB, REVIEW_TIME_FORMAT, StudentClassroomDB, StudyContentSource, StudySessionItem, TeacherClassroomDB, addNote55, addTagToCard, createPouchDBConfig, createTag, deleteTag, filterAllDocsByPrefix, getAncestorTagIDs, getAppliedTags, getChildTagStubs, getClassroomConfig, getClassroomDB, getCouchUserDB, getCourseDB, getCourseDataShapes, getCourseDoc, getCourseDocs, getCourseQuestionTypes, getCourseTagStubs, getCredentialledCourseConfig, getCredentialledDataShapes, getLatestVersion, getRandomCards, getStartAndEndKeys, getTag, getTagID, hexEncode, localUserDB, removeTagFromCard, scheduleCardReview, updateCardElo, updateCredentialledCourseConfig, updateGuestAccountExpirationDate, updateTag, usernameIsAvailable };
@@ -1,7 +1,7 @@
1
1
  import { T as TagStub, a as Tag, S as SkuilderCourseData, Q as QualifiedCardID } from '../../types-legacy-JXDxinpU.js';
2
2
  import { Moment } from 'moment';
3
- import { A as AdminDBInterface, g as AssignedContent, h as StudyContentSource, i as StudentClassroomDBInterface, U as UserDBInterface, W as WeightedCard, T as TeacherClassroomDBInterface, b as CoursesDBInterface, C as CourseDBInterface, d as CourseInfo, D as DataLayerResult, e as ContentNavigationStrategyData, f as ContentNavigator, S as StudySessionItem, j as ScheduledCard } from '../../contentSource-BmnmvH8C.js';
4
- export { q as ContentSourceID, k as StudySessionFailedItem, l as StudySessionFailedNewItem, m as StudySessionFailedReviewItem, n as StudySessionNewItem, o as StudySessionReviewItem, r as getStudySource, p as isReview } from '../../contentSource-BmnmvH8C.js';
3
+ import { A as AdminDBInterface, g as AssignedContent, h as StudyContentSource, i as StudentClassroomDBInterface, U as UserDBInterface, W as WeightedCard, T as TeacherClassroomDBInterface, b as CoursesDBInterface, C as CourseDBInterface, d as CourseInfo, D as DataLayerResult, e as ContentNavigationStrategyData, f as ContentNavigator, S as StudySessionItem, j as ScheduledCard } from '../../contentSource-Bdwkvqa8.js';
4
+ export { q as ContentSourceID, k as StudySessionFailedItem, l as StudySessionFailedNewItem, m as StudySessionFailedReviewItem, n as StudySessionNewItem, o as StudySessionReviewItem, r as getStudySource, p as isReview } from '../../contentSource-Bdwkvqa8.js';
5
5
  import * as _vue_skuilder_common from '@vue-skuilder/common';
6
6
  import { ClassroomConfig, DataShape, CourseElo, CourseConfig as CourseConfig$1 } from '@vue-skuilder/common';
7
7
  import { S as SyncStrategy, A as AccountCreationResult, a as AuthenticationResult } from '../../SyncStrategy-CyATpyLQ.js';
@@ -45,6 +45,27 @@ interface CourseConfig {
45
45
  questionTypes: QuestionType55[];
46
46
  disambiguator?: string;
47
47
  orchestration?: CourseOrchestrationConfig;
48
+ /**
49
+ * Opt-in client-side replication of the course database.
50
+ *
51
+ * When enabled, the client replicates the course DB to a local PouchDB
52
+ * instance on first visit (full one-shot sync) and performs incremental
53
+ * sync on subsequent visits. Pipeline scoring, tag hydration, and card
54
+ * lookup then run against the local replica — eliminating network round
55
+ * trips from the study-session hot path.
56
+ *
57
+ * **Read/write split:** The local DB is a read-only snapshot. All writes
58
+ * (card ELO updates, tag mutations, etc.) continue to target the remote
59
+ * CouchDB. This avoids propagating per-interaction ELO noise to every
60
+ * syncing client — the remote DB aggregates writes from all users, and
61
+ * each client's local snapshot is refreshed on the next page load.
62
+ *
63
+ * Defaults to `undefined` (disabled). Courses with small, relatively
64
+ * static content databases (e.g. < 50 MB) are good candidates.
65
+ */
66
+ localSync?: {
67
+ enabled: boolean;
68
+ };
48
69
  }
49
70
 
50
71
  declare class AdminDB implements AdminDBInterface {
@@ -156,11 +177,36 @@ declare class CoursesDB implements CoursesDBInterface {
156
177
  disambiguateCourse(courseId: string, disambiguator: string): Promise<void>;
157
178
  }
158
179
  declare class CourseDB implements CourseDBInterface {
180
+ /**
181
+ * Primary database handle used for all **read** operations (queries, gets).
182
+ *
183
+ * When local sync is active, this points to the local PouchDB replica for
184
+ * fast, network-free reads. Otherwise it points to the remote CouchDB.
185
+ */
159
186
  private db;
187
+ /**
188
+ * Remote database handle used for all **write** operations.
189
+ *
190
+ * Always points to the remote CouchDB so that writes (ELO updates, tag
191
+ * mutations, admin operations) aggregate on the server. The local replica
192
+ * is a read-only snapshot that refreshes on the next page load.
193
+ *
194
+ * When local sync is NOT active, this is the same instance as `this.db`.
195
+ */
196
+ private remoteDB;
160
197
  private id;
161
198
  private _getCurrentUser;
162
199
  private updateQueue;
163
- constructor(id: string, userLookup: () => Promise<UserDBInterface>);
200
+ /**
201
+ * @param id - Course ID
202
+ * @param userLookup - Async function returning the current user DB
203
+ * @param localDB - Optional local PouchDB replica for reads. When provided,
204
+ * `this.db` uses the local replica and `this.remoteDB` stays remote.
205
+ * The UpdateQueue reads from remote and writes to remote (local `_rev`
206
+ * values may be stale, so read-modify-write cycles must go through
207
+ * the remote DB to avoid conflicts).
208
+ */
209
+ constructor(id: string, userLookup: () => Promise<UserDBInterface>, localDB?: PouchDB.Database);
164
210
  getCourseID(): string;
165
211
  getCourseInfo(): Promise<CourseInfo>;
166
212
  getInexperiencedCards(limit?: number): Promise<{
@@ -197,6 +243,7 @@ declare class CourseDB implements CourseDBInterface {
197
243
  updateCardElo(cardId: string, elo: CourseElo): Promise<PouchDB.Core.Response>;
198
244
  getAppliedTags(cardId: string): Promise<PouchDB.Query.Response<TagStub>>;
199
245
  getAppliedTagsBatch(cardIds: string[]): Promise<Map<string, string[]>>;
246
+ getAllCardIds(): Promise<string[]>;
200
247
  addTagToCard(cardId: string, tagId: string, updateELO?: boolean): Promise<PouchDB.Core.Response>;
201
248
  removeTagFromCard(cardId: string, tagId: string): Promise<PouchDB.Core.Response>;
202
249
  createTag(name: string, author: string): Promise<PouchDB.Core.Response>;
@@ -268,6 +315,111 @@ declare function getAppliedTags(id_course: string, id_card: string): Promise<Pou
268
315
  declare function updateCardElo(courseID: string, cardID: string, elo: CourseElo): Promise<PouchDB.Core.Response | undefined>;
269
316
  declare function updateCredentialledCourseConfig(courseID: string, config: CourseConfig$1): Promise<PouchDB.Core.Response>;
270
317
 
318
+ /**
319
+ * Sync state for a single course database.
320
+ */
321
+ type CourseSyncState = 'not-started' | 'checking-config' | 'syncing' | 'warming-views' | 'ready' | 'disabled' | 'error';
322
+ /**
323
+ * Detailed sync status for observability.
324
+ */
325
+ interface CourseSyncStatus {
326
+ state: CourseSyncState;
327
+ /** Number of documents replicated (set after sync completes) */
328
+ docsReplicated?: number;
329
+ /** Total replication time in ms */
330
+ syncTimeMs?: number;
331
+ /** View warming time in ms */
332
+ viewWarmTimeMs?: number;
333
+ /** Error message if state is 'error' */
334
+ error?: string;
335
+ }
336
+ /**
337
+ * Service that manages local PouchDB replicas of course databases.
338
+ *
339
+ * Usage:
340
+ * ```typescript
341
+ * const syncService = CourseSyncService.getInstance();
342
+ *
343
+ * // Trigger sync (typically on app load / pre-session)
344
+ * await syncService.ensureSynced(courseId);
345
+ *
346
+ * // Get local DB for reads (returns null if sync not ready/enabled)
347
+ * const localDB = syncService.getLocalDB(courseId);
348
+ * ```
349
+ *
350
+ * The service is a singleton — course sync state is shared across the app.
351
+ */
352
+ declare class CourseSyncService {
353
+ private static instance;
354
+ private entries;
355
+ private constructor();
356
+ static getInstance(): CourseSyncService;
357
+ /**
358
+ * Reset the singleton (for testing).
359
+ */
360
+ static resetInstance(): void;
361
+ /**
362
+ * Ensure a course's local replica is synced.
363
+ *
364
+ * On first call for a course:
365
+ * 1. Fetches CourseConfig from remote to check localSync.enabled
366
+ * 2. If enabled, performs one-shot replication remote → local
367
+ * 3. Pre-warms PouchDB view indices (elo, getTags)
368
+ *
369
+ * On subsequent calls: returns immediately if already synced, or awaits
370
+ * the in-flight sync if one is in progress.
371
+ *
372
+ * Safe to call multiple times — concurrent calls coalesce to one sync.
373
+ *
374
+ * @param courseId - The course to sync
375
+ * @param forceEnabled - Skip the CourseConfig check and sync regardless.
376
+ * Useful when the caller already knows local sync is desired (e.g.,
377
+ * LettersPractice hardcodes this).
378
+ */
379
+ ensureSynced(courseId: string, forceEnabled?: boolean): Promise<void>;
380
+ /**
381
+ * Get the local PouchDB for a course, or null if not available.
382
+ *
383
+ * Returns null when:
384
+ * - Local sync is not enabled for this course
385
+ * - Sync has not been triggered yet
386
+ * - Sync is still in progress
387
+ * - Sync failed
388
+ */
389
+ getLocalDB(courseId: string): PouchDB.Database | null;
390
+ /**
391
+ * Check whether a course has a ready local replica.
392
+ */
393
+ isReady(courseId: string): boolean;
394
+ /**
395
+ * Get detailed sync status for a course.
396
+ */
397
+ getStatus(courseId: string): CourseSyncStatus;
398
+ private performSync;
399
+ /**
400
+ * Check CourseConfig.localSync.enabled on the remote DB.
401
+ */
402
+ private checkLocalSyncEnabled;
403
+ /**
404
+ * One-shot replication from remote to local.
405
+ */
406
+ private replicate;
407
+ /**
408
+ * Pre-warm PouchDB view indices by running a minimal query against each
409
+ * design doc. This forces PouchDB to build the MapReduce index now
410
+ * (during a loading phase) rather than on first pipeline query.
411
+ */
412
+ private warmViewIndices;
413
+ /**
414
+ * Get a remote PouchDB handle for a course.
415
+ */
416
+ private getRemoteDB;
417
+ /**
418
+ * Local DB naming convention.
419
+ */
420
+ private localDBName;
421
+ }
422
+
271
423
  /**
272
424
  * Sync strategy that implements full CouchDB remote synchronization
273
425
  * Handles account creation, authentication, and live sync with remote CouchDB server
@@ -339,4 +491,4 @@ declare function getStartAndEndKeys(key: string): {
339
491
  endkey: string;
340
492
  };
341
493
 
342
- export { AdminDB, CLASSROOM_CONFIG, ClassroomLookupDB, type ClassroomMessage, CouchDBSyncStrategy, CourseDB, CoursesDB, REVIEW_TIME_FORMAT, StudentClassroomDB, StudyContentSource, StudySessionItem, TeacherClassroomDB, addNote55, addTagToCard, createPouchDBConfig, createTag, deleteTag, filterAllDocsByPrefix, getAncestorTagIDs, getAppliedTags, getChildTagStubs, getClassroomConfig, getClassroomDB, getCouchUserDB, getCourseDB, getCourseDataShapes, getCourseDoc, getCourseDocs, getCourseQuestionTypes, getCourseTagStubs, getCredentialledCourseConfig, getCredentialledDataShapes, getLatestVersion, getRandomCards, getStartAndEndKeys, getTag, getTagID, hexEncode, localUserDB, removeTagFromCard, scheduleCardReview, updateCardElo, updateCredentialledCourseConfig, updateGuestAccountExpirationDate, updateTag, usernameIsAvailable };
494
+ export { AdminDB, CLASSROOM_CONFIG, ClassroomLookupDB, type ClassroomMessage, CouchDBSyncStrategy, CourseDB, CourseSyncService, type CourseSyncState, type CourseSyncStatus, CoursesDB, REVIEW_TIME_FORMAT, StudentClassroomDB, StudyContentSource, StudySessionItem, TeacherClassroomDB, addNote55, addTagToCard, createPouchDBConfig, createTag, deleteTag, filterAllDocsByPrefix, getAncestorTagIDs, getAppliedTags, getChildTagStubs, getClassroomConfig, getClassroomDB, getCouchUserDB, getCourseDB, getCourseDataShapes, getCourseDoc, getCourseDocs, getCourseQuestionTypes, getCourseTagStubs, getCredentialledCourseConfig, getCredentialledDataShapes, getLatestVersion, getRandomCards, getStartAndEndKeys, getTag, getTagID, hexEncode, localUserDB, removeTagFromCard, scheduleCardReview, updateCardElo, updateCredentialledCourseConfig, updateGuestAccountExpirationDate, updateTag, usernameIsAvailable };