@vue-skuilder/db 0.2.17 → 0.2.18
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/{SyncStrategy-CyATpyLQ.d.cts → SyncStrategy-BJMZq3WO.d.cts} +17 -0
- package/dist/{SyncStrategy-CyATpyLQ.d.ts → SyncStrategy-BJMZq3WO.d.ts} +17 -0
- package/dist/{contentSource-Brz42x7n.d.cts → contentSource-CBqZCoGU.d.cts} +85 -1
- package/dist/{contentSource-B1p-vdz7.d.ts → contentSource-CudEz5Tm.d.ts} +85 -1
- package/dist/core/index.d.cts +3 -3
- package/dist/core/index.d.ts +3 -3
- package/dist/core/index.js +146 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +145 -1
- package/dist/core/index.mjs.map +1 -1
- package/dist/{dataLayerProvider-CpwpT1rM.d.cts → dataLayerProvider-BBA8tJNx.d.cts} +1 -1
- package/dist/{dataLayerProvider-BWayUIoK.d.ts → dataLayerProvider-C9WgkBzR.d.ts} +1 -1
- package/dist/impl/couch/index.d.cts +16 -3
- package/dist/impl/couch/index.d.ts +16 -3
- package/dist/impl/couch/index.js +178 -3
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +178 -3
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.d.cts +3 -3
- package/dist/impl/static/index.d.ts +3 -3
- package/dist/impl/static/index.js +144 -1
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +144 -1
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +180 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +179 -3
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -3
- package/src/core/index.ts +1 -0
- package/src/core/interfaces/userDB.ts +11 -0
- package/src/core/types/hydration.ts +78 -0
- package/src/impl/common/BaseUserDB.ts +202 -2
- package/src/impl/common/SyncStrategy.ts +19 -0
- package/src/impl/couch/CouchDBSyncStrategy.ts +54 -4
- package/tests/impl/hydration.test.ts +281 -0
|
@@ -36,6 +36,23 @@ interface SyncStrategy {
|
|
|
36
36
|
* @returns PouchDB database instance for write operations
|
|
37
37
|
*/
|
|
38
38
|
getWriteDB?(username: string): PouchDB.Database;
|
|
39
|
+
/**
|
|
40
|
+
* Pull the remote database into the local one and RESOLVE WHEN DONE.
|
|
41
|
+
*
|
|
42
|
+
* Distinct from startSync(): this is a one-shot, awaitable catch-up used to
|
|
43
|
+
* populate an empty local mirror before the user object is handed out. A
|
|
44
|
+
* strategy that omits it declares that it has no remote worth waiting for.
|
|
45
|
+
*
|
|
46
|
+
* Mechanism only — BaseUser owns the policy (whether to run it, timeout,
|
|
47
|
+
* marker bookkeeping, resulting hydration state).
|
|
48
|
+
*
|
|
49
|
+
* @param localDB The local PouchDB instance
|
|
50
|
+
* @param remoteDB The remote PouchDB instance
|
|
51
|
+
* @returns Count of documents written locally
|
|
52
|
+
*/
|
|
53
|
+
hydrate?(localDB: PouchDB.Database, remoteDB: PouchDB.Database): Promise<{
|
|
54
|
+
docsWritten: number;
|
|
55
|
+
}>;
|
|
39
56
|
/**
|
|
40
57
|
* Start synchronization between local and remote databases
|
|
41
58
|
* @param localDB The local PouchDB instance
|
|
@@ -36,6 +36,23 @@ interface SyncStrategy {
|
|
|
36
36
|
* @returns PouchDB database instance for write operations
|
|
37
37
|
*/
|
|
38
38
|
getWriteDB?(username: string): PouchDB.Database;
|
|
39
|
+
/**
|
|
40
|
+
* Pull the remote database into the local one and RESOLVE WHEN DONE.
|
|
41
|
+
*
|
|
42
|
+
* Distinct from startSync(): this is a one-shot, awaitable catch-up used to
|
|
43
|
+
* populate an empty local mirror before the user object is handed out. A
|
|
44
|
+
* strategy that omits it declares that it has no remote worth waiting for.
|
|
45
|
+
*
|
|
46
|
+
* Mechanism only — BaseUser owns the policy (whether to run it, timeout,
|
|
47
|
+
* marker bookkeeping, resulting hydration state).
|
|
48
|
+
*
|
|
49
|
+
* @param localDB The local PouchDB instance
|
|
50
|
+
* @param remoteDB The remote PouchDB instance
|
|
51
|
+
* @returns Count of documents written locally
|
|
52
|
+
*/
|
|
53
|
+
hydrate?(localDB: PouchDB.Database, remoteDB: PouchDB.Database): Promise<{
|
|
54
|
+
docsWritten: number;
|
|
55
|
+
}>;
|
|
39
56
|
/**
|
|
40
57
|
* Start synchronization between local and remote databases
|
|
41
58
|
* @param localDB The local PouchDB instance
|
|
@@ -418,6 +418,81 @@ interface UserOutcomeRecord {
|
|
|
418
418
|
};
|
|
419
419
|
}
|
|
420
420
|
|
|
421
|
+
/**
|
|
422
|
+
* User database hydration.
|
|
423
|
+
*
|
|
424
|
+
* A logged-in user's data lives in a remote CouchDB `userdb-<hex>` and is
|
|
425
|
+
* mirrored into a browser-local PouchDB. On a device that has never synced
|
|
426
|
+
* that account, the local mirror starts EMPTY — and every local read
|
|
427
|
+
* (`getStrategyState`, `getConfig`, `getCourseRegistrationsDoc`, ...) answers
|
|
428
|
+
* "no such document", which is indistinguishable from "brand new user".
|
|
429
|
+
*
|
|
430
|
+
* Consumers acting on that answer render a new-user experience to an existing
|
|
431
|
+
* user, and — worse — compute writes from empty state that then lose to the
|
|
432
|
+
* real remote document at replication time. Hydration closes that window by
|
|
433
|
+
* pulling the account's documents down BEFORE the user object is handed out.
|
|
434
|
+
*/
|
|
435
|
+
/**
|
|
436
|
+
* Where a user's local database stands relative to its remote counterpart.
|
|
437
|
+
*
|
|
438
|
+
* Only `failed` is a blocking condition. `stale` explicitly is not: it means a
|
|
439
|
+
* full pull completed on this device previously, so local data is real and
|
|
440
|
+
* merely possibly-behind — live sync closes the gap in the background. That is
|
|
441
|
+
* the ordinary offline case and it must keep working.
|
|
442
|
+
*/
|
|
443
|
+
type UserHydrationState =
|
|
444
|
+
/** Guest, or a data layer with no remote. There is nothing to hydrate from. */
|
|
445
|
+
'not-required'
|
|
446
|
+
/** Initial pull in flight. */
|
|
447
|
+
| 'hydrating'
|
|
448
|
+
/** Initial pull completed this session. Local is a faithful mirror. */
|
|
449
|
+
| 'hydrated'
|
|
450
|
+
/**
|
|
451
|
+
* A previous session completed a full pull on this device (recorded by the
|
|
452
|
+
* `_local/hydration` marker). Local data is real; live sync is catching up.
|
|
453
|
+
*/
|
|
454
|
+
| 'stale'
|
|
455
|
+
/**
|
|
456
|
+
* No pull has ever completed on this device and one could not be completed
|
|
457
|
+
* now. Local reads CANNOT be trusted to mean "no data" — callers must not
|
|
458
|
+
* present a new-user experience or compute writes from what they read.
|
|
459
|
+
*/
|
|
460
|
+
| 'failed';
|
|
461
|
+
/**
|
|
462
|
+
* Hydration state plus observability detail.
|
|
463
|
+
*
|
|
464
|
+
* Shaped to mirror `CourseSyncStatus` in CourseSyncService, which solves the
|
|
465
|
+
* same problem for course databases.
|
|
466
|
+
*/
|
|
467
|
+
interface UserHydrationStatus {
|
|
468
|
+
state: UserHydrationState;
|
|
469
|
+
/** Documents pulled by the initial replication (set when state is `hydrated`). */
|
|
470
|
+
docsWritten?: number;
|
|
471
|
+
/** Wall-clock duration of the initial pull attempt, successful or not. */
|
|
472
|
+
durationMs?: number;
|
|
473
|
+
/** Failure detail, set when state is `failed`. */
|
|
474
|
+
error?: string;
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* ID of the per-device marker recording that a full pull once completed.
|
|
478
|
+
*
|
|
479
|
+
* `_local/*` documents are deliberately excluded from replication by CouchDB
|
|
480
|
+
* and PouchDB alike, which is exactly the semantics wanted here: the marker
|
|
481
|
+
* describes THIS device's mirror, and must never travel to another one.
|
|
482
|
+
*/
|
|
483
|
+
declare const HYDRATION_MARKER_ID = "_local/hydration";
|
|
484
|
+
/**
|
|
485
|
+
* Payload of {@link HYDRATION_MARKER_ID}.
|
|
486
|
+
*/
|
|
487
|
+
interface HydrationMarker {
|
|
488
|
+
_id: typeof HYDRATION_MARKER_ID;
|
|
489
|
+
_rev?: string;
|
|
490
|
+
/** Account the mirror was hydrated for — guards against a stale marker after an account switch. */
|
|
491
|
+
username: string;
|
|
492
|
+
/** ISO timestamp of the most recent successful full pull. */
|
|
493
|
+
hydratedAt: string;
|
|
494
|
+
}
|
|
495
|
+
|
|
421
496
|
type Update<T> = Partial<T> | ((x: T) => T);
|
|
422
497
|
|
|
423
498
|
interface DocumentUpdater {
|
|
@@ -438,6 +513,15 @@ interface UserDBReader {
|
|
|
438
513
|
get<T>(id: string): Promise<T & PouchDB.Core.RevisionIdMeta>;
|
|
439
514
|
getUsername(): string;
|
|
440
515
|
isLoggedIn(): boolean;
|
|
516
|
+
/**
|
|
517
|
+
* How far this device's local mirror of the user's data can be trusted.
|
|
518
|
+
*
|
|
519
|
+
* Reads answer from the local mirror, so on a device that has never synced
|
|
520
|
+
* this account "document not found" does not mean "no such data". Anything
|
|
521
|
+
* that would otherwise interpret an empty read as a new user should check
|
|
522
|
+
* for `failed` first. See {@link UserHydrationStatus}.
|
|
523
|
+
*/
|
|
524
|
+
hydrationStatus(): UserHydrationStatus;
|
|
441
525
|
/**
|
|
442
526
|
* Get user configuration
|
|
443
527
|
*/
|
|
@@ -1248,4 +1332,4 @@ interface StudyContentSource {
|
|
|
1248
1332
|
}
|
|
1249
1333
|
declare function getStudySource(source: ContentSourceID, user: UserDBInterface): Promise<StudyContentSource>;
|
|
1250
1334
|
|
|
1251
|
-
export {
|
|
1335
|
+
export { getRegisteredNavigatorNames as $, type AdminDBInterface as A, type UsrCrsDataInterface as B, type CourseDBInterface as C, type DataLayerResult as D, type ClassroomRegistrationDesignation as E, type ClassroomRegistration as F, type GeneratorResult as G, type ClassroomRegistrationDoc as H, type SessionTrackingData as I, type UserConfig as J, type ActivityRecord as K, type CourseRegistration as L, type UserOutcomeRecord as M, type UserHydrationState as N, type UserHydrationStatus as O, HYDRATION_MARKER_ID as P, type HydrationMarker as Q, type ReplanHints as R, type StudySessionItem as S, type TeacherClassroomDBInterface as T, type UserDBInterface as U, type NavigatorConstructor as V, type WeightedCard as W, registerNavigator as X, getRegisteredNavigator as Y, hasRegisteredNavigator as Z, getRegisteredNavigatorRole as _, type UserDBReader as a, initializeNavigatorRegistry as a0, type StrategyContribution as a1, getCardOrigin as a2, Navigators as a3, NavigatorRole as a4, NavigatorRoles as a5, isGenerator as a6, isFilter as a7, type CardGenerator as a8, type GeneratorContext as a9, type CardGeneratorFactory as aa, type LearnableWeight as ab, type OrchestrationContext as ac, computeDeviation as ad, computeSpread as ae, computeEffectiveWeight as af, createOrchestrationContext as ag, type DocumentUpdater as ah, newInterval as ai, type CoursesDBInterface as b, type ClassroomDBInterface as c, type CourseInfo as d, type ContentNavigationStrategyData as e, ContentNavigator as f, type AssignedContent as g, type StudyContentSource as h, type StudentClassroomDBInterface as i, type ScheduledCard as j, type StudySessionFailedItem as k, type StudySessionFailedNewItem as l, type StudySessionFailedReviewItem as m, type StudySessionNewItem as n, type StudySessionReviewItem as o, isReview as p, type ContentSourceID as q, getStudySource as r, type CourseRegistrationDoc as s, type AssignedTag as t, type AssignedCourse as u, type AssignedCard as v, type UserDBWriter as w, type UserDBAuthenticator as x, type UserCourseSettings as y, type UserCourseSetting as z };
|
|
@@ -418,6 +418,81 @@ interface UserOutcomeRecord {
|
|
|
418
418
|
};
|
|
419
419
|
}
|
|
420
420
|
|
|
421
|
+
/**
|
|
422
|
+
* User database hydration.
|
|
423
|
+
*
|
|
424
|
+
* A logged-in user's data lives in a remote CouchDB `userdb-<hex>` and is
|
|
425
|
+
* mirrored into a browser-local PouchDB. On a device that has never synced
|
|
426
|
+
* that account, the local mirror starts EMPTY — and every local read
|
|
427
|
+
* (`getStrategyState`, `getConfig`, `getCourseRegistrationsDoc`, ...) answers
|
|
428
|
+
* "no such document", which is indistinguishable from "brand new user".
|
|
429
|
+
*
|
|
430
|
+
* Consumers acting on that answer render a new-user experience to an existing
|
|
431
|
+
* user, and — worse — compute writes from empty state that then lose to the
|
|
432
|
+
* real remote document at replication time. Hydration closes that window by
|
|
433
|
+
* pulling the account's documents down BEFORE the user object is handed out.
|
|
434
|
+
*/
|
|
435
|
+
/**
|
|
436
|
+
* Where a user's local database stands relative to its remote counterpart.
|
|
437
|
+
*
|
|
438
|
+
* Only `failed` is a blocking condition. `stale` explicitly is not: it means a
|
|
439
|
+
* full pull completed on this device previously, so local data is real and
|
|
440
|
+
* merely possibly-behind — live sync closes the gap in the background. That is
|
|
441
|
+
* the ordinary offline case and it must keep working.
|
|
442
|
+
*/
|
|
443
|
+
type UserHydrationState =
|
|
444
|
+
/** Guest, or a data layer with no remote. There is nothing to hydrate from. */
|
|
445
|
+
'not-required'
|
|
446
|
+
/** Initial pull in flight. */
|
|
447
|
+
| 'hydrating'
|
|
448
|
+
/** Initial pull completed this session. Local is a faithful mirror. */
|
|
449
|
+
| 'hydrated'
|
|
450
|
+
/**
|
|
451
|
+
* A previous session completed a full pull on this device (recorded by the
|
|
452
|
+
* `_local/hydration` marker). Local data is real; live sync is catching up.
|
|
453
|
+
*/
|
|
454
|
+
| 'stale'
|
|
455
|
+
/**
|
|
456
|
+
* No pull has ever completed on this device and one could not be completed
|
|
457
|
+
* now. Local reads CANNOT be trusted to mean "no data" — callers must not
|
|
458
|
+
* present a new-user experience or compute writes from what they read.
|
|
459
|
+
*/
|
|
460
|
+
| 'failed';
|
|
461
|
+
/**
|
|
462
|
+
* Hydration state plus observability detail.
|
|
463
|
+
*
|
|
464
|
+
* Shaped to mirror `CourseSyncStatus` in CourseSyncService, which solves the
|
|
465
|
+
* same problem for course databases.
|
|
466
|
+
*/
|
|
467
|
+
interface UserHydrationStatus {
|
|
468
|
+
state: UserHydrationState;
|
|
469
|
+
/** Documents pulled by the initial replication (set when state is `hydrated`). */
|
|
470
|
+
docsWritten?: number;
|
|
471
|
+
/** Wall-clock duration of the initial pull attempt, successful or not. */
|
|
472
|
+
durationMs?: number;
|
|
473
|
+
/** Failure detail, set when state is `failed`. */
|
|
474
|
+
error?: string;
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* ID of the per-device marker recording that a full pull once completed.
|
|
478
|
+
*
|
|
479
|
+
* `_local/*` documents are deliberately excluded from replication by CouchDB
|
|
480
|
+
* and PouchDB alike, which is exactly the semantics wanted here: the marker
|
|
481
|
+
* describes THIS device's mirror, and must never travel to another one.
|
|
482
|
+
*/
|
|
483
|
+
declare const HYDRATION_MARKER_ID = "_local/hydration";
|
|
484
|
+
/**
|
|
485
|
+
* Payload of {@link HYDRATION_MARKER_ID}.
|
|
486
|
+
*/
|
|
487
|
+
interface HydrationMarker {
|
|
488
|
+
_id: typeof HYDRATION_MARKER_ID;
|
|
489
|
+
_rev?: string;
|
|
490
|
+
/** Account the mirror was hydrated for — guards against a stale marker after an account switch. */
|
|
491
|
+
username: string;
|
|
492
|
+
/** ISO timestamp of the most recent successful full pull. */
|
|
493
|
+
hydratedAt: string;
|
|
494
|
+
}
|
|
495
|
+
|
|
421
496
|
type Update<T> = Partial<T> | ((x: T) => T);
|
|
422
497
|
|
|
423
498
|
interface DocumentUpdater {
|
|
@@ -438,6 +513,15 @@ interface UserDBReader {
|
|
|
438
513
|
get<T>(id: string): Promise<T & PouchDB.Core.RevisionIdMeta>;
|
|
439
514
|
getUsername(): string;
|
|
440
515
|
isLoggedIn(): boolean;
|
|
516
|
+
/**
|
|
517
|
+
* How far this device's local mirror of the user's data can be trusted.
|
|
518
|
+
*
|
|
519
|
+
* Reads answer from the local mirror, so on a device that has never synced
|
|
520
|
+
* this account "document not found" does not mean "no such data". Anything
|
|
521
|
+
* that would otherwise interpret an empty read as a new user should check
|
|
522
|
+
* for `failed` first. See {@link UserHydrationStatus}.
|
|
523
|
+
*/
|
|
524
|
+
hydrationStatus(): UserHydrationStatus;
|
|
441
525
|
/**
|
|
442
526
|
* Get user configuration
|
|
443
527
|
*/
|
|
@@ -1248,4 +1332,4 @@ interface StudyContentSource {
|
|
|
1248
1332
|
}
|
|
1249
1333
|
declare function getStudySource(source: ContentSourceID, user: UserDBInterface): Promise<StudyContentSource>;
|
|
1250
1334
|
|
|
1251
|
-
export {
|
|
1335
|
+
export { getRegisteredNavigatorNames as $, type AdminDBInterface as A, type UsrCrsDataInterface as B, type CourseDBInterface as C, type DataLayerResult as D, type ClassroomRegistrationDesignation as E, type ClassroomRegistration as F, type GeneratorResult as G, type ClassroomRegistrationDoc as H, type SessionTrackingData as I, type UserConfig as J, type ActivityRecord as K, type CourseRegistration as L, type UserOutcomeRecord as M, type UserHydrationState as N, type UserHydrationStatus as O, HYDRATION_MARKER_ID as P, type HydrationMarker as Q, type ReplanHints as R, type StudySessionItem as S, type TeacherClassroomDBInterface as T, type UserDBInterface as U, type NavigatorConstructor as V, type WeightedCard as W, registerNavigator as X, getRegisteredNavigator as Y, hasRegisteredNavigator as Z, getRegisteredNavigatorRole as _, type UserDBReader as a, initializeNavigatorRegistry as a0, type StrategyContribution as a1, getCardOrigin as a2, Navigators as a3, NavigatorRole as a4, NavigatorRoles as a5, isGenerator as a6, isFilter as a7, type CardGenerator as a8, type GeneratorContext as a9, type CardGeneratorFactory as aa, type LearnableWeight as ab, type OrchestrationContext as ac, computeDeviation as ad, computeSpread as ae, computeEffectiveWeight as af, createOrchestrationContext as ag, type DocumentUpdater as ah, newInterval as ai, type CoursesDBInterface as b, type ClassroomDBInterface as c, type CourseInfo as d, type ContentNavigationStrategyData as e, ContentNavigator as f, type AssignedContent as g, type StudyContentSource as h, type StudentClassroomDBInterface as i, type ScheduledCard as j, type StudySessionFailedItem as k, type StudySessionFailedNewItem as l, type StudySessionFailedReviewItem as m, type StudySessionNewItem as n, type StudySessionReviewItem as o, isReview as p, type ContentSourceID as q, getStudySource as r, type CourseRegistrationDoc as s, type AssignedTag as t, type AssignedCourse as u, type AssignedCard as v, type UserDBWriter as w, type UserDBAuthenticator as x, type UserCourseSettings as y, type UserCourseSetting as z };
|
package/dist/core/index.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { K as ActivityRecord, A as AdminDBInterface, v as AssignedCard, g as AssignedContent, u as AssignedCourse, t as AssignedTag,
|
|
3
|
-
export { D as DataLayerProvider } from '../dataLayerProvider-
|
|
1
|
+
import { ab as LearnableWeight, M as UserOutcomeRecord, ac as OrchestrationContext, W as WeightedCard, U as UserDBInterface, C as CourseDBInterface, R as ReplanHints, a1 as StrategyContribution } from '../contentSource-CBqZCoGU.cjs';
|
|
2
|
+
export { K as ActivityRecord, A as AdminDBInterface, v as AssignedCard, g as AssignedContent, u as AssignedCourse, t as AssignedTag, a8 as CardGenerator, aa as CardGeneratorFactory, c as ClassroomDBInterface, F as ClassroomRegistration, E as ClassroomRegistrationDesignation, H as ClassroomRegistrationDoc, e as ContentNavigationStrategyData, f as ContentNavigator, q as ContentSourceID, d as CourseInfo, L as CourseRegistration, s as CourseRegistrationDoc, b as CoursesDBInterface, a9 as GeneratorContext, G as GeneratorResult, P as HYDRATION_MARKER_ID, Q as HydrationMarker, V as NavigatorConstructor, a4 as NavigatorRole, a5 as NavigatorRoles, a3 as Navigators, j as ScheduledCard, I as SessionTrackingData, i as StudentClassroomDBInterface, h as StudyContentSource, k as StudySessionFailedItem, l as StudySessionFailedNewItem, m as StudySessionFailedReviewItem, S as StudySessionItem, n as StudySessionNewItem, o as StudySessionReviewItem, T as TeacherClassroomDBInterface, J as UserConfig, z as UserCourseSetting, y as UserCourseSettings, x as UserDBAuthenticator, a as UserDBReader, w as UserDBWriter, N as UserHydrationState, O as UserHydrationStatus, B as UsrCrsDataInterface, ad as computeDeviation, af as computeEffectiveWeight, ae as computeSpread, ag as createOrchestrationContext, a2 as getCardOrigin, Y as getRegisteredNavigator, $ as getRegisteredNavigatorNames, _ as getRegisteredNavigatorRole, r as getStudySource, Z as hasRegisteredNavigator, a0 as initializeNavigatorRegistry, a7 as isFilter, a6 as isGenerator, p as isReview, X as registerNavigator } from '../contentSource-CBqZCoGU.cjs';
|
|
3
|
+
export { D as DataLayerProvider } from '../dataLayerProvider-BBA8tJNx.cjs';
|
|
4
4
|
import { D as DocType, d as QuestionRecord, b as DocTypePrefixes, C as CardHistory, c as CardRecord } from '../types-legacy-4tlwHnXo.cjs';
|
|
5
5
|
export { e as CardData, f as CourseListData, h as DataShapeData, g as DisplayableData, F as Field, G as GuestUsername, Q as QualifiedCardID, i as QuestionData, S as SkuilderCourseData, a as Tag, T as TagStub, l as log } from '../types-legacy-4tlwHnXo.cjs';
|
|
6
6
|
import { DataShape, ParsedCard } from '@vue-skuilder/common';
|
package/dist/core/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { K as ActivityRecord, A as AdminDBInterface, v as AssignedCard, g as AssignedContent, u as AssignedCourse, t as AssignedTag,
|
|
3
|
-
export { D as DataLayerProvider } from '../dataLayerProvider-
|
|
1
|
+
import { ab as LearnableWeight, M as UserOutcomeRecord, ac as OrchestrationContext, W as WeightedCard, U as UserDBInterface, C as CourseDBInterface, R as ReplanHints, a1 as StrategyContribution } from '../contentSource-CudEz5Tm.js';
|
|
2
|
+
export { K as ActivityRecord, A as AdminDBInterface, v as AssignedCard, g as AssignedContent, u as AssignedCourse, t as AssignedTag, a8 as CardGenerator, aa as CardGeneratorFactory, c as ClassroomDBInterface, F as ClassroomRegistration, E as ClassroomRegistrationDesignation, H as ClassroomRegistrationDoc, e as ContentNavigationStrategyData, f as ContentNavigator, q as ContentSourceID, d as CourseInfo, L as CourseRegistration, s as CourseRegistrationDoc, b as CoursesDBInterface, a9 as GeneratorContext, G as GeneratorResult, P as HYDRATION_MARKER_ID, Q as HydrationMarker, V as NavigatorConstructor, a4 as NavigatorRole, a5 as NavigatorRoles, a3 as Navigators, j as ScheduledCard, I as SessionTrackingData, i as StudentClassroomDBInterface, h as StudyContentSource, k as StudySessionFailedItem, l as StudySessionFailedNewItem, m as StudySessionFailedReviewItem, S as StudySessionItem, n as StudySessionNewItem, o as StudySessionReviewItem, T as TeacherClassroomDBInterface, J as UserConfig, z as UserCourseSetting, y as UserCourseSettings, x as UserDBAuthenticator, a as UserDBReader, w as UserDBWriter, N as UserHydrationState, O as UserHydrationStatus, B as UsrCrsDataInterface, ad as computeDeviation, af as computeEffectiveWeight, ae as computeSpread, ag as createOrchestrationContext, a2 as getCardOrigin, Y as getRegisteredNavigator, $ as getRegisteredNavigatorNames, _ as getRegisteredNavigatorRole, r as getStudySource, Z as hasRegisteredNavigator, a0 as initializeNavigatorRegistry, a7 as isFilter, a6 as isGenerator, p as isReview, X as registerNavigator } from '../contentSource-CudEz5Tm.js';
|
|
3
|
+
export { D as DataLayerProvider } from '../dataLayerProvider-C9WgkBzR.js';
|
|
4
4
|
import { D as DocType, d as QuestionRecord, b as DocTypePrefixes, C as CardHistory, c as CardRecord } from '../types-legacy-4tlwHnXo.js';
|
|
5
5
|
export { e as CardData, f as CourseListData, h as DataShapeData, g as DisplayableData, F as Field, G as GuestUsername, Q as QualifiedCardID, i as QuestionData, S as SkuilderCourseData, a as Tag, T as TagStub, l as log } from '../types-legacy-4tlwHnXo.js';
|
|
6
6
|
import { DataShape, ParsedCard } from '@vue-skuilder/common';
|
package/dist/core/index.js
CHANGED
|
@@ -6767,6 +6767,19 @@ var init_couch = __esm({
|
|
|
6767
6767
|
});
|
|
6768
6768
|
|
|
6769
6769
|
// src/impl/common/BaseUserDB.ts
|
|
6770
|
+
function withTimeout(p, ms, label) {
|
|
6771
|
+
let timer;
|
|
6772
|
+
return Promise.race([
|
|
6773
|
+
p,
|
|
6774
|
+
new Promise((_resolve, reject) => {
|
|
6775
|
+
timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
|
|
6776
|
+
})
|
|
6777
|
+
]).finally(() => {
|
|
6778
|
+
if (timer !== void 0) {
|
|
6779
|
+
clearTimeout(timer);
|
|
6780
|
+
}
|
|
6781
|
+
});
|
|
6782
|
+
}
|
|
6770
6783
|
async function getOrCreateClassroomRegistrationsDoc(user) {
|
|
6771
6784
|
let ret;
|
|
6772
6785
|
try {
|
|
@@ -6859,7 +6872,7 @@ async function dropUserFromClassroom(user, classID) {
|
|
|
6859
6872
|
async function getUserClassrooms(user) {
|
|
6860
6873
|
return getOrCreateClassroomRegistrationsDoc(user);
|
|
6861
6874
|
}
|
|
6862
|
-
var import_common12, import_moment6, log3, BaseUser, userCoursesDoc, userClassroomsDoc;
|
|
6875
|
+
var import_common12, import_moment6, log3, HYDRATION_TIMEOUT_MS, BaseUser, userCoursesDoc, userClassroomsDoc;
|
|
6863
6876
|
var init_BaseUserDB = __esm({
|
|
6864
6877
|
"src/impl/common/BaseUserDB.ts"() {
|
|
6865
6878
|
"use strict";
|
|
@@ -6876,6 +6889,7 @@ var init_BaseUserDB = __esm({
|
|
|
6876
6889
|
log3 = (s) => {
|
|
6877
6890
|
logger.info(s);
|
|
6878
6891
|
};
|
|
6892
|
+
HYDRATION_TIMEOUT_MS = 15e3;
|
|
6879
6893
|
BaseUser = class _BaseUser {
|
|
6880
6894
|
static _instance;
|
|
6881
6895
|
static _initialized = false;
|
|
@@ -6904,6 +6918,29 @@ var init_BaseUserDB = __esm({
|
|
|
6904
6918
|
writeDB;
|
|
6905
6919
|
// Database to use for write operations (local-first approach)
|
|
6906
6920
|
updateQueue;
|
|
6921
|
+
_hydration = { state: "not-required" };
|
|
6922
|
+
/**
|
|
6923
|
+
* How far the local mirror can be trusted. See {@link UserHydrationStatus}.
|
|
6924
|
+
*
|
|
6925
|
+
* Consumers that would otherwise read a 404 as "new user" — onboarding
|
|
6926
|
+
* gates, first-run experiences, progress dashboards — should check for
|
|
6927
|
+
* `failed` and present a retry affordance rather than an empty state.
|
|
6928
|
+
*/
|
|
6929
|
+
hydrationStatus() {
|
|
6930
|
+
return { ...this._hydration };
|
|
6931
|
+
}
|
|
6932
|
+
/**
|
|
6933
|
+
* Whether a missing document may be treated as genuinely absent, allowing
|
|
6934
|
+
* callers to create it with defaults.
|
|
6935
|
+
*
|
|
6936
|
+
* False only when hydration failed or is still running: a 404 then may just
|
|
6937
|
+
* mean "not pulled down yet", and materializing a default would both lie to
|
|
6938
|
+
* the user and, once live sync starts, push a rev-1 document that loses to
|
|
6939
|
+
* the real one — silently discarding it.
|
|
6940
|
+
*/
|
|
6941
|
+
canMaterializeDefaults() {
|
|
6942
|
+
return this._hydration.state !== "failed" && this._hydration.state !== "hydrating";
|
|
6943
|
+
}
|
|
6907
6944
|
async createAccount(username, password) {
|
|
6908
6945
|
if (!this.syncStrategy.canCreateAccount()) {
|
|
6909
6946
|
throw new Error("Account creation not supported by current sync strategy");
|
|
@@ -7021,6 +7058,11 @@ Currently logged-in as ${this._username}.`
|
|
|
7021
7058
|
} catch (e) {
|
|
7022
7059
|
const err = e;
|
|
7023
7060
|
if (err.status === 404) {
|
|
7061
|
+
if (!this.canMaterializeDefaults()) {
|
|
7062
|
+
throw new Error(
|
|
7063
|
+
`Cannot read course registrations for ${this._username}: the local mirror is unavailable (hydration state '${this._hydration.state}'). Refusing to create an empty registration doc \u2014 that would report an existing user as unregistered, and then lose to their real document once sync resumes.`
|
|
7064
|
+
);
|
|
7065
|
+
}
|
|
7024
7066
|
await this.localDB.put({
|
|
7025
7067
|
_id: _BaseUser.DOC_IDS.COURSE_REGISTRATIONS,
|
|
7026
7068
|
courses: [],
|
|
@@ -7263,6 +7305,11 @@ Currently logged-in as ${this._username}.`
|
|
|
7263
7305
|
} catch (e) {
|
|
7264
7306
|
const err = e;
|
|
7265
7307
|
if (err.name && err.name === "not_found") {
|
|
7308
|
+
if (!this.canMaterializeDefaults()) {
|
|
7309
|
+
throw new Error(
|
|
7310
|
+
`Cannot read config for ${this._username}: the local mirror is unavailable (hydration state '${this._hydration.state}'). Refusing to write a default config over what may be an existing one.`
|
|
7311
|
+
);
|
|
7312
|
+
}
|
|
7266
7313
|
await this.localDB.put(defaultConfig);
|
|
7267
7314
|
return this.getConfig();
|
|
7268
7315
|
} else {
|
|
@@ -7336,7 +7383,9 @@ Currently logged-in as ${this._username}.`
|
|
|
7336
7383
|
_BaseUser._initialized = true;
|
|
7337
7384
|
return;
|
|
7338
7385
|
}
|
|
7386
|
+
this.syncStrategy.stopSync?.();
|
|
7339
7387
|
this.setDBandQ();
|
|
7388
|
+
await this.hydrateLocalMirror();
|
|
7340
7389
|
this.syncStrategy.startSync(this.localDB, this.remoteDB);
|
|
7341
7390
|
this.applyDesignDocs().catch((error) => {
|
|
7342
7391
|
log3(`Error in applyDesignDocs background task: ${error}`);
|
|
@@ -7352,6 +7401,85 @@ Currently logged-in as ${this._username}.`
|
|
|
7352
7401
|
});
|
|
7353
7402
|
_BaseUser._initialized = true;
|
|
7354
7403
|
}
|
|
7404
|
+
/**
|
|
7405
|
+
* Pull this account's documents into the local mirror, if that hasn't
|
|
7406
|
+
* happened on this device before.
|
|
7407
|
+
*
|
|
7408
|
+
* Runs between setDBandQ() and startSync() — after the handles exist, before
|
|
7409
|
+
* anything can observe an empty local DB or replicate one upward.
|
|
7410
|
+
*
|
|
7411
|
+
* Never throws: a failure is recorded as `failed` state and reported through
|
|
7412
|
+
* hydrationStatus(), because throwing here would abort init() and leave
|
|
7413
|
+
* BaseUser._initialized false forever (BaseUser.instance() polls it with no
|
|
7414
|
+
* ceiling). Callers decide what a failure means for them.
|
|
7415
|
+
*/
|
|
7416
|
+
async hydrateLocalMirror() {
|
|
7417
|
+
if (this.localDB.name === this.remoteDB.name || !this.syncStrategy.hydrate) {
|
|
7418
|
+
this._hydration = { state: "not-required" };
|
|
7419
|
+
return;
|
|
7420
|
+
}
|
|
7421
|
+
if (await this.hasHydrationMarker()) {
|
|
7422
|
+
this._hydration = { state: "stale" };
|
|
7423
|
+
return;
|
|
7424
|
+
}
|
|
7425
|
+
this._hydration = { state: "hydrating" };
|
|
7426
|
+
const start = Date.now();
|
|
7427
|
+
try {
|
|
7428
|
+
const { docsWritten } = await withTimeout(
|
|
7429
|
+
this.syncStrategy.hydrate(this.localDB, this.remoteDB),
|
|
7430
|
+
HYDRATION_TIMEOUT_MS,
|
|
7431
|
+
`Hydration of local mirror for ${this._username}`
|
|
7432
|
+
);
|
|
7433
|
+
await this.writeHydrationMarker();
|
|
7434
|
+
this._hydration = {
|
|
7435
|
+
state: "hydrated",
|
|
7436
|
+
docsWritten,
|
|
7437
|
+
durationMs: Date.now() - start
|
|
7438
|
+
};
|
|
7439
|
+
log3(
|
|
7440
|
+
`Hydrated local mirror for ${this._username}: ${docsWritten} docs in ${this._hydration.durationMs}ms`
|
|
7441
|
+
);
|
|
7442
|
+
} catch (e) {
|
|
7443
|
+
this.syncStrategy.stopSync?.();
|
|
7444
|
+
this._hydration = {
|
|
7445
|
+
state: "failed",
|
|
7446
|
+
durationMs: Date.now() - start,
|
|
7447
|
+
error: e instanceof Error ? e.message : String(e)
|
|
7448
|
+
};
|
|
7449
|
+
logger.error(`Failed to hydrate local mirror for ${this._username}:`, e);
|
|
7450
|
+
}
|
|
7451
|
+
}
|
|
7452
|
+
/**
|
|
7453
|
+
* Has a full pull completed on this device for the CURRENT account?
|
|
7454
|
+
*
|
|
7455
|
+
* The marker is a `_local/` document, which never replicates — so its
|
|
7456
|
+
* presence describes this device's mirror and cannot arrive from elsewhere.
|
|
7457
|
+
*/
|
|
7458
|
+
async hasHydrationMarker() {
|
|
7459
|
+
try {
|
|
7460
|
+
const marker = await this.localDB.get(HYDRATION_MARKER_ID);
|
|
7461
|
+
return marker.username === this._username;
|
|
7462
|
+
} catch {
|
|
7463
|
+
return false;
|
|
7464
|
+
}
|
|
7465
|
+
}
|
|
7466
|
+
async writeHydrationMarker() {
|
|
7467
|
+
try {
|
|
7468
|
+
let existingRev;
|
|
7469
|
+
try {
|
|
7470
|
+
existingRev = (await this.localDB.get(HYDRATION_MARKER_ID))._rev;
|
|
7471
|
+
} catch {
|
|
7472
|
+
}
|
|
7473
|
+
await this.localDB.put({
|
|
7474
|
+
_id: HYDRATION_MARKER_ID,
|
|
7475
|
+
_rev: existingRev,
|
|
7476
|
+
username: this._username,
|
|
7477
|
+
hydratedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
7478
|
+
});
|
|
7479
|
+
} catch (e) {
|
|
7480
|
+
logger.warn(`Could not write hydration marker for ${this._username}: ${e}`);
|
|
7481
|
+
}
|
|
7482
|
+
}
|
|
7355
7483
|
static designDocs = [
|
|
7356
7484
|
{
|
|
7357
7485
|
_id: "_design/reviewCards",
|
|
@@ -7688,6 +7816,11 @@ Currently logged-in as ${this._username}.`
|
|
|
7688
7816
|
} catch (e) {
|
|
7689
7817
|
const err = e;
|
|
7690
7818
|
if (err.status === 404) {
|
|
7819
|
+
if (!this.canMaterializeDefaults()) {
|
|
7820
|
+
throw new Error(
|
|
7821
|
+
`Cannot read strategy state '${strategyKey}' for ${this._username}: the local mirror is unavailable (hydration state '${this._hydration.state}').`
|
|
7822
|
+
);
|
|
7823
|
+
}
|
|
7691
7824
|
return null;
|
|
7692
7825
|
}
|
|
7693
7826
|
throw e;
|
|
@@ -8036,6 +8169,15 @@ var init_userOutcome = __esm({
|
|
|
8036
8169
|
}
|
|
8037
8170
|
});
|
|
8038
8171
|
|
|
8172
|
+
// src/core/types/hydration.ts
|
|
8173
|
+
var HYDRATION_MARKER_ID;
|
|
8174
|
+
var init_hydration = __esm({
|
|
8175
|
+
"src/core/types/hydration.ts"() {
|
|
8176
|
+
"use strict";
|
|
8177
|
+
HYDRATION_MARKER_ID = "_local/hydration";
|
|
8178
|
+
}
|
|
8179
|
+
});
|
|
8180
|
+
|
|
8039
8181
|
// src/core/bulkImport/cardProcessor.ts
|
|
8040
8182
|
async function importParsedCards(parsedCards, courseDB, config) {
|
|
8041
8183
|
const results = [];
|
|
@@ -8516,6 +8658,7 @@ __export(core_exports, {
|
|
|
8516
8658
|
DocType: () => DocType,
|
|
8517
8659
|
DocTypePrefixes: () => DocTypePrefixes,
|
|
8518
8660
|
GuestUsername: () => GuestUsername,
|
|
8661
|
+
HYDRATION_MARKER_ID: () => HYDRATION_MARKER_ID,
|
|
8519
8662
|
Loggable: () => Loggable,
|
|
8520
8663
|
NavigatorRole: () => NavigatorRole,
|
|
8521
8664
|
NavigatorRoles: () => NavigatorRoles,
|
|
@@ -8573,6 +8716,7 @@ var init_core = __esm({
|
|
|
8573
8716
|
init_user();
|
|
8574
8717
|
init_strategyState();
|
|
8575
8718
|
init_userOutcome();
|
|
8719
|
+
init_hydration();
|
|
8576
8720
|
init_Loggable();
|
|
8577
8721
|
init_util();
|
|
8578
8722
|
init_navigators();
|
|
@@ -8590,6 +8734,7 @@ init_core();
|
|
|
8590
8734
|
DocType,
|
|
8591
8735
|
DocTypePrefixes,
|
|
8592
8736
|
GuestUsername,
|
|
8737
|
+
HYDRATION_MARKER_ID,
|
|
8593
8738
|
Loggable,
|
|
8594
8739
|
NavigatorRole,
|
|
8595
8740
|
NavigatorRoles,
|