@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.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
|
|
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
|
|
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
|
-
|
|
13974
|
-
this.
|
|
13975
|
-
|
|
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();
|