@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.mjs CHANGED
@@ -6451,16 +6451,25 @@ var init_adminDB2 = __esm({
6451
6451
  });
6452
6452
 
6453
6453
  // src/impl/couch/CourseSyncService.ts
6454
- var CourseSyncService;
6454
+ var CourseSyncService_exports = {};
6455
+ __export(CourseSyncService_exports, {
6456
+ CourseSyncService: () => CourseSyncService
6457
+ });
6458
+ var DEFAULT_REPLICATION, CourseSyncService;
6455
6459
  var init_CourseSyncService = __esm({
6456
6460
  "src/impl/couch/CourseSyncService.ts"() {
6457
6461
  "use strict";
6458
6462
  init_pouchdb_setup();
6459
6463
  init_couch();
6460
6464
  init_logger();
6465
+ DEFAULT_REPLICATION = {
6466
+ batchSize: 1e3,
6467
+ batchesLimit: 5
6468
+ };
6461
6469
  CourseSyncService = class _CourseSyncService {
6462
6470
  static instance = null;
6463
6471
  entries = /* @__PURE__ */ new Map();
6472
+ replicationOptions = DEFAULT_REPLICATION;
6464
6473
  constructor() {
6465
6474
  }
6466
6475
  static getInstance() {
@@ -6469,6 +6478,21 @@ var init_CourseSyncService = __esm({
6469
6478
  }
6470
6479
  return _CourseSyncService.instance;
6471
6480
  }
6481
+ /**
6482
+ * Apply replication tuning. Typically called once from `initializeDataLayer`.
6483
+ * Partial overrides merge with defaults.
6484
+ */
6485
+ configure(opts) {
6486
+ if (opts.replication) {
6487
+ this.replicationOptions = {
6488
+ ...DEFAULT_REPLICATION,
6489
+ ...opts.replication
6490
+ };
6491
+ logger.info(
6492
+ `[CourseSyncService] Replication configured: batch_size=${this.replicationOptions.batchSize}, batches_limit=${this.replicationOptions.batchesLimit}`
6493
+ );
6494
+ }
6495
+ }
6472
6496
  /**
6473
6497
  * Reset the singleton (for testing).
6474
6498
  */
@@ -6597,7 +6621,7 @@ var init_CourseSyncService = __esm({
6597
6621
  const remoteDB = this.getRemoteDB(courseId);
6598
6622
  const syncStart = Date.now();
6599
6623
  logger.info(
6600
- `[CourseSyncService] Starting one-shot replication for course ${courseId}`
6624
+ `[CourseSyncService] Starting one-shot replication for course ${courseId} (batch_size=${this.replicationOptions.batchSize}, batches_limit=${this.replicationOptions.batchesLimit})`
6601
6625
  );
6602
6626
  const result = await this.replicate(remoteDB, localDB);
6603
6627
  const syncTimeMs = Date.now() - syncStart;
@@ -6655,6 +6679,8 @@ var init_CourseSyncService = __esm({
6655
6679
  return new Promise((resolve, reject) => {
6656
6680
  void pouchdb_setup_default.replicate(source, target, {
6657
6681
  // One-shot, not live. Local is a read-only snapshot.
6682
+ batch_size: this.replicationOptions.batchSize,
6683
+ batches_limit: this.replicationOptions.batchesLimit
6658
6684
  }).on("complete", (info) => {
6659
6685
  resolve(info);
6660
6686
  }).on("error", (err) => {
@@ -9279,6 +9305,10 @@ async function initializeDataLayer(config) {
9279
9305
  ENV.COUCHDB_SERVER_URL = config.options.COUCHDB_SERVER_URL;
9280
9306
  ENV.COUCHDB_USERNAME = config.options.COUCHDB_USERNAME;
9281
9307
  ENV.COUCHDB_PASSWORD = config.options.COUCHDB_PASSWORD;
9308
+ if (config.options.courseSync) {
9309
+ const { CourseSyncService: CourseSyncService2 } = await Promise.resolve().then(() => (init_CourseSyncService(), CourseSyncService_exports));
9310
+ CourseSyncService2.getInstance().configure(config.options.courseSync);
9311
+ }
9282
9312
  if (config.options.COUCHDB_PASSWORD && config.options.COUCHDB_USERNAME && typeof window !== "undefined") {
9283
9313
  const { CouchDBSyncStrategy: CouchDBSyncStrategy2 } = await Promise.resolve().then(() => (init_CouchDBSyncStrategy(), CouchDBSyncStrategy_exports));
9284
9314
  const syncStrategy = new CouchDBSyncStrategy2();
@@ -13285,13 +13315,6 @@ var SessionController = class _SessionController extends Loggable {
13285
13315
  * Cleared when the depletion-triggered replan fires (newQ exhausted).
13286
13316
  */
13287
13317
  _suppressQualityReplan = false;
13288
- /**
13289
- * Guards against infinite depletion-triggered replans. Set to true
13290
- * when a depletion replan fires; cleared when a replan produces
13291
- * content (newQ.length > 0 after replan) or when an explicit
13292
- * (non-auto) replan is requested.
13293
- */
13294
- _depletionReplanAttempted = false;
13295
13318
  /**
13296
13319
  * When > 0, the session timer cannot end the session. Decremented on
13297
13320
  * each nextCard() draw. Set by replans that include `minFollowUpCards`.
@@ -13449,9 +13472,6 @@ var SessionController = class _SessionController extends Loggable {
13449
13472
  */
13450
13473
  async requestReplan(options) {
13451
13474
  const opts = this.normalizeReplanOptions(options);
13452
- if (opts.hints || opts.label || opts.limit) {
13453
- this._depletionReplanAttempted = false;
13454
- }
13455
13475
  const hasIntent = this._replanHasIntent(opts);
13456
13476
  if (this._replanPromise) {
13457
13477
  if (!hasIntent) {
@@ -13530,6 +13550,25 @@ var SessionController = class _SessionController extends Loggable {
13530
13550
  }
13531
13551
  await this._executeReplan(opts);
13532
13552
  }
13553
+ /**
13554
+ * Run a replan, bypassing requestReplan()'s coalesce logic.
13555
+ *
13556
+ * Use this when correctness depends on a *fresh* pipeline run, not on
13557
+ * the existence of *some* in-flight replan. Specifically: the
13558
+ * wedge-breaker path in nextCard(), where coalescing into a previous
13559
+ * run that we now know produced insufficient content would re-create
13560
+ * the bug we're trying to prevent.
13561
+ *
13562
+ * Still tracks _replanPromise like requestReplan() does so concurrent
13563
+ * observers (auto-trigger guards in nextCard()) see consistent state.
13564
+ */
13565
+ async _replanUncoalesced(opts) {
13566
+ const run = this._runReplan(opts);
13567
+ this._replanPromise = run.finally(() => {
13568
+ if (this._replanPromise === run) this._replanPromise = null;
13569
+ });
13570
+ await run;
13571
+ }
13533
13572
  /**
13534
13573
  * Normalise the requestReplan argument. Accepts either a ReplanOptions
13535
13574
  * object (new API) or a plain Record<string, unknown> (legacy callers
@@ -13583,9 +13622,6 @@ var SessionController = class _SessionController extends Loggable {
13583
13622
  `[Replan] Only ${wellIndicated}/${_SessionController.MIN_WELL_INDICATED} well-indicated cards after replan`
13584
13623
  );
13585
13624
  }
13586
- if (this.newQ.length > 0) {
13587
- this._depletionReplanAttempted = false;
13588
- }
13589
13625
  await this.hydrationService.ensureHydratedCards();
13590
13626
  const labelTag = opts.label ? ` [${opts.label}]` : "";
13591
13627
  this.log(`Replan complete${labelTag}: newQ now has ${this.newQ.length} cards (mode=${mode})`);
@@ -13860,25 +13896,17 @@ var SessionController = class _SessionController extends Loggable {
13860
13896
  this._minCardsGuarantee--;
13861
13897
  this.log(`[CardGuarantee] ${this._minCardsGuarantee} guaranteed cards remaining`);
13862
13898
  }
13863
- if (this._replanPromise) {
13864
- this.log("nextCard: awaiting in-flight replan before drawing");
13899
+ if (this._replanPromise && this.newQ.length === 0 && this.reviewQ.length === 0 && this.failedQ.length === 0) {
13900
+ this.log("nextCard: queues empty, awaiting in-flight replan before drawing");
13865
13901
  await this._replanPromise;
13866
13902
  }
13867
- if (this.newQ.length <= 1 && this._secondsRemaining > 0 && !this._replanPromise && !this._depletionReplanAttempted) {
13903
+ if (this.newQ.length <= 1 && this._secondsRemaining > 0 && !this._replanPromise) {
13868
13904
  this._suppressQualityReplan = false;
13869
- this._depletionReplanAttempted = true;
13870
13905
  const otherContent = this.reviewQ.length + this.failedQ.length;
13871
- if (this.newQ.length === 0 && otherContent === 0) {
13872
- this.log(
13873
- `[AutoReplan:depletion] All queues empty with ${this._secondsRemaining}s remaining. Awaiting replan.`
13874
- );
13875
- await this.requestReplan();
13876
- } else {
13877
- this.log(
13878
- `[AutoReplan:depletion] newQ has ${this.newQ.length} card(s) (${otherContent} in other queues) with ${this._secondsRemaining}s remaining. Triggering background replan.`
13879
- );
13880
- void this.requestReplan();
13881
- }
13906
+ this.log(
13907
+ `[AutoReplan:depletion] newQ has ${this.newQ.length} card(s) (${otherContent} in other queues) with ${this._secondsRemaining}s remaining. Triggering background replan.`
13908
+ );
13909
+ void this.requestReplan();
13882
13910
  }
13883
13911
  const REPLAN_BUFFER = 3;
13884
13912
  if (!this._suppressQualityReplan && this._wellIndicatedRemaining <= REPLAN_BUFFER && this.newQ.length > 0 && !this._replanPromise) {
@@ -13892,6 +13920,27 @@ var SessionController = class _SessionController extends Loggable {
13892
13920
  endSessionTracking();
13893
13921
  return null;
13894
13922
  }
13923
+ const WEDGE_MAX_EMPTY_STREAK = 3;
13924
+ const WEDGE_BACKOFF_MS = 250;
13925
+ let wedgeEmptyStreak = 0;
13926
+ while (this._secondsRemaining > 0 && this.newQ.length === 0 && this.reviewQ.length === 0 && this.failedQ.length === 0) {
13927
+ this.log(
13928
+ `[WedgeBreaker] All queues empty with ${this._secondsRemaining}s remaining. Running pipeline (attempt ${wedgeEmptyStreak + 1}/${WEDGE_MAX_EMPTY_STREAK}).`
13929
+ );
13930
+ await this._replanUncoalesced({ label: "wedge-breaker" });
13931
+ if (this.newQ.length === 0 && this.reviewQ.length === 0 && this.failedQ.length === 0) {
13932
+ wedgeEmptyStreak++;
13933
+ if (wedgeEmptyStreak >= WEDGE_MAX_EMPTY_STREAK) {
13934
+ this.log(
13935
+ `[WedgeBreaker] Pipeline returned no content ${WEDGE_MAX_EMPTY_STREAK} consecutive times. Giving up; session will end.`
13936
+ );
13937
+ break;
13938
+ }
13939
+ await new Promise((resolve) => setTimeout(resolve, WEDGE_BACKOFF_MS));
13940
+ } else {
13941
+ wedgeEmptyStreak = 0;
13942
+ }
13943
+ }
13895
13944
  const MAX_SKIP = 20;
13896
13945
  for (let attempt = 0; attempt < MAX_SKIP; attempt++) {
13897
13946
  const nextItem = this._selectNextItemToHydrate();