@vue-skuilder/db 0.1.39 → 0.2.0
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 +11 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +34 -4
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +34 -4
- 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 +12 -5
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
7
|
-
"version": "0.
|
|
7
|
+
"version": "0.2.0",
|
|
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.
|
|
51
|
+
"@vue-skuilder/common": "0.2.0",
|
|
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.
|
|
65
|
+
"stableVersion": "0.2.0"
|
|
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
|
|
937
|
-
//
|
|
938
|
-
//
|
|
939
|
-
|
|
940
|
-
|
|
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
|
|