@vue-skuilder/db 0.2.18 → 0.2.20

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.
Files changed (36) hide show
  1. package/dist/{SyncStrategy-BJMZq3WO.d.cts → SyncStrategy-CCU1H81I.d.cts} +8 -2
  2. package/dist/{SyncStrategy-BJMZq3WO.d.ts → SyncStrategy-CCU1H81I.d.ts} +8 -2
  3. package/dist/{contentSource-CudEz5Tm.d.ts → contentSource-BDRX_YYJ.d.ts} +13 -1
  4. package/dist/{contentSource-CBqZCoGU.d.cts → contentSource-D-LQYFyx.d.cts} +13 -1
  5. package/dist/core/index.d.cts +3 -3
  6. package/dist/core/index.d.ts +3 -3
  7. package/dist/core/index.js +38 -7
  8. package/dist/core/index.js.map +1 -1
  9. package/dist/core/index.mjs +38 -7
  10. package/dist/core/index.mjs.map +1 -1
  11. package/dist/{dataLayerProvider-BBA8tJNx.d.cts → dataLayerProvider-Dd2UcCif.d.cts} +1 -1
  12. package/dist/{dataLayerProvider-C9WgkBzR.d.ts → dataLayerProvider-Dpshuql4.d.ts} +1 -1
  13. package/dist/impl/couch/index.d.cts +20 -4
  14. package/dist/impl/couch/index.d.ts +20 -4
  15. package/dist/impl/couch/index.js +80 -11
  16. package/dist/impl/couch/index.js.map +1 -1
  17. package/dist/impl/couch/index.mjs +80 -11
  18. package/dist/impl/couch/index.mjs.map +1 -1
  19. package/dist/impl/static/index.d.cts +3 -3
  20. package/dist/impl/static/index.d.ts +3 -3
  21. package/dist/impl/static/index.js +38 -7
  22. package/dist/impl/static/index.js.map +1 -1
  23. package/dist/impl/static/index.mjs +38 -7
  24. package/dist/impl/static/index.mjs.map +1 -1
  25. package/dist/index.d.cts +3 -3
  26. package/dist/index.d.ts +3 -3
  27. package/dist/index.js +80 -11
  28. package/dist/index.js.map +1 -1
  29. package/dist/index.mjs +80 -11
  30. package/dist/index.mjs.map +1 -1
  31. package/package.json +3 -3
  32. package/src/core/interfaces/userDB.ts +14 -1
  33. package/src/impl/common/BaseUserDB.ts +56 -8
  34. package/src/impl/common/SyncStrategy.ts +17 -4
  35. package/src/impl/couch/CouchDBSyncStrategy.ts +52 -4
  36. package/tests/impl/hydration.test.ts +49 -0
@@ -48,17 +48,23 @@ interface SyncStrategy {
48
48
  *
49
49
  * @param localDB The local PouchDB instance
50
50
  * @param remoteDB The remote PouchDB instance
51
- * @returns Count of documents written locally
51
+ * @returns Count of documents written locally, and — only when the pull is
52
+ * known to have landed the remote's complete current state — the sequence
53
+ * it corresponds to, for startSync() to resume from. Omitted means "no
54
+ * safe shortcut, walk from the checkpoint".
52
55
  */
53
56
  hydrate?(localDB: PouchDB.Database, remoteDB: PouchDB.Database): Promise<{
54
57
  docsWritten: number;
58
+ lastSeq?: string | number;
55
59
  }>;
56
60
  /**
57
61
  * Start synchronization between local and remote databases
58
62
  * @param localDB The local PouchDB instance
59
63
  * @param remoteDB The remote PouchDB instance
64
+ * @param since Sequence to start the pull from, as returned by a hydrate()
65
+ * in the same session. Omit to resume from the stored checkpoint.
60
66
  */
61
- startSync(localDB: PouchDB.Database, remoteDB: PouchDB.Database): void;
67
+ startSync(localDB: PouchDB.Database, remoteDB: PouchDB.Database, since?: string | number): void;
62
68
  /**
63
69
  * Stop synchronization (optional - for cleanup)
64
70
  */
@@ -48,17 +48,23 @@ interface SyncStrategy {
48
48
  *
49
49
  * @param localDB The local PouchDB instance
50
50
  * @param remoteDB The remote PouchDB instance
51
- * @returns Count of documents written locally
51
+ * @returns Count of documents written locally, and — only when the pull is
52
+ * known to have landed the remote's complete current state — the sequence
53
+ * it corresponds to, for startSync() to resume from. Omitted means "no
54
+ * safe shortcut, walk from the checkpoint".
52
55
  */
53
56
  hydrate?(localDB: PouchDB.Database, remoteDB: PouchDB.Database): Promise<{
54
57
  docsWritten: number;
58
+ lastSeq?: string | number;
55
59
  }>;
56
60
  /**
57
61
  * Start synchronization between local and remote databases
58
62
  * @param localDB The local PouchDB instance
59
63
  * @param remoteDB The remote PouchDB instance
64
+ * @param since Sequence to start the pull from, as returned by a hydrate()
65
+ * in the same session. Omit to resume from the stored checkpoint.
60
66
  */
61
- startSync(localDB: PouchDB.Database, remoteDB: PouchDB.Database): void;
67
+ startSync(localDB: PouchDB.Database, remoteDB: PouchDB.Database, since?: string | number): void;
62
68
  /**
63
69
  * Stop synchronization (optional - for cleanup)
64
70
  */
@@ -514,14 +514,26 @@ interface UserDBReader {
514
514
  getUsername(): string;
515
515
  isLoggedIn(): boolean;
516
516
  /**
517
- * How far this device's local mirror of the user's data can be trusted.
517
+ * How far this device's local mirror of the user's data can be trusted,
518
+ * as of right now.
518
519
  *
519
520
  * Reads answer from the local mirror, so on a device that has never synced
520
521
  * this account "document not found" does not mean "no such data". Anything
521
522
  * that would otherwise interpret an empty read as a new user should check
522
523
  * for `failed` first. See {@link UserHydrationStatus}.
524
+ *
525
+ * Can return `hydrating`; use {@link awaitHydration} to decide on a settled
526
+ * answer instead of a snapshot.
523
527
  */
524
528
  hydrationStatus(): UserHydrationStatus;
529
+ /**
530
+ * The hydration status once settled — never `hydrating`.
531
+ *
532
+ * Resolves immediately unless a pull is in flight. Prefer this over
533
+ * hydrationStatus() wherever the answer gates behaviour, such as a route
534
+ * guard that may run while a login is still completing.
535
+ */
536
+ awaitHydration(): Promise<UserHydrationStatus>;
525
537
  /**
526
538
  * Get user configuration
527
539
  */
@@ -514,14 +514,26 @@ interface UserDBReader {
514
514
  getUsername(): string;
515
515
  isLoggedIn(): boolean;
516
516
  /**
517
- * How far this device's local mirror of the user's data can be trusted.
517
+ * How far this device's local mirror of the user's data can be trusted,
518
+ * as of right now.
518
519
  *
519
520
  * Reads answer from the local mirror, so on a device that has never synced
520
521
  * this account "document not found" does not mean "no such data". Anything
521
522
  * that would otherwise interpret an empty read as a new user should check
522
523
  * for `failed` first. See {@link UserHydrationStatus}.
524
+ *
525
+ * Can return `hydrating`; use {@link awaitHydration} to decide on a settled
526
+ * answer instead of a snapshot.
523
527
  */
524
528
  hydrationStatus(): UserHydrationStatus;
529
+ /**
530
+ * The hydration status once settled — never `hydrating`.
531
+ *
532
+ * Resolves immediately unless a pull is in flight. Prefer this over
533
+ * hydrationStatus() wherever the answer gates behaviour, such as a route
534
+ * guard that may run while a login is still completing.
535
+ */
536
+ awaitHydration(): Promise<UserHydrationStatus>;
525
537
  /**
526
538
  * Get user configuration
527
539
  */
@@ -1,6 +1,6 @@
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';
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-D-LQYFyx.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-D-LQYFyx.cjs';
3
+ export { D as DataLayerProvider } from '../dataLayerProvider-Dd2UcCif.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';
@@ -1,6 +1,6 @@
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';
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-BDRX_YYJ.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-BDRX_YYJ.js';
3
+ export { D as DataLayerProvider } from '../dataLayerProvider-Dpshuql4.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';
@@ -6919,16 +6919,44 @@ var init_BaseUserDB = __esm({
6919
6919
  // Database to use for write operations (local-first approach)
6920
6920
  updateQueue;
6921
6921
  _hydration = { state: "not-required" };
6922
+ /** In-flight hydration, so concurrent callers can wait for a real answer. */
6923
+ _hydrationPromise = null;
6922
6924
  /**
6923
- * How far the local mirror can be trusted. See {@link UserHydrationStatus}.
6925
+ * Sequence the local mirror was filled to by a hydration that succeeded in
6926
+ * THIS init(), handed to startSync() so the live pull can skip history it
6927
+ * would otherwise re-walk. Undefined whenever there is no such guarantee —
6928
+ * hydration skipped, not required, or failed. Never persisted; see the
6929
+ * `since` note in CouchDBSyncStrategy.startSync().
6930
+ */
6931
+ _hydratedSeq;
6932
+ /**
6933
+ * How far the local mirror can be trusted, RIGHT NOW. See
6934
+ * {@link UserHydrationStatus}.
6924
6935
  *
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.
6936
+ * This is a snapshot, and during login it can legitimately read
6937
+ * `hydrating` prefer awaitHydration() anywhere a decision hangs on the
6938
+ * answer.
6928
6939
  */
6929
6940
  hydrationStatus() {
6930
6941
  return { ...this._hydration };
6931
6942
  }
6943
+ /**
6944
+ * The hydration status once it has settled — never `hydrating`.
6945
+ *
6946
+ * Resolves immediately unless a pull is in flight. Exists for callers that
6947
+ * must decide something (a route guard, a first-run branch) and would
6948
+ * otherwise sample `hydrating` and guess. Since init() is awaited by both
6949
+ * app startup and login(), that only happens when something navigates
6950
+ * concurrently with a login still in progress — a window that stretches to
6951
+ * HYDRATION_TIMEOUT_MS on a slow connection.
6952
+ */
6953
+ async awaitHydration() {
6954
+ try {
6955
+ await this._hydrationPromise;
6956
+ } catch {
6957
+ }
6958
+ return this.hydrationStatus();
6959
+ }
6932
6960
  /**
6933
6961
  * Whether a missing document may be treated as genuinely absent, allowing
6934
6962
  * callers to create it with defaults.
@@ -7385,8 +7413,9 @@ Currently logged-in as ${this._username}.`
7385
7413
  }
7386
7414
  this.syncStrategy.stopSync?.();
7387
7415
  this.setDBandQ();
7388
- await this.hydrateLocalMirror();
7389
- this.syncStrategy.startSync(this.localDB, this.remoteDB);
7416
+ this._hydrationPromise = this.hydrateLocalMirror();
7417
+ await this._hydrationPromise;
7418
+ this.syncStrategy.startSync(this.localDB, this.remoteDB, this._hydratedSeq);
7390
7419
  this.applyDesignDocs().catch((error) => {
7391
7420
  log3(`Error in applyDesignDocs background task: ${error}`);
7392
7421
  if (error && typeof error === "object") {
@@ -7414,6 +7443,7 @@ Currently logged-in as ${this._username}.`
7414
7443
  * ceiling). Callers decide what a failure means for them.
7415
7444
  */
7416
7445
  async hydrateLocalMirror() {
7446
+ this._hydratedSeq = void 0;
7417
7447
  if (this.localDB.name === this.remoteDB.name || !this.syncStrategy.hydrate) {
7418
7448
  this._hydration = { state: "not-required" };
7419
7449
  return;
@@ -7425,11 +7455,12 @@ Currently logged-in as ${this._username}.`
7425
7455
  this._hydration = { state: "hydrating" };
7426
7456
  const start = Date.now();
7427
7457
  try {
7428
- const { docsWritten } = await withTimeout(
7458
+ const { docsWritten, lastSeq } = await withTimeout(
7429
7459
  this.syncStrategy.hydrate(this.localDB, this.remoteDB),
7430
7460
  HYDRATION_TIMEOUT_MS,
7431
7461
  `Hydration of local mirror for ${this._username}`
7432
7462
  );
7463
+ this._hydratedSeq = lastSeq;
7433
7464
  await this.writeHydrationMarker();
7434
7465
  this._hydration = {
7435
7466
  state: "hydrated",