@vue-skuilder/db 0.1.32-b → 0.1.32-c

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/core/index.d.cts +16 -12
  2. package/dist/core/index.d.ts +16 -12
  3. package/dist/core/index.js +2262 -223
  4. package/dist/core/index.js.map +1 -1
  5. package/dist/core/index.mjs +2239 -196
  6. package/dist/core/index.mjs.map +1 -1
  7. package/dist/{contentSource-Bdwkvqa8.d.ts → dataLayerProvider-BAn-LRh5.d.ts} +626 -83
  8. package/dist/{contentSource-DF1nUbPQ.d.cts → dataLayerProvider-BJqBlMIl.d.cts} +626 -83
  9. package/dist/impl/couch/index.d.cts +17 -4
  10. package/dist/impl/couch/index.d.ts +17 -4
  11. package/dist/impl/couch/index.js +2306 -220
  12. package/dist/impl/couch/index.js.map +1 -1
  13. package/dist/impl/couch/index.mjs +2294 -204
  14. package/dist/impl/couch/index.mjs.map +1 -1
  15. package/dist/impl/static/index.d.cts +4 -5
  16. package/dist/impl/static/index.d.ts +4 -5
  17. package/dist/impl/static/index.js +2266 -227
  18. package/dist/impl/static/index.js.map +1 -1
  19. package/dist/impl/static/index.mjs +2251 -208
  20. package/dist/impl/static/index.mjs.map +1 -1
  21. package/dist/{index-BWvO-_rJ.d.ts → index-X6wHrURm.d.ts} +1 -1
  22. package/dist/{index-Ba7hYbHj.d.cts → index-m8MMGxxR.d.cts} +1 -1
  23. package/dist/index.d.cts +9 -444
  24. package/dist/index.d.ts +9 -444
  25. package/dist/index.js +9637 -8931
  26. package/dist/index.js.map +1 -1
  27. package/dist/index.mjs +9539 -8833
  28. package/dist/index.mjs.map +1 -1
  29. package/dist/{types-CJrLM1Ew.d.ts → types-DZ5dUqbL.d.ts} +1 -1
  30. package/dist/{types-W8n-B6HG.d.cts → types-ZL8tOPQZ.d.cts} +1 -1
  31. package/dist/{types-legacy-JXDxinpU.d.cts → types-legacy-C7r0T4OV.d.cts} +1 -1
  32. package/dist/{types-legacy-JXDxinpU.d.ts → types-legacy-C7r0T4OV.d.ts} +1 -1
  33. package/dist/util/packer/index.d.cts +3 -3
  34. package/dist/util/packer/index.d.ts +3 -3
  35. package/docs/navigators-architecture.md +2 -2
  36. package/package.json +2 -2
  37. package/src/core/interfaces/contentSource.ts +2 -1
  38. package/src/core/navigators/Pipeline.ts +47 -29
  39. package/src/core/navigators/PipelineDebugger.ts +49 -1
  40. package/src/core/navigators/filters/hierarchyDefinition.ts +88 -5
  41. package/src/core/navigators/generators/prescribed.ts +618 -43
  42. package/src/core/navigators/index.ts +2 -1
  43. package/src/impl/couch/CourseSyncService.ts +72 -4
  44. package/src/impl/couch/courseDB.ts +3 -2
  45. package/src/impl/static/courseDB.ts +3 -2
  46. package/src/study/SessionController.ts +79 -9
  47. package/src/study/services/EloService.ts +22 -3
  48. package/src/study/services/ResponseProcessor.ts +7 -3
  49. package/dist/dataLayerProvider-BKmVoyJR.d.ts +0 -67
  50. package/dist/dataLayerProvider-BQdfJuBN.d.cts +0 -67
@@ -1,4 +1,5 @@
1
1
  import { StudyContentSource, UserDBInterface, CourseDBInterface } from '..';
2
+ import type { ReplanHints } from '@db/study/SessionController';
2
3
 
3
4
  // Re-export filter types
4
5
  export type { CardFilter, FilterContext, CardFilterFactory } from './filters/types';
@@ -633,7 +634,7 @@ export abstract class ContentNavigator implements StudyContentSource {
633
634
  * Set ephemeral hints for the next pipeline run.
634
635
  * No-op for non-Pipeline navigators. Pipeline overrides this.
635
636
  */
636
- setEphemeralHints(_hints: Record<string, unknown>): void {
637
+ setEphemeralHints(_hints: ReplanHints): void {
637
638
  // no-op — only Pipeline implements this
638
639
  }
639
640
  }
@@ -137,9 +137,26 @@ export class CourseSyncService {
137
137
  async ensureSynced(courseId: string, forceEnabled?: boolean): Promise<void> {
138
138
  const existing = this.entries.get(courseId);
139
139
 
140
- // Already synced
141
- if (existing?.status.state === 'ready') {
142
- return;
140
+ // Already synced — but check if the remote DB has been recreated
141
+ // (e.g., after a dev reseed). The seed script writes a `db-epoch`
142
+ // doc; if the remote epoch differs from our local copy, the local
143
+ // replica is stale and must be destroyed before re-syncing.
144
+ if (existing?.status.state === 'ready' && existing.localDB) {
145
+ const stale = await this.isLocalEpochStale(courseId, existing.localDB);
146
+ if (!stale) {
147
+ return;
148
+ }
149
+ logger.info(
150
+ `[CourseSyncService] Remote DB epoch changed for course ${courseId} — destroying stale local replica`
151
+ );
152
+ try {
153
+ await existing.localDB.destroy();
154
+ } catch {
155
+ // Ignore cleanup errors
156
+ }
157
+ existing.localDB = null;
158
+ existing.readyPromise = null;
159
+ // Fall through to start a fresh sync
143
160
  }
144
161
 
145
162
  // Already disabled
@@ -224,7 +241,23 @@ export class CourseSyncService {
224
241
  // Step 2: Create local PouchDB and replicate
225
242
  entry.status = { state: 'syncing' };
226
243
  const localDBName = this.localDBName(courseId);
227
- const localDB = new pouch(localDBName);
244
+ let localDB = new pouch(localDBName);
245
+
246
+ // Check for stale local replica before replicating. If the remote DB
247
+ // was wiped and recreated (e.g., `yarn db:seed`), the local PouchDB
248
+ // has documents at rev 1-oldHash while the remote has 1-newHash for
249
+ // the same _ids. PouchDB replication treats this as a conflict rather
250
+ // than an update, and the stale revision can win — causing partial or
251
+ // incorrect data. Destroying the stale local DB avoids this entirely.
252
+ const stale = await this.isLocalEpochStale(courseId, localDB);
253
+ if (stale) {
254
+ logger.info(
255
+ `[CourseSyncService] Stale local DB detected for course ${courseId} — destroying before sync`
256
+ );
257
+ await localDB.destroy();
258
+ localDB = new pouch(localDBName);
259
+ }
260
+
228
261
  entry.localDB = localDB;
229
262
 
230
263
  const remoteDB = this.getRemoteDB(courseId);
@@ -340,6 +373,41 @@ export class CourseSyncService {
340
373
  }
341
374
  }
342
375
 
376
+ /**
377
+ * Check whether the local replica's `db-epoch` doc matches the remote.
378
+ *
379
+ * The seed script (and optionally upload-cards) writes a `db-epoch`
380
+ * document with a numeric timestamp. If the remote epoch differs from
381
+ * the local copy, the remote DB was recreated (e.g., `yarn db:seed`)
382
+ * and the local PouchDB is stale.
383
+ *
384
+ * Returns `true` if stale (epoch mismatch or remote has epoch but local
385
+ * doesn't). Returns `false` (not stale) if epochs match, or if the
386
+ * remote doesn't have an epoch doc at all (backwards compat).
387
+ */
388
+ private async isLocalEpochStale(
389
+ courseId: string,
390
+ localDB: PouchDB.Database
391
+ ): Promise<boolean> {
392
+ try {
393
+ const remoteDB = this.getRemoteDB(courseId);
394
+ const remoteEpoch = await remoteDB.get<{ epoch: number }>('db-epoch');
395
+
396
+ let localEpoch: { epoch: number } | null = null;
397
+ try {
398
+ localEpoch = await localDB.get<{ epoch: number }>('db-epoch');
399
+ } catch {
400
+ // Local doesn't have the epoch doc — stale
401
+ return true;
402
+ }
403
+
404
+ return remoteEpoch.epoch !== localEpoch.epoch;
405
+ } catch {
406
+ // Remote doesn't have db-epoch — no epoch tracking, not stale
407
+ return false;
408
+ }
409
+ }
410
+
343
411
  /**
344
412
  * Get a remote PouchDB handle for a course.
345
413
  */
@@ -1,4 +1,5 @@
1
1
  import { CourseDBInterface, CourseInfo, CoursesDBInterface, UserDBInterface } from '@db/core';
2
+ import type { ReplanHints } from '@db/study/SessionController';
2
3
  import {
3
4
  CourseConfig,
4
5
  CourseElo,
@@ -650,9 +651,9 @@ above:\n${above.rows.map((r) => `\t${r.id}-${r.key}\n`)}`;
650
651
  * @param limit - Maximum number of cards to return
651
652
  * @returns Cards sorted by score descending
652
653
  */
653
- private _pendingHints: Record<string, unknown> | null = null;
654
+ private _pendingHints: ReplanHints | null = null;
654
655
 
655
- public setEphemeralHints(hints: Record<string, unknown>): void {
656
+ public setEphemeralHints(hints: ReplanHints): void {
656
657
  this._pendingHints = hints;
657
658
  }
658
659
 
@@ -6,6 +6,7 @@ import {
6
6
  CourseInfo,
7
7
  StudySessionItem,
8
8
  } from '../../core/interfaces';
9
+ import type { ReplanHints } from '@db/study/SessionController';
9
10
  import { StaticDataUnpacker } from './StaticDataUnpacker';
10
11
  import { StaticCourseManifest } from '../../util/packer/types';
11
12
  import { CourseConfig, CourseElo, DataShape, Status } from '@vue-skuilder/common';
@@ -443,9 +444,9 @@ export class StaticCourseDB implements CourseDBInterface {
443
444
 
444
445
  // Study Content Source implementation
445
446
 
446
- private _pendingHints: Record<string, unknown> | null = null;
447
+ private _pendingHints: ReplanHints | null = null;
447
448
 
448
- setEphemeralHints(hints: Record<string, unknown>): void {
449
+ setEphemeralHints(hints: ReplanHints): void {
449
450
  this._pendingHints = hints;
450
451
  }
451
452
 
@@ -20,6 +20,33 @@ import { SourceMixer, QuotaRoundRobinMixer, SourceBatch } from './SourceMixer';
20
20
  import { captureMixerRun } from './MixerDebugger';
21
21
  import { startSessionTracking, recordCardPresentation, snapshotQueues, endSessionTracking } from './SessionDebugger';
22
22
 
23
+ /**
24
+ * Typed ephemeral pipeline hints for a single run.
25
+ * All fields are optional. Tag/card patterns support `*` wildcards.
26
+ *
27
+ * Previously defined in Pipeline.ts; moved here so it's co-exported
28
+ * with ReplanOptions from the public `@vue-skuilder/db` surface.
29
+ */
30
+ export interface ReplanHints {
31
+ /** Multiply scores for cards matching these tag patterns. */
32
+ boostTags?: Record<string, number>;
33
+ /** Multiply scores for these specific card IDs (glob patterns). */
34
+ boostCards?: Record<string, number>;
35
+ /** Cards matching these tag patterns MUST appear in results. */
36
+ requireTags?: string[];
37
+ /** These specific card IDs MUST appear in results. */
38
+ requireCards?: string[];
39
+ /** Remove cards matching these tag patterns from results. */
40
+ excludeTags?: string[];
41
+ /** Remove these specific card IDs from results. */
42
+ excludeCards?: string[];
43
+ /**
44
+ * Debugging label threaded from the replan requester.
45
+ * Prefixed with `_` to signal it's metadata, not a scoring hint.
46
+ */
47
+ _label?: string;
48
+ }
49
+
23
50
  /**
24
51
  * Options for requesting a mid-session replan.
25
52
  *
@@ -29,7 +56,7 @@ import { startSessionTracking, recordCardPresentation, snapshotQueues, endSessio
29
56
  */
30
57
  export interface ReplanOptions {
31
58
  /** Scoring hints forwarded to the pipeline (boost/exclude/require). */
32
- hints?: Record<string, unknown>;
59
+ hints?: ReplanHints;
33
60
  /**
34
61
  * Maximum number of new cards to return from the pipeline.
35
62
  * Default: 20 (the standard session batch size).
@@ -41,6 +68,13 @@ export interface ReplanOptions {
41
68
  * - `'merge'`: insert new cards at the front, keeping existing cards.
42
69
  */
43
70
  mode?: 'replace' | 'merge';
71
+ /**
72
+ * Guarantee that at least this many cards will be served after the
73
+ * replan, even if the session timer has expired. Prevents intro cards
74
+ * from surfacing at the end of a session with zero follow-up exercise.
75
+ * Decremented on each card draw while active.
76
+ */
77
+ minFollowUpCards?: number;
44
78
  /**
45
79
  * Human-readable label for debugging / provenance.
46
80
  * Appears in console logs and in card provenance entries created
@@ -156,12 +190,22 @@ export class SessionController<TView = unknown> extends Loggable {
156
190
  */
157
191
  private _depletionReplanAttempted: boolean = false;
158
192
 
193
+ /**
194
+ * When > 0, the session timer cannot end the session. Decremented on
195
+ * each nextCard() draw. Set by replans that include `minFollowUpCards`.
196
+ */
197
+ private _minCardsGuarantee: number = 0;
198
+
159
199
  private startTime: Date;
160
200
  private endTime: Date;
161
201
  private _secondsRemaining: number;
162
202
  public get secondsRemaining(): number {
163
203
  return this._secondsRemaining;
164
204
  }
205
+ /** True when a card guarantee is active, preventing timer-based session end. */
206
+ public get hasCardGuarantee(): boolean {
207
+ return this._minCardsGuarantee > 0;
208
+ }
165
209
  public get report(): string {
166
210
  const reviewCount = this.reviewQ.dequeueCount;
167
211
  const newCount = this.newQ.dequeueCount;
@@ -316,7 +360,7 @@ export class SessionController<TView = unknown> extends Loggable {
316
360
  * Typical trigger: application-level code (e.g. after a GPC intro completion)
317
361
  * calls this to ensure newly-unlocked content appears in the session.
318
362
  */
319
- public async requestReplan(options?: ReplanOptions | Record<string, unknown>): Promise<void> {
363
+ public async requestReplan(options?: ReplanOptions | ReplanHints): Promise<void> {
320
364
  // Normalise: bare hints object (legacy callers) → ReplanOptions wrapper
321
365
  const opts = this.normalizeReplanOptions(options);
322
366
 
@@ -331,6 +375,19 @@ export class SessionController<TView = unknown> extends Loggable {
331
375
  return this._replanPromise;
332
376
  }
333
377
 
378
+ // Auto-exclude the currently-displayed card so the replan doesn't
379
+ // surface it again (avoids showing the same card twice in a row).
380
+ if (this._currentCard?.item.cardID) {
381
+ const currentId = this._currentCard.item.cardID;
382
+ if (!opts.hints) opts.hints = {};
383
+ const hints = opts.hints;
384
+ const excludeCards = hints.excludeCards ?? [];
385
+ if (!excludeCards.includes(currentId)) {
386
+ excludeCards.push(currentId);
387
+ }
388
+ hints.excludeCards = excludeCards;
389
+ }
390
+
334
391
  // Forward hints to all sources (CourseDB stashes them, Pipeline consumes them)
335
392
  if (opts.hints) {
336
393
  // Thread label into hints so Pipeline can attach it to provenance
@@ -348,6 +405,12 @@ export class SessionController<TView = unknown> extends Loggable {
348
405
  ` (limit: ${opts.limit ?? 'default'}, mode: ${opts.mode ?? 'replace'}` +
349
406
  `${opts.hints ? ', with hints' : ''})`
350
407
  );
408
+ // Update card guarantee if requested
409
+ if (opts.minFollowUpCards !== undefined && opts.minFollowUpCards > 0) {
410
+ this._minCardsGuarantee = Math.max(this._minCardsGuarantee, opts.minFollowUpCards);
411
+ this.log(`[Replan] Card guarantee set to ${this._minCardsGuarantee}`);
412
+ }
413
+
351
414
  this._replanPromise = this._executeReplan(opts);
352
415
 
353
416
  try {
@@ -364,19 +427,19 @@ export class SessionController<TView = unknown> extends Loggable {
364
427
  * the presence of ReplanOptions-specific keys.
365
428
  */
366
429
  private normalizeReplanOptions(
367
- input?: ReplanOptions | Record<string, unknown>
430
+ input?: ReplanOptions | ReplanHints
368
431
  ): ReplanOptions {
369
432
  if (!input) return {};
370
433
 
371
434
  // If the input has any ReplanOptions-specific key, treat it as ReplanOptions
372
- const replanKeys = ['hints', 'limit', 'mode', 'label'];
435
+ const replanKeys = ['hints', 'limit', 'mode', 'label', 'minFollowUpCards'];
373
436
  const inputKeys = Object.keys(input);
374
437
  if (inputKeys.some((k) => replanKeys.includes(k))) {
375
438
  return input as ReplanOptions;
376
439
  }
377
440
 
378
441
  // Otherwise treat as legacy bare-hints object
379
- return { hints: input as Record<string, unknown> };
442
+ return { hints: input as ReplanHints };
380
443
  }
381
444
 
382
445
  /** Minimum well-indicated cards before an additive retry is attempted */
@@ -509,6 +572,7 @@ export class SessionController<TView = unknown> extends Loggable {
509
572
  inProgress: this._replanPromise !== null,
510
573
  suppressQualityReplan: this._suppressQualityReplan,
511
574
  defaultBatchLimit: this._defaultBatchLimit,
575
+ minCardsGuarantee: this._minCardsGuarantee,
512
576
  },
513
577
  };
514
578
  }
@@ -709,13 +773,13 @@ export class SessionController<TView = unknown> extends Loggable {
709
773
  return null;
710
774
  }
711
775
 
712
- if (this._secondsRemaining < 2 && this.failedQ.length === 0) {
776
+ if (this._secondsRemaining < 2 && this.failedQ.length === 0 && this._minCardsGuarantee <= 0) {
713
777
  // session is over!
714
778
  return null;
715
779
  }
716
780
 
717
- // If timer expired, only return failed cards
718
- if (this._secondsRemaining <= 0) {
781
+ // If timer expired, only return failed cards (unless card guarantee active)
782
+ if (this._secondsRemaining <= 0 && this._minCardsGuarantee <= 0) {
719
783
  if (this.failedQ.length > 0) {
720
784
  return this.failedQ.peek(0);
721
785
  } else {
@@ -777,6 +841,12 @@ export class SessionController<TView = unknown> extends Loggable {
777
841
  // dismiss (or sort to failedQ) the current card
778
842
  this.dismissCurrentCard(action);
779
843
 
844
+ // Decrement card guarantee counter
845
+ if (this._minCardsGuarantee > 0) {
846
+ this._minCardsGuarantee--;
847
+ this.log(`[CardGuarantee] ${this._minCardsGuarantee} guaranteed cards remaining`);
848
+ }
849
+
780
850
  // If a replan is in flight, wait for it to complete before drawing.
781
851
  // This ensures the user sees cards scored against their latest state
782
852
  // (e.g. after a GPC intro unlocked new content).
@@ -853,7 +923,7 @@ export class SessionController<TView = unknown> extends Loggable {
853
923
  void this.requestReplan();
854
924
  }
855
925
 
856
- if (this._secondsRemaining <= 0 && this.failedQ.length === 0) {
926
+ if (this._secondsRemaining <= 0 && this.failedQ.length === 0 && this._minCardsGuarantee <= 0) {
857
927
  this._currentCard = null;
858
928
  endSessionTracking();
859
929
  return null;
@@ -111,10 +111,29 @@ export class EloService {
111
111
  const userElo = toCourseElo(
112
112
  userCourseRegDoc.courses.find((c) => c.courseID === course_id)!.elo
113
113
  );
114
- const cardElo = (await courseDB.getCardEloData([currentCard.card.card_id]))[0];
114
+
115
+ const [cardEloResults, cardTagsMap] = await Promise.all([
116
+ courseDB.getCardEloData([currentCard.card.card_id]),
117
+ courseDB.getAppliedTagsBatch([card_id]),
118
+ ]);
119
+ const cardElo = cardEloResults[0];
120
+
121
+ // Enrich TaggedPerformance with card-level tags not explicitly graded by
122
+ // the question's evaluate(). Category tags (concept:*, ui:*, etc.) are not
123
+ // emitted by individual question types; applying the global score as a proxy
124
+ // keeps hierarchy filter ELO thresholds functional without overriding any
125
+ // fine-grained per-GPC scores the question already provided.
126
+ const cardTags = cardTagsMap.get(card_id) ?? [];
127
+ const enriched: TaggedPerformance = { ...taggedPerformance };
128
+ const globalScore = taggedPerformance._global;
129
+ for (const tag of cardTags) {
130
+ if (!(tag in enriched)) {
131
+ enriched[tag] = globalScore;
132
+ }
133
+ }
115
134
 
116
135
  if (cardElo && userElo) {
117
- const eloUpdate = adjustCourseScoresPerTag(userElo, cardElo, taggedPerformance);
136
+ const eloUpdate = adjustCourseScoresPerTag(userElo, cardElo, enriched);
118
137
  userCourseRegDoc.courses.find((c) => c.courseID === course_id)!.elo = eloUpdate.userElo;
119
138
 
120
139
  const results = await Promise.allSettled([
@@ -131,7 +150,7 @@ export class EloService {
131
150
  const card = (results[1] as PromiseFulfilledResult<any>).value;
132
151
 
133
152
  if (user.ok && card && card.ok) {
134
- const tagCount = Object.keys(taggedPerformance).length - 1; // exclude _global
153
+ const tagCount = Object.keys(enriched).length - 1; // exclude _global
135
154
  logger.info(
136
155
  `[EloService] Updated ELOS (per-tag, ${tagCount} tags):
137
156
  \tUser: ${JSON.stringify(eloUpdate.userElo)})
@@ -181,6 +181,13 @@ export class ResponseProcessor {
181
181
  // Update ELO ratings
182
182
  if (taggedPerformance) {
183
183
  // Per-tag ELO update
184
+ const tagKeys = Object.keys(taggedPerformance).filter((k) => k !== '_global');
185
+ const nullTags = tagKeys.filter((k) => taggedPerformance[k] === null);
186
+ const scoredTags = tagKeys.filter((k) => taggedPerformance[k] !== null);
187
+ logger.info(
188
+ `[ResponseProcessor] per-tag ELO update for ${cardId}: scored=[${scoredTags.join(', ')}] count-only=[${nullTags.join(', ')}]`
189
+ );
190
+
184
191
  void this.eloService.updateUserAndCardEloPerTag(
185
192
  taggedPerformance,
186
193
  courseId,
@@ -188,9 +195,6 @@ export class ResponseProcessor {
188
195
  courseRegistrationDoc,
189
196
  currentCard
190
197
  );
191
- logger.info(
192
- `[ResponseProcessor] Processed correct response with per-tag ELO update (${Object.keys(taggedPerformance).length - 1} tags)`
193
- );
194
198
  } else {
195
199
  // Standard single-score ELO update (backward compatible)
196
200
  const userScore = 0.5 + globalScore / 2;
@@ -1,67 +0,0 @@
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
-
3
- /**
4
- * Main factory interface for data access
5
- */
6
- interface DataLayerProvider {
7
- /**
8
- * Get the user database interface
9
- */
10
- getUserDB(): UserDBInterface;
11
- /**
12
- * Create a UserDBReader for a specific user (admin access required)
13
- * Uses session authentication to verify requesting user is admin
14
- * @param targetUsername - The username to create a reader for
15
- * @throws Error if requesting user is not 'admin'
16
- */
17
- createUserReaderForUser(targetUsername: string): Promise<UserDBReader>;
18
- /**
19
- * Get a course database interface
20
- */
21
- getCourseDB(courseId: string): CourseDBInterface;
22
- /**
23
- * Get the courses-lookup interface
24
- */
25
- getCoursesDB(): CoursesDBInterface;
26
- /**
27
- * Get a classroom database interface
28
- */
29
- getClassroomDB(classId: string, type: 'student' | 'teacher'): Promise<ClassroomDBInterface>;
30
- /**
31
- * Get the admin database interface
32
- */
33
- getAdminDB(): AdminDBInterface;
34
- /**
35
- * Initialize the data layer
36
- */
37
- initialize(): Promise<void>;
38
- /**
39
- * Teardown the data layer
40
- */
41
- teardown(): Promise<void>;
42
- /**
43
- * Check if this data layer is read-only
44
- */
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>;
65
- }
66
-
67
- export type { DataLayerProvider as D };
@@ -1,67 +0,0 @@
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
-
3
- /**
4
- * Main factory interface for data access
5
- */
6
- interface DataLayerProvider {
7
- /**
8
- * Get the user database interface
9
- */
10
- getUserDB(): UserDBInterface;
11
- /**
12
- * Create a UserDBReader for a specific user (admin access required)
13
- * Uses session authentication to verify requesting user is admin
14
- * @param targetUsername - The username to create a reader for
15
- * @throws Error if requesting user is not 'admin'
16
- */
17
- createUserReaderForUser(targetUsername: string): Promise<UserDBReader>;
18
- /**
19
- * Get a course database interface
20
- */
21
- getCourseDB(courseId: string): CourseDBInterface;
22
- /**
23
- * Get the courses-lookup interface
24
- */
25
- getCoursesDB(): CoursesDBInterface;
26
- /**
27
- * Get a classroom database interface
28
- */
29
- getClassroomDB(classId: string, type: 'student' | 'teacher'): Promise<ClassroomDBInterface>;
30
- /**
31
- * Get the admin database interface
32
- */
33
- getAdminDB(): AdminDBInterface;
34
- /**
35
- * Initialize the data layer
36
- */
37
- initialize(): Promise<void>;
38
- /**
39
- * Teardown the data layer
40
- */
41
- teardown(): Promise<void>;
42
- /**
43
- * Check if this data layer is read-only
44
- */
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>;
65
- }
66
-
67
- export type { DataLayerProvider as D };