@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.
Files changed (38) hide show
  1. package/dist/{SyncStrategy-CyATpyLQ.d.cts → SyncStrategy-BJMZq3WO.d.cts} +17 -0
  2. package/dist/{SyncStrategy-CyATpyLQ.d.ts → SyncStrategy-BJMZq3WO.d.ts} +17 -0
  3. package/dist/{contentSource-Brz42x7n.d.cts → contentSource-CBqZCoGU.d.cts} +85 -1
  4. package/dist/{contentSource-B1p-vdz7.d.ts → contentSource-CudEz5Tm.d.ts} +85 -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 +146 -1
  8. package/dist/core/index.js.map +1 -1
  9. package/dist/core/index.mjs +145 -1
  10. package/dist/core/index.mjs.map +1 -1
  11. package/dist/{dataLayerProvider-CpwpT1rM.d.cts → dataLayerProvider-BBA8tJNx.d.cts} +1 -1
  12. package/dist/{dataLayerProvider-BWayUIoK.d.ts → dataLayerProvider-C9WgkBzR.d.ts} +1 -1
  13. package/dist/impl/couch/index.d.cts +16 -3
  14. package/dist/impl/couch/index.d.ts +16 -3
  15. package/dist/impl/couch/index.js +178 -3
  16. package/dist/impl/couch/index.js.map +1 -1
  17. package/dist/impl/couch/index.mjs +178 -3
  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 +144 -1
  22. package/dist/impl/static/index.js.map +1 -1
  23. package/dist/impl/static/index.mjs +144 -1
  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 +180 -3
  28. package/dist/index.js.map +1 -1
  29. package/dist/index.mjs +179 -3
  30. package/dist/index.mjs.map +1 -1
  31. package/package.json +4 -3
  32. package/src/core/index.ts +1 -0
  33. package/src/core/interfaces/userDB.ts +11 -0
  34. package/src/core/types/hydration.ts +78 -0
  35. package/src/impl/common/BaseUserDB.ts +202 -2
  36. package/src/impl/common/SyncStrategy.ts +19 -0
  37. package/src/impl/couch/CouchDBSyncStrategy.ts +54 -4
  38. package/tests/impl/hydration.test.ts +281 -0
@@ -1,9 +1,9 @@
1
- import { U as UserDBInterface, C as CourseDBInterface, b as CoursesDBInterface, c as ClassroomDBInterface, A as AdminDBInterface, a as UserDBReader, d as CourseInfo, S as StudySessionItem, D as DataLayerResult, e as ContentNavigationStrategyData, f as ContentNavigator, R as ReplanHints, G as GeneratorResult } from '../../contentSource-Brz42x7n.cjs';
2
- import { D as DataLayerProvider } from '../../dataLayerProvider-CpwpT1rM.cjs';
1
+ import { U as UserDBInterface, C as CourseDBInterface, b as CoursesDBInterface, c as ClassroomDBInterface, A as AdminDBInterface, a as UserDBReader, d as CourseInfo, S as StudySessionItem, D as DataLayerResult, e as ContentNavigationStrategyData, f as ContentNavigator, R as ReplanHints, G as GeneratorResult } from '../../contentSource-CBqZCoGU.cjs';
2
+ import { D as DataLayerProvider } from '../../dataLayerProvider-BBA8tJNx.cjs';
3
3
  import { S as StaticCourseManifest } from '../../types-BFUa1pa3.cjs';
4
4
  import { CourseConfig, CourseElo, DataShape } from '@vue-skuilder/common';
5
5
  import { S as SkuilderCourseData, Q as QualifiedCardID, T as TagStub, a as Tag } from '../../types-legacy-4tlwHnXo.cjs';
6
- import { S as SyncStrategy, A as AccountCreationResult, a as AuthenticationResult } from '../../SyncStrategy-CyATpyLQ.cjs';
6
+ import { S as SyncStrategy, A as AccountCreationResult, a as AuthenticationResult } from '../../SyncStrategy-BJMZq3WO.cjs';
7
7
  import 'moment';
8
8
 
9
9
  interface SkuilderManifest {
@@ -1,9 +1,9 @@
1
- import { U as UserDBInterface, C as CourseDBInterface, b as CoursesDBInterface, c as ClassroomDBInterface, A as AdminDBInterface, a as UserDBReader, d as CourseInfo, S as StudySessionItem, D as DataLayerResult, e as ContentNavigationStrategyData, f as ContentNavigator, R as ReplanHints, G as GeneratorResult } from '../../contentSource-B1p-vdz7.js';
2
- import { D as DataLayerProvider } from '../../dataLayerProvider-BWayUIoK.js';
1
+ import { U as UserDBInterface, C as CourseDBInterface, b as CoursesDBInterface, c as ClassroomDBInterface, A as AdminDBInterface, a as UserDBReader, d as CourseInfo, S as StudySessionItem, D as DataLayerResult, e as ContentNavigationStrategyData, f as ContentNavigator, R as ReplanHints, G as GeneratorResult } from '../../contentSource-CudEz5Tm.js';
2
+ import { D as DataLayerProvider } from '../../dataLayerProvider-C9WgkBzR.js';
3
3
  import { S as StaticCourseManifest } from '../../types-CHgpWQAY.js';
4
4
  import { CourseConfig, CourseElo, DataShape } from '@vue-skuilder/common';
5
5
  import { S as SkuilderCourseData, Q as QualifiedCardID, T as TagStub, a as Tag } from '../../types-legacy-4tlwHnXo.js';
6
- import { S as SyncStrategy, A as AccountCreationResult, a as AuthenticationResult } from '../../SyncStrategy-CyATpyLQ.js';
6
+ import { S as SyncStrategy, A as AccountCreationResult, a as AuthenticationResult } from '../../SyncStrategy-BJMZq3WO.js';
7
7
  import 'moment';
8
8
 
9
9
  interface SkuilderManifest {
@@ -5391,6 +5391,19 @@ var init_couch = __esm({
5391
5391
  });
5392
5392
 
5393
5393
  // src/impl/common/BaseUserDB.ts
5394
+ function withTimeout(p, ms, label) {
5395
+ let timer;
5396
+ return Promise.race([
5397
+ p,
5398
+ new Promise((_resolve, reject) => {
5399
+ timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
5400
+ })
5401
+ ]).finally(() => {
5402
+ if (timer !== void 0) {
5403
+ clearTimeout(timer);
5404
+ }
5405
+ });
5406
+ }
5394
5407
  function accomodateGuest() {
5395
5408
  logger.log("[funnel] accomodateGuest() called");
5396
5409
  if (typeof localStorage === "undefined") {
@@ -5564,7 +5577,7 @@ async function dropUserFromClassroom(user, classID) {
5564
5577
  async function getUserClassrooms(user) {
5565
5578
  return getOrCreateClassroomRegistrationsDoc(user);
5566
5579
  }
5567
- var import_common12, import_moment6, log3, BaseUser, userCoursesDoc, userClassroomsDoc;
5580
+ var import_common12, import_moment6, log3, HYDRATION_TIMEOUT_MS, BaseUser, userCoursesDoc, userClassroomsDoc;
5568
5581
  var init_BaseUserDB = __esm({
5569
5582
  "src/impl/common/BaseUserDB.ts"() {
5570
5583
  "use strict";
@@ -5581,6 +5594,7 @@ var init_BaseUserDB = __esm({
5581
5594
  log3 = (s) => {
5582
5595
  logger.info(s);
5583
5596
  };
5597
+ HYDRATION_TIMEOUT_MS = 15e3;
5584
5598
  BaseUser = class _BaseUser {
5585
5599
  static _instance;
5586
5600
  static _initialized = false;
@@ -5609,6 +5623,29 @@ var init_BaseUserDB = __esm({
5609
5623
  writeDB;
5610
5624
  // Database to use for write operations (local-first approach)
5611
5625
  updateQueue;
5626
+ _hydration = { state: "not-required" };
5627
+ /**
5628
+ * How far the local mirror can be trusted. See {@link UserHydrationStatus}.
5629
+ *
5630
+ * Consumers that would otherwise read a 404 as "new user" — onboarding
5631
+ * gates, first-run experiences, progress dashboards — should check for
5632
+ * `failed` and present a retry affordance rather than an empty state.
5633
+ */
5634
+ hydrationStatus() {
5635
+ return { ...this._hydration };
5636
+ }
5637
+ /**
5638
+ * Whether a missing document may be treated as genuinely absent, allowing
5639
+ * callers to create it with defaults.
5640
+ *
5641
+ * False only when hydration failed or is still running: a 404 then may just
5642
+ * mean "not pulled down yet", and materializing a default would both lie to
5643
+ * the user and, once live sync starts, push a rev-1 document that loses to
5644
+ * the real one — silently discarding it.
5645
+ */
5646
+ canMaterializeDefaults() {
5647
+ return this._hydration.state !== "failed" && this._hydration.state !== "hydrating";
5648
+ }
5612
5649
  async createAccount(username, password) {
5613
5650
  if (!this.syncStrategy.canCreateAccount()) {
5614
5651
  throw new Error("Account creation not supported by current sync strategy");
@@ -5726,6 +5763,11 @@ Currently logged-in as ${this._username}.`
5726
5763
  } catch (e) {
5727
5764
  const err = e;
5728
5765
  if (err.status === 404) {
5766
+ if (!this.canMaterializeDefaults()) {
5767
+ throw new Error(
5768
+ `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.`
5769
+ );
5770
+ }
5729
5771
  await this.localDB.put({
5730
5772
  _id: _BaseUser.DOC_IDS.COURSE_REGISTRATIONS,
5731
5773
  courses: [],
@@ -5968,6 +6010,11 @@ Currently logged-in as ${this._username}.`
5968
6010
  } catch (e) {
5969
6011
  const err = e;
5970
6012
  if (err.name && err.name === "not_found") {
6013
+ if (!this.canMaterializeDefaults()) {
6014
+ throw new Error(
6015
+ `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.`
6016
+ );
6017
+ }
5971
6018
  await this.localDB.put(defaultConfig);
5972
6019
  return this.getConfig();
5973
6020
  } else {
@@ -6041,7 +6088,9 @@ Currently logged-in as ${this._username}.`
6041
6088
  _BaseUser._initialized = true;
6042
6089
  return;
6043
6090
  }
6091
+ this.syncStrategy.stopSync?.();
6044
6092
  this.setDBandQ();
6093
+ await this.hydrateLocalMirror();
6045
6094
  this.syncStrategy.startSync(this.localDB, this.remoteDB);
6046
6095
  this.applyDesignDocs().catch((error) => {
6047
6096
  log3(`Error in applyDesignDocs background task: ${error}`);
@@ -6057,6 +6106,85 @@ Currently logged-in as ${this._username}.`
6057
6106
  });
6058
6107
  _BaseUser._initialized = true;
6059
6108
  }
6109
+ /**
6110
+ * Pull this account's documents into the local mirror, if that hasn't
6111
+ * happened on this device before.
6112
+ *
6113
+ * Runs between setDBandQ() and startSync() — after the handles exist, before
6114
+ * anything can observe an empty local DB or replicate one upward.
6115
+ *
6116
+ * Never throws: a failure is recorded as `failed` state and reported through
6117
+ * hydrationStatus(), because throwing here would abort init() and leave
6118
+ * BaseUser._initialized false forever (BaseUser.instance() polls it with no
6119
+ * ceiling). Callers decide what a failure means for them.
6120
+ */
6121
+ async hydrateLocalMirror() {
6122
+ if (this.localDB.name === this.remoteDB.name || !this.syncStrategy.hydrate) {
6123
+ this._hydration = { state: "not-required" };
6124
+ return;
6125
+ }
6126
+ if (await this.hasHydrationMarker()) {
6127
+ this._hydration = { state: "stale" };
6128
+ return;
6129
+ }
6130
+ this._hydration = { state: "hydrating" };
6131
+ const start = Date.now();
6132
+ try {
6133
+ const { docsWritten } = await withTimeout(
6134
+ this.syncStrategy.hydrate(this.localDB, this.remoteDB),
6135
+ HYDRATION_TIMEOUT_MS,
6136
+ `Hydration of local mirror for ${this._username}`
6137
+ );
6138
+ await this.writeHydrationMarker();
6139
+ this._hydration = {
6140
+ state: "hydrated",
6141
+ docsWritten,
6142
+ durationMs: Date.now() - start
6143
+ };
6144
+ log3(
6145
+ `Hydrated local mirror for ${this._username}: ${docsWritten} docs in ${this._hydration.durationMs}ms`
6146
+ );
6147
+ } catch (e) {
6148
+ this.syncStrategy.stopSync?.();
6149
+ this._hydration = {
6150
+ state: "failed",
6151
+ durationMs: Date.now() - start,
6152
+ error: e instanceof Error ? e.message : String(e)
6153
+ };
6154
+ logger.error(`Failed to hydrate local mirror for ${this._username}:`, e);
6155
+ }
6156
+ }
6157
+ /**
6158
+ * Has a full pull completed on this device for the CURRENT account?
6159
+ *
6160
+ * The marker is a `_local/` document, which never replicates — so its
6161
+ * presence describes this device's mirror and cannot arrive from elsewhere.
6162
+ */
6163
+ async hasHydrationMarker() {
6164
+ try {
6165
+ const marker = await this.localDB.get(HYDRATION_MARKER_ID);
6166
+ return marker.username === this._username;
6167
+ } catch {
6168
+ return false;
6169
+ }
6170
+ }
6171
+ async writeHydrationMarker() {
6172
+ try {
6173
+ let existingRev;
6174
+ try {
6175
+ existingRev = (await this.localDB.get(HYDRATION_MARKER_ID))._rev;
6176
+ } catch {
6177
+ }
6178
+ await this.localDB.put({
6179
+ _id: HYDRATION_MARKER_ID,
6180
+ _rev: existingRev,
6181
+ username: this._username,
6182
+ hydratedAt: (/* @__PURE__ */ new Date()).toISOString()
6183
+ });
6184
+ } catch (e) {
6185
+ logger.warn(`Could not write hydration marker for ${this._username}: ${e}`);
6186
+ }
6187
+ }
6060
6188
  static designDocs = [
6061
6189
  {
6062
6190
  _id: "_design/reviewCards",
@@ -6393,6 +6521,11 @@ Currently logged-in as ${this._username}.`
6393
6521
  } catch (e) {
6394
6522
  const err = e;
6395
6523
  if (err.status === 404) {
6524
+ if (!this.canMaterializeDefaults()) {
6525
+ throw new Error(
6526
+ `Cannot read strategy state '${strategyKey}' for ${this._username}: the local mirror is unavailable (hydration state '${this._hydration.state}').`
6527
+ );
6528
+ }
6396
6529
  return null;
6397
6530
  }
6398
6531
  throw e;
@@ -6568,6 +6701,15 @@ var init_userOutcome = __esm({
6568
6701
  }
6569
6702
  });
6570
6703
 
6704
+ // src/core/types/hydration.ts
6705
+ var HYDRATION_MARKER_ID;
6706
+ var init_hydration = __esm({
6707
+ "src/core/types/hydration.ts"() {
6708
+ "use strict";
6709
+ HYDRATION_MARKER_ID = "_local/hydration";
6710
+ }
6711
+ });
6712
+
6571
6713
  // src/core/bulkImport/cardProcessor.ts
6572
6714
  var import_common16;
6573
6715
  var init_cardProcessor = __esm({
@@ -6937,6 +7079,7 @@ var init_core = __esm({
6937
7079
  init_user();
6938
7080
  init_strategyState();
6939
7081
  init_userOutcome();
7082
+ init_hydration();
6940
7083
  init_Loggable();
6941
7084
  init_util();
6942
7085
  init_navigators();