@vue-skuilder/db 0.1.38 → 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/dist/index.d.cts CHANGED
@@ -422,13 +422,6 @@ declare class SessionController<TView = unknown> extends Loggable {
422
422
  * Cleared when the depletion-triggered replan fires (newQ exhausted).
423
423
  */
424
424
  private _suppressQualityReplan;
425
- /**
426
- * Guards against infinite depletion-triggered replans. Set to true
427
- * when a depletion replan fires; cleared when a replan produces
428
- * content (newQ.length > 0 after replan) or when an explicit
429
- * (non-auto) replan is requested.
430
- */
431
- private _depletionReplanAttempted;
432
425
  /**
433
426
  * When > 0, the session timer cannot end the session. Decremented on
434
427
  * each nextCard() draw. Set by replans that include `minFollowUpCards`.
@@ -521,6 +514,19 @@ declare class SessionController<TView = unknown> extends Loggable {
521
514
  * newQ.peek(0) is the imminent draw we need to exclude.
522
515
  */
523
516
  private _runReplan;
517
+ /**
518
+ * Run a replan, bypassing requestReplan()'s coalesce logic.
519
+ *
520
+ * Use this when correctness depends on a *fresh* pipeline run, not on
521
+ * the existence of *some* in-flight replan. Specifically: the
522
+ * wedge-breaker path in nextCard(), where coalescing into a previous
523
+ * run that we now know produced insufficient content would re-create
524
+ * the bug we're trying to prevent.
525
+ *
526
+ * Still tracks _replanPromise like requestReplan() does so concurrent
527
+ * observers (auto-trigger guards in nextCard()) see consistent state.
528
+ */
529
+ private _replanUncoalesced;
524
530
  /**
525
531
  * Normalise the requestReplan argument. Accepts either a ReplanOptions
526
532
  * object (new API) or a plain Record<string, unknown> (legacy callers
@@ -1111,6 +1117,17 @@ interface DataLayerConfig {
1111
1117
  COUCHDB_USERNAME?: string;
1112
1118
  COUCHDB_PASSWORD?: string;
1113
1119
  COURSE_IDS?: string[];
1120
+ /**
1121
+ * Per-app tuning for the CouchDB→PouchDB course sync. Only applies when
1122
+ * `type === 'couch'` and the course has `localSync.enabled === true`.
1123
+ * See CourseSyncService.ReplicationOptions for defaults.
1124
+ */
1125
+ courseSync?: {
1126
+ replication?: {
1127
+ batchSize?: number;
1128
+ batchesLimit?: number;
1129
+ };
1130
+ };
1114
1131
  };
1115
1132
  }
1116
1133
  /**
package/dist/index.d.ts CHANGED
@@ -422,13 +422,6 @@ declare class SessionController<TView = unknown> extends Loggable {
422
422
  * Cleared when the depletion-triggered replan fires (newQ exhausted).
423
423
  */
424
424
  private _suppressQualityReplan;
425
- /**
426
- * Guards against infinite depletion-triggered replans. Set to true
427
- * when a depletion replan fires; cleared when a replan produces
428
- * content (newQ.length > 0 after replan) or when an explicit
429
- * (non-auto) replan is requested.
430
- */
431
- private _depletionReplanAttempted;
432
425
  /**
433
426
  * When > 0, the session timer cannot end the session. Decremented on
434
427
  * each nextCard() draw. Set by replans that include `minFollowUpCards`.
@@ -521,6 +514,19 @@ declare class SessionController<TView = unknown> extends Loggable {
521
514
  * newQ.peek(0) is the imminent draw we need to exclude.
522
515
  */
523
516
  private _runReplan;
517
+ /**
518
+ * Run a replan, bypassing requestReplan()'s coalesce logic.
519
+ *
520
+ * Use this when correctness depends on a *fresh* pipeline run, not on
521
+ * the existence of *some* in-flight replan. Specifically: the
522
+ * wedge-breaker path in nextCard(), where coalescing into a previous
523
+ * run that we now know produced insufficient content would re-create
524
+ * the bug we're trying to prevent.
525
+ *
526
+ * Still tracks _replanPromise like requestReplan() does so concurrent
527
+ * observers (auto-trigger guards in nextCard()) see consistent state.
528
+ */
529
+ private _replanUncoalesced;
524
530
  /**
525
531
  * Normalise the requestReplan argument. Accepts either a ReplanOptions
526
532
  * object (new API) or a plain Record<string, unknown> (legacy callers
@@ -1111,6 +1117,17 @@ interface DataLayerConfig {
1111
1117
  COUCHDB_USERNAME?: string;
1112
1118
  COUCHDB_PASSWORD?: string;
1113
1119
  COURSE_IDS?: string[];
1120
+ /**
1121
+ * Per-app tuning for the CouchDB→PouchDB course sync. Only applies when
1122
+ * `type === 'couch'` and the course has `localSync.enabled === true`.
1123
+ * See CourseSyncService.ReplicationOptions for defaults.
1124
+ */
1125
+ courseSync?: {
1126
+ replication?: {
1127
+ batchSize?: number;
1128
+ batchesLimit?: number;
1129
+ };
1130
+ };
1114
1131
  };
1115
1132
  }
1116
1133
  /**
package/dist/index.js CHANGED
@@ -6469,16 +6469,25 @@ var init_adminDB2 = __esm({
6469
6469
  });
6470
6470
 
6471
6471
  // src/impl/couch/CourseSyncService.ts
6472
- var CourseSyncService;
6472
+ var CourseSyncService_exports = {};
6473
+ __export(CourseSyncService_exports, {
6474
+ CourseSyncService: () => CourseSyncService
6475
+ });
6476
+ var DEFAULT_REPLICATION, CourseSyncService;
6473
6477
  var init_CourseSyncService = __esm({
6474
6478
  "src/impl/couch/CourseSyncService.ts"() {
6475
6479
  "use strict";
6476
6480
  init_pouchdb_setup();
6477
6481
  init_couch();
6478
6482
  init_logger();
6483
+ DEFAULT_REPLICATION = {
6484
+ batchSize: 1e3,
6485
+ batchesLimit: 5
6486
+ };
6479
6487
  CourseSyncService = class _CourseSyncService {
6480
6488
  static instance = null;
6481
6489
  entries = /* @__PURE__ */ new Map();
6490
+ replicationOptions = DEFAULT_REPLICATION;
6482
6491
  constructor() {
6483
6492
  }
6484
6493
  static getInstance() {
@@ -6487,6 +6496,21 @@ var init_CourseSyncService = __esm({
6487
6496
  }
6488
6497
  return _CourseSyncService.instance;
6489
6498
  }
6499
+ /**
6500
+ * Apply replication tuning. Typically called once from `initializeDataLayer`.
6501
+ * Partial overrides merge with defaults.
6502
+ */
6503
+ configure(opts) {
6504
+ if (opts.replication) {
6505
+ this.replicationOptions = {
6506
+ ...DEFAULT_REPLICATION,
6507
+ ...opts.replication
6508
+ };
6509
+ logger.info(
6510
+ `[CourseSyncService] Replication configured: batch_size=${this.replicationOptions.batchSize}, batches_limit=${this.replicationOptions.batchesLimit}`
6511
+ );
6512
+ }
6513
+ }
6490
6514
  /**
6491
6515
  * Reset the singleton (for testing).
6492
6516
  */
@@ -6615,7 +6639,7 @@ var init_CourseSyncService = __esm({
6615
6639
  const remoteDB = this.getRemoteDB(courseId);
6616
6640
  const syncStart = Date.now();
6617
6641
  logger.info(
6618
- `[CourseSyncService] Starting one-shot replication for course ${courseId}`
6642
+ `[CourseSyncService] Starting one-shot replication for course ${courseId} (batch_size=${this.replicationOptions.batchSize}, batches_limit=${this.replicationOptions.batchesLimit})`
6619
6643
  );
6620
6644
  const result = await this.replicate(remoteDB, localDB);
6621
6645
  const syncTimeMs = Date.now() - syncStart;
@@ -6673,6 +6697,8 @@ var init_CourseSyncService = __esm({
6673
6697
  return new Promise((resolve, reject) => {
6674
6698
  void pouchdb_setup_default.replicate(source, target, {
6675
6699
  // One-shot, not live. Local is a read-only snapshot.
6700
+ batch_size: this.replicationOptions.batchSize,
6701
+ batches_limit: this.replicationOptions.batchesLimit
6676
6702
  }).on("complete", (info) => {
6677
6703
  resolve(info);
6678
6704
  }).on("error", (err) => {
@@ -9298,6 +9324,10 @@ async function initializeDataLayer(config) {
9298
9324
  ENV.COUCHDB_SERVER_URL = config.options.COUCHDB_SERVER_URL;
9299
9325
  ENV.COUCHDB_USERNAME = config.options.COUCHDB_USERNAME;
9300
9326
  ENV.COUCHDB_PASSWORD = config.options.COUCHDB_PASSWORD;
9327
+ if (config.options.courseSync) {
9328
+ const { CourseSyncService: CourseSyncService2 } = await Promise.resolve().then(() => (init_CourseSyncService(), CourseSyncService_exports));
9329
+ CourseSyncService2.getInstance().configure(config.options.courseSync);
9330
+ }
9301
9331
  if (config.options.COUCHDB_PASSWORD && config.options.COUCHDB_USERNAME && typeof window !== "undefined") {
9302
9332
  const { CouchDBSyncStrategy: CouchDBSyncStrategy2 } = await Promise.resolve().then(() => (init_CouchDBSyncStrategy(), CouchDBSyncStrategy_exports));
9303
9333
  const syncStrategy = new CouchDBSyncStrategy2();
@@ -13387,13 +13417,6 @@ var SessionController = class _SessionController extends Loggable {
13387
13417
  * Cleared when the depletion-triggered replan fires (newQ exhausted).
13388
13418
  */
13389
13419
  _suppressQualityReplan = false;
13390
- /**
13391
- * Guards against infinite depletion-triggered replans. Set to true
13392
- * when a depletion replan fires; cleared when a replan produces
13393
- * content (newQ.length > 0 after replan) or when an explicit
13394
- * (non-auto) replan is requested.
13395
- */
13396
- _depletionReplanAttempted = false;
13397
13420
  /**
13398
13421
  * When > 0, the session timer cannot end the session. Decremented on
13399
13422
  * each nextCard() draw. Set by replans that include `minFollowUpCards`.
@@ -13551,9 +13574,6 @@ var SessionController = class _SessionController extends Loggable {
13551
13574
  */
13552
13575
  async requestReplan(options) {
13553
13576
  const opts = this.normalizeReplanOptions(options);
13554
- if (opts.hints || opts.label || opts.limit) {
13555
- this._depletionReplanAttempted = false;
13556
- }
13557
13577
  const hasIntent = this._replanHasIntent(opts);
13558
13578
  if (this._replanPromise) {
13559
13579
  if (!hasIntent) {
@@ -13632,6 +13652,25 @@ var SessionController = class _SessionController extends Loggable {
13632
13652
  }
13633
13653
  await this._executeReplan(opts);
13634
13654
  }
13655
+ /**
13656
+ * Run a replan, bypassing requestReplan()'s coalesce logic.
13657
+ *
13658
+ * Use this when correctness depends on a *fresh* pipeline run, not on
13659
+ * the existence of *some* in-flight replan. Specifically: the
13660
+ * wedge-breaker path in nextCard(), where coalescing into a previous
13661
+ * run that we now know produced insufficient content would re-create
13662
+ * the bug we're trying to prevent.
13663
+ *
13664
+ * Still tracks _replanPromise like requestReplan() does so concurrent
13665
+ * observers (auto-trigger guards in nextCard()) see consistent state.
13666
+ */
13667
+ async _replanUncoalesced(opts) {
13668
+ const run = this._runReplan(opts);
13669
+ this._replanPromise = run.finally(() => {
13670
+ if (this._replanPromise === run) this._replanPromise = null;
13671
+ });
13672
+ await run;
13673
+ }
13635
13674
  /**
13636
13675
  * Normalise the requestReplan argument. Accepts either a ReplanOptions
13637
13676
  * object (new API) or a plain Record<string, unknown> (legacy callers
@@ -13685,9 +13724,6 @@ var SessionController = class _SessionController extends Loggable {
13685
13724
  `[Replan] Only ${wellIndicated}/${_SessionController.MIN_WELL_INDICATED} well-indicated cards after replan`
13686
13725
  );
13687
13726
  }
13688
- if (this.newQ.length > 0) {
13689
- this._depletionReplanAttempted = false;
13690
- }
13691
13727
  await this.hydrationService.ensureHydratedCards();
13692
13728
  const labelTag = opts.label ? ` [${opts.label}]` : "";
13693
13729
  this.log(`Replan complete${labelTag}: newQ now has ${this.newQ.length} cards (mode=${mode})`);
@@ -13962,25 +13998,17 @@ var SessionController = class _SessionController extends Loggable {
13962
13998
  this._minCardsGuarantee--;
13963
13999
  this.log(`[CardGuarantee] ${this._minCardsGuarantee} guaranteed cards remaining`);
13964
14000
  }
13965
- if (this._replanPromise) {
13966
- this.log("nextCard: awaiting in-flight replan before drawing");
14001
+ if (this._replanPromise && this.newQ.length === 0 && this.reviewQ.length === 0 && this.failedQ.length === 0) {
14002
+ this.log("nextCard: queues empty, awaiting in-flight replan before drawing");
13967
14003
  await this._replanPromise;
13968
14004
  }
13969
- if (this.newQ.length <= 1 && this._secondsRemaining > 0 && !this._replanPromise && !this._depletionReplanAttempted) {
14005
+ if (this.newQ.length <= 1 && this._secondsRemaining > 0 && !this._replanPromise) {
13970
14006
  this._suppressQualityReplan = false;
13971
- this._depletionReplanAttempted = true;
13972
14007
  const otherContent = this.reviewQ.length + this.failedQ.length;
13973
- if (this.newQ.length === 0 && otherContent === 0) {
13974
- this.log(
13975
- `[AutoReplan:depletion] All queues empty with ${this._secondsRemaining}s remaining. Awaiting replan.`
13976
- );
13977
- await this.requestReplan();
13978
- } else {
13979
- this.log(
13980
- `[AutoReplan:depletion] newQ has ${this.newQ.length} card(s) (${otherContent} in other queues) with ${this._secondsRemaining}s remaining. Triggering background replan.`
13981
- );
13982
- void this.requestReplan();
13983
- }
14008
+ this.log(
14009
+ `[AutoReplan:depletion] newQ has ${this.newQ.length} card(s) (${otherContent} in other queues) with ${this._secondsRemaining}s remaining. Triggering background replan.`
14010
+ );
14011
+ void this.requestReplan();
13984
14012
  }
13985
14013
  const REPLAN_BUFFER = 3;
13986
14014
  if (!this._suppressQualityReplan && this._wellIndicatedRemaining <= REPLAN_BUFFER && this.newQ.length > 0 && !this._replanPromise) {
@@ -13994,6 +14022,27 @@ var SessionController = class _SessionController extends Loggable {
13994
14022
  endSessionTracking();
13995
14023
  return null;
13996
14024
  }
14025
+ const WEDGE_MAX_EMPTY_STREAK = 3;
14026
+ const WEDGE_BACKOFF_MS = 250;
14027
+ let wedgeEmptyStreak = 0;
14028
+ while (this._secondsRemaining > 0 && this.newQ.length === 0 && this.reviewQ.length === 0 && this.failedQ.length === 0) {
14029
+ this.log(
14030
+ `[WedgeBreaker] All queues empty with ${this._secondsRemaining}s remaining. Running pipeline (attempt ${wedgeEmptyStreak + 1}/${WEDGE_MAX_EMPTY_STREAK}).`
14031
+ );
14032
+ await this._replanUncoalesced({ label: "wedge-breaker" });
14033
+ if (this.newQ.length === 0 && this.reviewQ.length === 0 && this.failedQ.length === 0) {
14034
+ wedgeEmptyStreak++;
14035
+ if (wedgeEmptyStreak >= WEDGE_MAX_EMPTY_STREAK) {
14036
+ this.log(
14037
+ `[WedgeBreaker] Pipeline returned no content ${WEDGE_MAX_EMPTY_STREAK} consecutive times. Giving up; session will end.`
14038
+ );
14039
+ break;
14040
+ }
14041
+ await new Promise((resolve) => setTimeout(resolve, WEDGE_BACKOFF_MS));
14042
+ } else {
14043
+ wedgeEmptyStreak = 0;
14044
+ }
14045
+ }
13997
14046
  const MAX_SKIP = 20;
13998
14047
  for (let attempt = 0; attempt < MAX_SKIP; attempt++) {
13999
14048
  const nextItem = this._selectNextItemToHydrate();