@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/core/index.js.map +1 -1
- package/dist/core/index.mjs.map +1 -1
- package/dist/impl/couch/index.d.cts +25 -1
- package/dist/impl/couch/index.d.ts +25 -1
- package/dist/impl/couch/index.js +24 -2
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +24 -2
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/index.d.cts +24 -7
- package/dist/index.d.ts +24 -7
- package/dist/index.js +79 -30
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +79 -30
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/factory.ts +17 -0
- package/src/impl/couch/CourseSyncService.ts +46 -1
- package/src/study/SessionController.ts +104 -69
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
|
|
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
|
|
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
|
-
|
|
13872
|
-
this.
|
|
13873
|
-
|
|
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();
|