@vue-skuilder/db 0.1.39 → 0.1.40

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/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
7
- "version": "0.1.39",
7
+ "version": "0.1.40",
8
8
  "description": "Database layer for vue-skuilder",
9
9
  "main": "dist/index.js",
10
10
  "module": "dist/index.mjs",
@@ -48,7 +48,7 @@
48
48
  },
49
49
  "dependencies": {
50
50
  "@nilock2/pouchdb-authentication": "^1.0.2",
51
- "@vue-skuilder/common": "0.1.39",
51
+ "@vue-skuilder/common": "0.1.40",
52
52
  "cross-fetch": "^4.1.0",
53
53
  "moment": "^2.29.4",
54
54
  "pouchdb": "^9.0.0",
@@ -62,5 +62,5 @@
62
62
  "vite": "^8.0.0",
63
63
  "vitest": "^4.1.0"
64
64
  },
65
- "stableVersion": "0.1.39"
65
+ "stableVersion": "0.1.40"
66
66
  }
package/src/factory.ts CHANGED
@@ -37,6 +37,18 @@ export interface DataLayerConfig {
37
37
  COUCHDB_PASSWORD?: string;
38
38
 
39
39
  COURSE_IDS?: string[];
40
+
41
+ /**
42
+ * Per-app tuning for the CouchDB→PouchDB course sync. Only applies when
43
+ * `type === 'couch'` and the course has `localSync.enabled === true`.
44
+ * See CourseSyncService.ReplicationOptions for defaults.
45
+ */
46
+ courseSync?: {
47
+ replication?: {
48
+ batchSize?: number;
49
+ batchesLimit?: number;
50
+ };
51
+ };
40
52
  };
41
53
  }
42
54
 
@@ -69,6 +81,11 @@ export async function initializeDataLayer(config: DataLayerConfig): Promise<Data
69
81
  ENV.COUCHDB_USERNAME = config.options.COUCHDB_USERNAME;
70
82
  ENV.COUCHDB_PASSWORD = config.options.COUCHDB_PASSWORD;
71
83
 
84
+ if (config.options.courseSync) {
85
+ const { CourseSyncService } = await import('./impl/couch/CourseSyncService');
86
+ CourseSyncService.getInstance().configure(config.options.courseSync);
87
+ }
88
+
72
89
  if (
73
90
  config.options.COUCHDB_PASSWORD &&
74
91
  config.options.COUCHDB_USERNAME &&
@@ -82,10 +82,33 @@ interface SyncEntry {
82
82
  *
83
83
  * The service is a singleton — course sync state is shared across the app.
84
84
  */
85
+ /**
86
+ * Tuning knobs for one-shot remote→local replication.
87
+ * Defaults are chosen for one-shot snapshot replication of bounded course
88
+ * corpora (text/JSON docs, no large attachments). PouchDB's built-in
89
+ * defaults (100/10) target live continuous sync — too conservative for
90
+ * cold-load UX on a course of a few thousand docs.
91
+ *
92
+ * Apps with smaller/larger corpora can override via
93
+ * `initializeDataLayer({ options: { courseSync: { replication: {...} } } })`.
94
+ */
95
+ export interface ReplicationOptions {
96
+ /** Docs per `_bulk_get` HTTP request. Higher = fewer roundtrips. */
97
+ batchSize?: number;
98
+ /** Concurrent batches in flight. */
99
+ batchesLimit?: number;
100
+ }
101
+
102
+ const DEFAULT_REPLICATION: Required<ReplicationOptions> = {
103
+ batchSize: 1000,
104
+ batchesLimit: 5,
105
+ };
106
+
85
107
  export class CourseSyncService {
86
108
  private static instance: CourseSyncService | null = null;
87
109
 
88
110
  private entries: Map<string, SyncEntry> = new Map();
111
+ private replicationOptions: Required<ReplicationOptions> = DEFAULT_REPLICATION;
89
112
 
90
113
  private constructor() {}
91
114
 
@@ -96,6 +119,24 @@ export class CourseSyncService {
96
119
  return CourseSyncService.instance;
97
120
  }
98
121
 
122
+ /**
123
+ * Apply replication tuning. Typically called once from `initializeDataLayer`.
124
+ * Partial overrides merge with defaults.
125
+ */
126
+ configure(opts: { replication?: ReplicationOptions }): void {
127
+ if (opts.replication) {
128
+ this.replicationOptions = {
129
+ ...DEFAULT_REPLICATION,
130
+ ...opts.replication,
131
+ };
132
+ logger.info(
133
+ `[CourseSyncService] Replication configured: ` +
134
+ `batch_size=${this.replicationOptions.batchSize}, ` +
135
+ `batches_limit=${this.replicationOptions.batchesLimit}`
136
+ );
137
+ }
138
+ }
139
+
99
140
  /**
100
141
  * Reset the singleton (for testing).
101
142
  */
@@ -264,7 +305,9 @@ export class CourseSyncService {
264
305
  const syncStart = Date.now();
265
306
 
266
307
  logger.info(
267
- `[CourseSyncService] Starting one-shot replication for course ${courseId}`
308
+ `[CourseSyncService] Starting one-shot replication for course ${courseId} ` +
309
+ `(batch_size=${this.replicationOptions.batchSize}, ` +
310
+ `batches_limit=${this.replicationOptions.batchesLimit})`
268
311
  );
269
312
 
270
313
  const result = await this.replicate(remoteDB, localDB);
@@ -339,6 +382,8 @@ export class CourseSyncService {
339
382
  return new Promise((resolve, reject) => {
340
383
  void pouch.replicate(source, target, {
341
384
  // One-shot, not live. Local is a read-only snapshot.
385
+ batch_size: this.replicationOptions.batchSize,
386
+ batches_limit: this.replicationOptions.batchesLimit,
342
387
  })
343
388
  .on('complete', (info) => {
344
389
  resolve(info);
@@ -933,11 +933,18 @@ export class SessionController<TView = unknown> extends Loggable {
933
933
  this.log(`[CardGuarantee] ${this._minCardsGuarantee} guaranteed cards remaining`);
934
934
  }
935
935
 
936
- // If a replan is in flight, wait for it to complete before drawing.
937
- // This ensures the user sees cards scored against their latest state
938
- // (e.g. after a GPC intro unlocked new content).
939
- if (this._replanPromise) {
940
- this.log('nextCard: awaiting in-flight replan before drawing');
936
+ // If a replan is in flight AND we have nothing queued to serve, wait for
937
+ // it. When queues still have cards, draw from them immediately and let
938
+ // the replan land asynchronously blocking here adds visible lag
939
+ // between cards for a marginal scoring-freshness benefit. The
940
+ // wedge-breaker below handles the genuinely-empty case.
941
+ if (
942
+ this._replanPromise &&
943
+ this.newQ.length === 0 &&
944
+ this.reviewQ.length === 0 &&
945
+ this.failedQ.length === 0
946
+ ) {
947
+ this.log('nextCard: queues empty, awaiting in-flight replan before drawing');
941
948
  await this._replanPromise;
942
949
  }
943
950