@vue-skuilder/db 0.2.12 → 0.2.14
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 +5 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +5 -0
- package/dist/core/index.mjs.map +1 -1
- package/dist/impl/couch/index.js +13 -0
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +13 -0
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.js +5 -0
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +5 -0
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +46 -27
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +46 -27
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/impl/common/BaseUserDB.ts +13 -0
- package/src/impl/couch/CouchDBSyncStrategy.ts +17 -0
- package/src/study/services/EloService.ts +20 -8
- package/src/study/services/ResponseProcessor.ts +62 -50
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
7
|
-
"version": "0.2.
|
|
7
|
+
"version": "0.2.14",
|
|
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.2.
|
|
51
|
+
"@vue-skuilder/common": "0.2.14",
|
|
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.2.
|
|
65
|
+
"stableVersion": "0.2.14"
|
|
66
66
|
}
|
|
@@ -214,6 +214,19 @@ Currently logged-in as ${this._username}.`
|
|
|
214
214
|
}
|
|
215
215
|
|
|
216
216
|
const ret = await this.syncStrategy.logout!();
|
|
217
|
+
|
|
218
|
+
// Mint a FRESH guest identity on logout rather than re-adopting whatever
|
|
219
|
+
// sk-guest-uuid happens to survive in localStorage. A stale UUID can point
|
|
220
|
+
// at an already-consumed guest DB (one previously migrated into a real
|
|
221
|
+
// account); re-adopting it would resurface that account's progress and, on
|
|
222
|
+
// the next signup, migrate it into the new account. Clearing the pointer
|
|
223
|
+
// forces accomodateGuest() to generate a new UUID + empty guest DB.
|
|
224
|
+
try {
|
|
225
|
+
localStorage.removeItem('sk-guest-uuid');
|
|
226
|
+
} catch (e) {
|
|
227
|
+
logger.warn('localStorage not available (Node.js environment):', e);
|
|
228
|
+
}
|
|
229
|
+
|
|
217
230
|
// return to 'guest' mode
|
|
218
231
|
this._username = await this.syncStrategy.getCurrentUsername();
|
|
219
232
|
await this.init();
|
|
@@ -228,6 +228,23 @@ export class CouchDBSyncStrategy implements SyncStrategy {
|
|
|
228
228
|
logger.info('No documents to migrate from funnel account');
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
+
// Destroy the consumed guest DB so it can never be re-migrated into a
|
|
232
|
+
// future account. Without this, an orphaned userdb-sk-guest-<uuid> lingers
|
|
233
|
+
// in IndexedDB indefinitely; if its stale identity is later re-adopted
|
|
234
|
+
// (e.g. on logout via a surviving sk-guest-uuid), its progress bleeds into
|
|
235
|
+
// the next new account. Best-effort: migration has already succeeded, so a
|
|
236
|
+
// teardown failure must not fail account creation.
|
|
237
|
+
try {
|
|
238
|
+
await oldLocalDB.destroy();
|
|
239
|
+
logger.info(`Destroyed consumed guest DB for ${oldUsername}`);
|
|
240
|
+
} catch (destroyErr) {
|
|
241
|
+
logger.warn(
|
|
242
|
+
`Failed to destroy consumed guest DB for ${oldUsername} (non-fatal): ${
|
|
243
|
+
destroyErr instanceof Error ? destroyErr.message : String(destroyErr)
|
|
244
|
+
}`
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
231
248
|
return { success: true };
|
|
232
249
|
} catch (error) {
|
|
233
250
|
logger.error('Migration failed:', error);
|
|
@@ -41,14 +41,20 @@ export class EloService {
|
|
|
41
41
|
logger.warn(`k value interpretation not currently implemented`);
|
|
42
42
|
}
|
|
43
43
|
const courseDB = this.dataLayer.getCourseDB(currentCard.card.course_id);
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
const courseReg = userCourseRegDoc.courses.find((c) => c.courseID === course_id);
|
|
45
|
+
if (!courseReg) {
|
|
46
|
+
logger.error(
|
|
47
|
+
`[EloService] No registration for course ${course_id} on user's registration doc — ` +
|
|
48
|
+
`skipping ELO update for card ${card_id}. (Is the user registered for this course?)`
|
|
49
|
+
);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const userElo = toCourseElo(courseReg.elo);
|
|
47
53
|
const cardElo = (await courseDB.getCardEloData([currentCard.card.card_id]))[0];
|
|
48
54
|
|
|
49
55
|
if (cardElo && userElo) {
|
|
50
56
|
const eloUpdate = adjustCourseScores(userElo, cardElo, userScore);
|
|
51
|
-
|
|
57
|
+
courseReg.elo = eloUpdate.userElo;
|
|
52
58
|
|
|
53
59
|
const results = await Promise.allSettled([
|
|
54
60
|
this.user.updateUserElo(course_id, eloUpdate.userElo),
|
|
@@ -108,9 +114,15 @@ export class EloService {
|
|
|
108
114
|
currentCard: StudySessionRecord
|
|
109
115
|
): Promise<void> {
|
|
110
116
|
const courseDB = this.dataLayer.getCourseDB(currentCard.card.course_id);
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
117
|
+
const courseReg = userCourseRegDoc.courses.find((c) => c.courseID === course_id);
|
|
118
|
+
if (!courseReg) {
|
|
119
|
+
logger.error(
|
|
120
|
+
`[EloService] No registration for course ${course_id} on user's registration doc — ` +
|
|
121
|
+
`skipping per-tag ELO update for card ${card_id}. (Is the user registered for this course?)`
|
|
122
|
+
);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const userElo = toCourseElo(courseReg.elo);
|
|
114
126
|
|
|
115
127
|
const [cardEloResults, cardTagsMap] = await Promise.all([
|
|
116
128
|
courseDB.getCardEloData([currentCard.card.card_id]),
|
|
@@ -134,7 +146,7 @@ export class EloService {
|
|
|
134
146
|
|
|
135
147
|
if (cardElo && userElo) {
|
|
136
148
|
const eloUpdate = adjustCourseScoresPerTag(userElo, cardElo, enriched);
|
|
137
|
-
|
|
149
|
+
courseReg.elo = eloUpdate.userElo;
|
|
138
150
|
|
|
139
151
|
const results = await Promise.allSettled([
|
|
140
152
|
this.user.updateUserElo(course_id, eloUpdate.userElo),
|
|
@@ -35,6 +35,16 @@ export class ResponseProcessor {
|
|
|
35
35
|
this.eloService = eloService;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* ELO updates are fired without awaiting so response handling isn't blocked
|
|
40
|
+
* on DB writes — but an unhandled rejection silently drops the update.
|
|
41
|
+
* This produces a catch handler that surfaces the failure in logs.
|
|
42
|
+
*/
|
|
43
|
+
private logEloFailure(context: string, cardId: string): (e: unknown) => void {
|
|
44
|
+
return (e) =>
|
|
45
|
+
logger.error(`[ResponseProcessor] ELO update failed (${context}) for ${cardId}:`, e);
|
|
46
|
+
}
|
|
47
|
+
|
|
38
48
|
/**
|
|
39
49
|
* Parses performance data into global score and optional per-tag scores.
|
|
40
50
|
*
|
|
@@ -194,37 +204,37 @@ export class ResponseProcessor {
|
|
|
194
204
|
`scored=[${scoredTags.join(', ')}] count-only=[${nullTags.join(', ')}]`
|
|
195
205
|
);
|
|
196
206
|
|
|
197
|
-
void this.eloService
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
207
|
+
void this.eloService
|
|
208
|
+
.updateUserAndCardEloPerTag(
|
|
209
|
+
taggedPerformance,
|
|
210
|
+
courseId,
|
|
211
|
+
cardId,
|
|
212
|
+
courseRegistrationDoc,
|
|
213
|
+
currentCard
|
|
214
|
+
)
|
|
215
|
+
.catch(this.logEloFailure('correct per-tag', cardId));
|
|
204
216
|
} else {
|
|
205
217
|
// Standard single-score ELO update (backward compatible)
|
|
206
218
|
const userScore = 0.5 + globalScore / 2;
|
|
207
219
|
|
|
208
220
|
if (history.records.length === 1) {
|
|
209
221
|
// First interaction with this card - standard ELO update
|
|
210
|
-
void this.eloService
|
|
211
|
-
userScore,
|
|
212
|
-
|
|
213
|
-
cardId,
|
|
214
|
-
courseRegistrationDoc,
|
|
215
|
-
currentCard
|
|
216
|
-
);
|
|
222
|
+
void this.eloService
|
|
223
|
+
.updateUserAndCardElo(userScore, courseId, cardId, courseRegistrationDoc, currentCard)
|
|
224
|
+
.catch(this.logEloFailure('correct', cardId));
|
|
217
225
|
} else {
|
|
218
226
|
// Multiple interactions - reduce K-factor to limit ELO volatility
|
|
219
227
|
const k = Math.ceil(32 / history.records.length);
|
|
220
|
-
void this.eloService
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
+
void this.eloService
|
|
229
|
+
.updateUserAndCardElo(
|
|
230
|
+
userScore,
|
|
231
|
+
courseId,
|
|
232
|
+
cardId,
|
|
233
|
+
courseRegistrationDoc,
|
|
234
|
+
currentCard,
|
|
235
|
+
k
|
|
236
|
+
)
|
|
237
|
+
.catch(this.logEloFailure('correct repeat-view', cardId));
|
|
228
238
|
}
|
|
229
239
|
logger.info(
|
|
230
240
|
`[FirstContactElo] correct first-attempt ELO update (score=${userScore.toFixed(3)}) ` +
|
|
@@ -286,13 +296,15 @@ export class ResponseProcessor {
|
|
|
286
296
|
if (cardRecord.priorAttemps === 0) {
|
|
287
297
|
if (taggedPerformance) {
|
|
288
298
|
// Per-tag ELO update for incorrect response
|
|
289
|
-
void this.eloService
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
299
|
+
void this.eloService
|
|
300
|
+
.updateUserAndCardEloPerTag(
|
|
301
|
+
taggedPerformance,
|
|
302
|
+
courseId,
|
|
303
|
+
cardId,
|
|
304
|
+
courseRegistrationDoc,
|
|
305
|
+
currentCard
|
|
306
|
+
)
|
|
307
|
+
.catch(this.logEloFailure('incorrect per-tag', cardId));
|
|
296
308
|
logger.info(
|
|
297
309
|
`[FirstContactElo] incorrect first-attempt per-tag ELO update for ${cardId} ` +
|
|
298
310
|
`(historyLen=${history.records.length}, priorAttemps=${cardRecord.priorAttemps}, ` +
|
|
@@ -300,13 +312,15 @@ export class ResponseProcessor {
|
|
|
300
312
|
);
|
|
301
313
|
} else {
|
|
302
314
|
// Standard single-score ELO update
|
|
303
|
-
void this.eloService
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
315
|
+
void this.eloService
|
|
316
|
+
.updateUserAndCardElo(
|
|
317
|
+
0, // Failed response = 0 score
|
|
318
|
+
courseId,
|
|
319
|
+
cardId,
|
|
320
|
+
courseRegistrationDoc,
|
|
321
|
+
currentCard
|
|
322
|
+
)
|
|
323
|
+
.catch(this.logEloFailure('incorrect', cardId));
|
|
310
324
|
logger.info(
|
|
311
325
|
`[FirstContactElo] incorrect first-attempt ELO update (score=0) for ${cardId} ` +
|
|
312
326
|
`(historyLen=${history.records.length}, priorAttemps=${cardRecord.priorAttemps})`
|
|
@@ -329,21 +343,19 @@ export class ResponseProcessor {
|
|
|
329
343
|
if (!eloUpdated) {
|
|
330
344
|
if (taggedPerformance) {
|
|
331
345
|
// Use tagged performance for final failure
|
|
332
|
-
void this.eloService
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
346
|
+
void this.eloService
|
|
347
|
+
.updateUserAndCardEloPerTag(
|
|
348
|
+
taggedPerformance,
|
|
349
|
+
courseId,
|
|
350
|
+
cardId,
|
|
351
|
+
courseRegistrationDoc,
|
|
352
|
+
currentCard
|
|
353
|
+
)
|
|
354
|
+
.catch(this.logEloFailure('dismiss-failed per-tag', cardId));
|
|
339
355
|
} else {
|
|
340
|
-
void this.eloService
|
|
341
|
-
0,
|
|
342
|
-
|
|
343
|
-
cardId,
|
|
344
|
-
courseRegistrationDoc,
|
|
345
|
-
currentCard
|
|
346
|
-
);
|
|
356
|
+
void this.eloService
|
|
357
|
+
.updateUserAndCardElo(0, courseId, cardId, courseRegistrationDoc, currentCard)
|
|
358
|
+
.catch(this.logEloFailure('dismiss-failed', cardId));
|
|
347
359
|
}
|
|
348
360
|
logger.info(
|
|
349
361
|
`[FirstContactElo] dismiss-failed final ELO penalty for ${cardId} ` +
|