@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
@@ -1,4 +1,4 @@
1
- import { U as UserDBInterface, a as UserDBReader, C as CourseDBInterface, b as CoursesDBInterface, c as ClassroomDBInterface, A as AdminDBInterface } from './contentSource-CBqZCoGU.cjs';
1
+ import { U as UserDBInterface, a as UserDBReader, C as CourseDBInterface, b as CoursesDBInterface, c as ClassroomDBInterface, A as AdminDBInterface } from './contentSource-D-LQYFyx.cjs';
2
2
 
3
3
  /**
4
4
  * Main factory interface for data access
@@ -1,4 +1,4 @@
1
- import { U as UserDBInterface, a as UserDBReader, C as CourseDBInterface, b as CoursesDBInterface, c as ClassroomDBInterface, A as AdminDBInterface } from './contentSource-CudEz5Tm.js';
1
+ import { U as UserDBInterface, a as UserDBReader, C as CourseDBInterface, b as CoursesDBInterface, c as ClassroomDBInterface, A as AdminDBInterface } from './contentSource-BDRX_YYJ.js';
2
2
 
3
3
  /**
4
4
  * Main factory interface for data access
@@ -1,10 +1,10 @@
1
1
  import { T as TagStub, a as Tag, S as SkuilderCourseData, Q as QualifiedCardID } from '../../types-legacy-4tlwHnXo.cjs';
2
2
  import { Moment } from 'moment';
3
- import { A as AdminDBInterface, g as AssignedContent, h as StudyContentSource, i as StudentClassroomDBInterface, U as UserDBInterface, G as GeneratorResult, T as TeacherClassroomDBInterface, b as CoursesDBInterface, C as CourseDBInterface, d as CourseInfo, D as DataLayerResult, e as ContentNavigationStrategyData, f as ContentNavigator, R as ReplanHints, S as StudySessionItem, j as ScheduledCard } from '../../contentSource-CBqZCoGU.cjs';
4
- export { q as ContentSourceID, k as StudySessionFailedItem, l as StudySessionFailedNewItem, m as StudySessionFailedReviewItem, n as StudySessionNewItem, o as StudySessionReviewItem, r as getStudySource, p as isReview } from '../../contentSource-CBqZCoGU.cjs';
3
+ import { A as AdminDBInterface, g as AssignedContent, h as StudyContentSource, i as StudentClassroomDBInterface, U as UserDBInterface, G as GeneratorResult, T as TeacherClassroomDBInterface, b as CoursesDBInterface, C as CourseDBInterface, d as CourseInfo, D as DataLayerResult, e as ContentNavigationStrategyData, f as ContentNavigator, R as ReplanHints, S as StudySessionItem, j as ScheduledCard } from '../../contentSource-D-LQYFyx.cjs';
4
+ export { q as ContentSourceID, k as StudySessionFailedItem, l as StudySessionFailedNewItem, m as StudySessionFailedReviewItem, n as StudySessionNewItem, o as StudySessionReviewItem, r as getStudySource, p as isReview } from '../../contentSource-D-LQYFyx.cjs';
5
5
  import * as _vue_skuilder_common from '@vue-skuilder/common';
6
6
  import { ClassroomConfig, DataShape, CourseElo, CourseConfig as CourseConfig$1 } from '@vue-skuilder/common';
7
- import { S as SyncStrategy, A as AccountCreationResult, a as AuthenticationResult } from '../../SyncStrategy-BJMZq3WO.cjs';
7
+ import { S as SyncStrategy, A as AccountCreationResult, a as AuthenticationResult } from '../../SyncStrategy-CCU1H81I.cjs';
8
8
 
9
9
  type NamespacedDatashape = string;
10
10
  interface DataShape55 {
@@ -508,11 +508,27 @@ declare class CouchDBSyncStrategy implements SyncStrategy {
508
508
  * before we know what the remote already has, which is the conflict-leaf
509
509
  * problem hydration exists to avoid. Local changes go up when startSync()
510
510
  * takes over.
511
+ *
512
+ * Into an EMPTY local DB this skips deleted documents, because a mirror that
513
+ * never held a document does not need to be told it was removed. That
514
+ * matters more than it sounds: scheduled reviews (`card_review_*`) are
515
+ * created and deleted once per review completed, so the changes feed is
516
+ * dominated by tombstones and grows without bound, while the durable record
517
+ * lives in card history. One real account measured 385 live documents behind
518
+ * 8,370 deletions — an unfiltered pull moved 9,520 documents and took ~20s,
519
+ * blowing the hydration timeout on a phone; filtered, the same pull moves
520
+ * 404 and takes ~2s, reaching a byte-identical local state.
521
+ *
522
+ * The filter runs server-side, so `last_seq` still reports the source's true
523
+ * sequence. Returning it lets startSync() begin the live pull from there
524
+ * instead of re-walking the same history in the background — without that,
525
+ * the cost is merely deferred, not removed.
511
526
  */
512
527
  hydrate(localDB: PouchDB.Database, remoteDB: PouchDB.Database): Promise<{
513
528
  docsWritten: number;
529
+ lastSeq?: string | number;
514
530
  }>;
515
- startSync(localDB: PouchDB.Database, remoteDB: PouchDB.Database): void;
531
+ startSync(localDB: PouchDB.Database, remoteDB: PouchDB.Database, since?: string | number): void;
516
532
  stopSync?(): void;
517
533
  canCreateAccount(): boolean;
518
534
  canAuthenticate(): boolean;
@@ -1,10 +1,10 @@
1
1
  import { T as TagStub, a as Tag, S as SkuilderCourseData, Q as QualifiedCardID } from '../../types-legacy-4tlwHnXo.js';
2
2
  import { Moment } from 'moment';
3
- import { A as AdminDBInterface, g as AssignedContent, h as StudyContentSource, i as StudentClassroomDBInterface, U as UserDBInterface, G as GeneratorResult, T as TeacherClassroomDBInterface, b as CoursesDBInterface, C as CourseDBInterface, d as CourseInfo, D as DataLayerResult, e as ContentNavigationStrategyData, f as ContentNavigator, R as ReplanHints, S as StudySessionItem, j as ScheduledCard } from '../../contentSource-CudEz5Tm.js';
4
- export { q as ContentSourceID, k as StudySessionFailedItem, l as StudySessionFailedNewItem, m as StudySessionFailedReviewItem, n as StudySessionNewItem, o as StudySessionReviewItem, r as getStudySource, p as isReview } from '../../contentSource-CudEz5Tm.js';
3
+ import { A as AdminDBInterface, g as AssignedContent, h as StudyContentSource, i as StudentClassroomDBInterface, U as UserDBInterface, G as GeneratorResult, T as TeacherClassroomDBInterface, b as CoursesDBInterface, C as CourseDBInterface, d as CourseInfo, D as DataLayerResult, e as ContentNavigationStrategyData, f as ContentNavigator, R as ReplanHints, S as StudySessionItem, j as ScheduledCard } from '../../contentSource-BDRX_YYJ.js';
4
+ export { q as ContentSourceID, k as StudySessionFailedItem, l as StudySessionFailedNewItem, m as StudySessionFailedReviewItem, n as StudySessionNewItem, o as StudySessionReviewItem, r as getStudySource, p as isReview } from '../../contentSource-BDRX_YYJ.js';
5
5
  import * as _vue_skuilder_common from '@vue-skuilder/common';
6
6
  import { ClassroomConfig, DataShape, CourseElo, CourseConfig as CourseConfig$1 } from '@vue-skuilder/common';
7
- import { S as SyncStrategy, A as AccountCreationResult, a as AuthenticationResult } from '../../SyncStrategy-BJMZq3WO.js';
7
+ import { S as SyncStrategy, A as AccountCreationResult, a as AuthenticationResult } from '../../SyncStrategy-CCU1H81I.js';
8
8
 
9
9
  type NamespacedDatashape = string;
10
10
  interface DataShape55 {
@@ -508,11 +508,27 @@ declare class CouchDBSyncStrategy implements SyncStrategy {
508
508
  * before we know what the remote already has, which is the conflict-leaf
509
509
  * problem hydration exists to avoid. Local changes go up when startSync()
510
510
  * takes over.
511
+ *
512
+ * Into an EMPTY local DB this skips deleted documents, because a mirror that
513
+ * never held a document does not need to be told it was removed. That
514
+ * matters more than it sounds: scheduled reviews (`card_review_*`) are
515
+ * created and deleted once per review completed, so the changes feed is
516
+ * dominated by tombstones and grows without bound, while the durable record
517
+ * lives in card history. One real account measured 385 live documents behind
518
+ * 8,370 deletions — an unfiltered pull moved 9,520 documents and took ~20s,
519
+ * blowing the hydration timeout on a phone; filtered, the same pull moves
520
+ * 404 and takes ~2s, reaching a byte-identical local state.
521
+ *
522
+ * The filter runs server-side, so `last_seq` still reports the source's true
523
+ * sequence. Returning it lets startSync() begin the live pull from there
524
+ * instead of re-walking the same history in the background — without that,
525
+ * the cost is merely deferred, not removed.
511
526
  */
512
527
  hydrate(localDB: PouchDB.Database, remoteDB: PouchDB.Database): Promise<{
513
528
  docsWritten: number;
529
+ lastSeq?: string | number;
514
530
  }>;
515
- startSync(localDB: PouchDB.Database, remoteDB: PouchDB.Database): void;
531
+ startSync(localDB: PouchDB.Database, remoteDB: PouchDB.Database, since?: string | number): void;
516
532
  stopSync?(): void;
517
533
  canCreateAccount(): boolean;
518
534
  canAuthenticate(): boolean;
@@ -7561,16 +7561,44 @@ var init_BaseUserDB = __esm({
7561
7561
  // Database to use for write operations (local-first approach)
7562
7562
  updateQueue;
7563
7563
  _hydration = { state: "not-required" };
7564
+ /** In-flight hydration, so concurrent callers can wait for a real answer. */
7565
+ _hydrationPromise = null;
7564
7566
  /**
7565
- * How far the local mirror can be trusted. See {@link UserHydrationStatus}.
7567
+ * Sequence the local mirror was filled to by a hydration that succeeded in
7568
+ * THIS init(), handed to startSync() so the live pull can skip history it
7569
+ * would otherwise re-walk. Undefined whenever there is no such guarantee —
7570
+ * hydration skipped, not required, or failed. Never persisted; see the
7571
+ * `since` note in CouchDBSyncStrategy.startSync().
7572
+ */
7573
+ _hydratedSeq;
7574
+ /**
7575
+ * How far the local mirror can be trusted, RIGHT NOW. See
7576
+ * {@link UserHydrationStatus}.
7566
7577
  *
7567
- * Consumers that would otherwise read a 404 as "new user" — onboarding
7568
- * gates, first-run experiences, progress dashboards should check for
7569
- * `failed` and present a retry affordance rather than an empty state.
7578
+ * This is a snapshot, and during login it can legitimately read
7579
+ * `hydrating` prefer awaitHydration() anywhere a decision hangs on the
7580
+ * answer.
7570
7581
  */
7571
7582
  hydrationStatus() {
7572
7583
  return { ...this._hydration };
7573
7584
  }
7585
+ /**
7586
+ * The hydration status once it has settled — never `hydrating`.
7587
+ *
7588
+ * Resolves immediately unless a pull is in flight. Exists for callers that
7589
+ * must decide something (a route guard, a first-run branch) and would
7590
+ * otherwise sample `hydrating` and guess. Since init() is awaited by both
7591
+ * app startup and login(), that only happens when something navigates
7592
+ * concurrently with a login still in progress — a window that stretches to
7593
+ * HYDRATION_TIMEOUT_MS on a slow connection.
7594
+ */
7595
+ async awaitHydration() {
7596
+ try {
7597
+ await this._hydrationPromise;
7598
+ } catch {
7599
+ }
7600
+ return this.hydrationStatus();
7601
+ }
7574
7602
  /**
7575
7603
  * Whether a missing document may be treated as genuinely absent, allowing
7576
7604
  * callers to create it with defaults.
@@ -8027,8 +8055,9 @@ Currently logged-in as ${this._username}.`
8027
8055
  }
8028
8056
  this.syncStrategy.stopSync?.();
8029
8057
  this.setDBandQ();
8030
- await this.hydrateLocalMirror();
8031
- this.syncStrategy.startSync(this.localDB, this.remoteDB);
8058
+ this._hydrationPromise = this.hydrateLocalMirror();
8059
+ await this._hydrationPromise;
8060
+ this.syncStrategy.startSync(this.localDB, this.remoteDB, this._hydratedSeq);
8032
8061
  this.applyDesignDocs().catch((error) => {
8033
8062
  log3(`Error in applyDesignDocs background task: ${error}`);
8034
8063
  if (error && typeof error === "object") {
@@ -8056,6 +8085,7 @@ Currently logged-in as ${this._username}.`
8056
8085
  * ceiling). Callers decide what a failure means for them.
8057
8086
  */
8058
8087
  async hydrateLocalMirror() {
8088
+ this._hydratedSeq = void 0;
8059
8089
  if (this.localDB.name === this.remoteDB.name || !this.syncStrategy.hydrate) {
8060
8090
  this._hydration = { state: "not-required" };
8061
8091
  return;
@@ -8067,11 +8097,12 @@ Currently logged-in as ${this._username}.`
8067
8097
  this._hydration = { state: "hydrating" };
8068
8098
  const start = Date.now();
8069
8099
  try {
8070
- const { docsWritten } = await withTimeout(
8100
+ const { docsWritten, lastSeq } = await withTimeout(
8071
8101
  this.syncStrategy.hydrate(this.localDB, this.remoteDB),
8072
8102
  HYDRATION_TIMEOUT_MS,
8073
8103
  `Hydration of local mirror for ${this._username}`
8074
8104
  );
8105
+ this._hydratedSeq = lastSeq;
8075
8106
  await this.writeHydrationMarker();
8076
8107
  this._hydration = {
8077
8108
  state: "hydrated",
@@ -9010,22 +9041,60 @@ var init_CouchDBSyncStrategy = __esm({
9010
9041
  * before we know what the remote already has, which is the conflict-leaf
9011
9042
  * problem hydration exists to avoid. Local changes go up when startSync()
9012
9043
  * takes over.
9044
+ *
9045
+ * Into an EMPTY local DB this skips deleted documents, because a mirror that
9046
+ * never held a document does not need to be told it was removed. That
9047
+ * matters more than it sounds: scheduled reviews (`card_review_*`) are
9048
+ * created and deleted once per review completed, so the changes feed is
9049
+ * dominated by tombstones and grows without bound, while the durable record
9050
+ * lives in card history. One real account measured 385 live documents behind
9051
+ * 8,370 deletions — an unfiltered pull moved 9,520 documents and took ~20s,
9052
+ * blowing the hydration timeout on a phone; filtered, the same pull moves
9053
+ * 404 and takes ~2s, reaching a byte-identical local state.
9054
+ *
9055
+ * The filter runs server-side, so `last_seq` still reports the source's true
9056
+ * sequence. Returning it lets startSync() begin the live pull from there
9057
+ * instead of re-walking the same history in the background — without that,
9058
+ * the cost is merely deferred, not removed.
9013
9059
  */
9014
9060
  async hydrate(localDB, remoteDB) {
9015
- const replication = pouchdb_setup_default.replicate(remoteDB, localDB, {});
9061
+ const pristine = (await localDB.info()).doc_count === 0;
9062
+ const replication = pouchdb_setup_default.replicate(
9063
+ remoteDB,
9064
+ localDB,
9065
+ pristine ? { selector: { _deleted: { $exists: false } } } : {}
9066
+ );
9016
9067
  this.hydrationHandle = replication;
9017
9068
  try {
9018
9069
  const info = await replication;
9019
- return { docsWritten: info.docs_written };
9070
+ return {
9071
+ docsWritten: info.docs_written,
9072
+ // Only a pristine pull is a trustworthy starting point for the live
9073
+ // sync; otherwise leave startSync() to its own checkpoint.
9074
+ lastSeq: pristine ? info.last_seq : void 0
9075
+ };
9020
9076
  } finally {
9021
9077
  this.hydrationHandle = void 0;
9022
9078
  }
9023
9079
  }
9024
- startSync(localDB, remoteDB) {
9080
+ startSync(localDB, remoteDB, since) {
9025
9081
  if (localDB.name !== remoteDB.name) {
9026
9082
  this.syncHandle = pouchdb_setup_default.sync(localDB, remoteDB, {
9027
9083
  live: true,
9028
- retry: true
9084
+ retry: true,
9085
+ // `since` applies to the pull only, and only when hydrate() just
9086
+ // established that local matches remote at that sequence. Without it,
9087
+ // a filtered hydration leaves the pull with no usable checkpoint (the
9088
+ // filter changes the replication id), so it restarts from zero and
9089
+ // re-walks in the background exactly the tombstone history hydration
9090
+ // just skipped — same work, now competing with the study session.
9091
+ //
9092
+ // Deliberately NOT persisted across launches: on later boots hydration
9093
+ // is skipped and the pull's own checkpoint is the correct resume
9094
+ // point, so a stored sequence could only be stale. If the app dies
9095
+ // before that first checkpoint is written, the next launch simply
9096
+ // walks the full feed once.
9097
+ ...since !== void 0 ? { pull: { since } } : {}
9029
9098
  });
9030
9099
  }
9031
9100
  }