@vue-skuilder/db 0.2.17 → 0.2.19

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-B1p-vdz7.d.ts → contentSource-BDRX_YYJ.d.ts} +97 -1
  4. package/dist/{contentSource-Brz42x7n.d.cts → contentSource-D-LQYFyx.d.cts} +97 -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 +167 -1
  8. package/dist/core/index.js.map +1 -1
  9. package/dist/core/index.mjs +166 -1
  10. package/dist/core/index.mjs.map +1 -1
  11. package/dist/{dataLayerProvider-CpwpT1rM.d.cts → dataLayerProvider-Dd2UcCif.d.cts} +1 -1
  12. package/dist/{dataLayerProvider-BWayUIoK.d.ts → dataLayerProvider-Dpshuql4.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 +199 -3
  16. package/dist/impl/couch/index.js.map +1 -1
  17. package/dist/impl/couch/index.mjs +199 -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 +165 -1
  22. package/dist/impl/static/index.js.map +1 -1
  23. package/dist/impl/static/index.mjs +165 -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 +201 -3
  28. package/dist/index.js.map +1 -1
  29. package/dist/index.mjs +200 -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 +24 -0
  34. package/src/core/types/hydration.ts +78 -0
  35. package/src/impl/common/BaseUserDB.ts +233 -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 +330 -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-D-LQYFyx.cjs';
2
+ import { D as DataLayerProvider } from '../../dataLayerProvider-Dd2UcCif.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-BDRX_YYJ.js';
2
+ import { D as DataLayerProvider } from '../../dataLayerProvider-Dpshuql4.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,49 @@ 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
+ /** In-flight hydration, so concurrent callers can wait for a real answer. */
5628
+ _hydrationPromise = null;
5629
+ /**
5630
+ * How far the local mirror can be trusted, RIGHT NOW. See
5631
+ * {@link UserHydrationStatus}.
5632
+ *
5633
+ * This is a snapshot, and during login it can legitimately read
5634
+ * `hydrating` — prefer awaitHydration() anywhere a decision hangs on the
5635
+ * answer.
5636
+ */
5637
+ hydrationStatus() {
5638
+ return { ...this._hydration };
5639
+ }
5640
+ /**
5641
+ * The hydration status once it has settled — never `hydrating`.
5642
+ *
5643
+ * Resolves immediately unless a pull is in flight. Exists for callers that
5644
+ * must decide something (a route guard, a first-run branch) and would
5645
+ * otherwise sample `hydrating` and guess. Since init() is awaited by both
5646
+ * app startup and login(), that only happens when something navigates
5647
+ * concurrently with a login still in progress — a window that stretches to
5648
+ * HYDRATION_TIMEOUT_MS on a slow connection.
5649
+ */
5650
+ async awaitHydration() {
5651
+ try {
5652
+ await this._hydrationPromise;
5653
+ } catch {
5654
+ }
5655
+ return this.hydrationStatus();
5656
+ }
5657
+ /**
5658
+ * Whether a missing document may be treated as genuinely absent, allowing
5659
+ * callers to create it with defaults.
5660
+ *
5661
+ * False only when hydration failed or is still running: a 404 then may just
5662
+ * mean "not pulled down yet", and materializing a default would both lie to
5663
+ * the user and, once live sync starts, push a rev-1 document that loses to
5664
+ * the real one — silently discarding it.
5665
+ */
5666
+ canMaterializeDefaults() {
5667
+ return this._hydration.state !== "failed" && this._hydration.state !== "hydrating";
5668
+ }
5612
5669
  async createAccount(username, password) {
5613
5670
  if (!this.syncStrategy.canCreateAccount()) {
5614
5671
  throw new Error("Account creation not supported by current sync strategy");
@@ -5726,6 +5783,11 @@ Currently logged-in as ${this._username}.`
5726
5783
  } catch (e) {
5727
5784
  const err = e;
5728
5785
  if (err.status === 404) {
5786
+ if (!this.canMaterializeDefaults()) {
5787
+ throw new Error(
5788
+ `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.`
5789
+ );
5790
+ }
5729
5791
  await this.localDB.put({
5730
5792
  _id: _BaseUser.DOC_IDS.COURSE_REGISTRATIONS,
5731
5793
  courses: [],
@@ -5968,6 +6030,11 @@ Currently logged-in as ${this._username}.`
5968
6030
  } catch (e) {
5969
6031
  const err = e;
5970
6032
  if (err.name && err.name === "not_found") {
6033
+ if (!this.canMaterializeDefaults()) {
6034
+ throw new Error(
6035
+ `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.`
6036
+ );
6037
+ }
5971
6038
  await this.localDB.put(defaultConfig);
5972
6039
  return this.getConfig();
5973
6040
  } else {
@@ -6041,7 +6108,10 @@ Currently logged-in as ${this._username}.`
6041
6108
  _BaseUser._initialized = true;
6042
6109
  return;
6043
6110
  }
6111
+ this.syncStrategy.stopSync?.();
6044
6112
  this.setDBandQ();
6113
+ this._hydrationPromise = this.hydrateLocalMirror();
6114
+ await this._hydrationPromise;
6045
6115
  this.syncStrategy.startSync(this.localDB, this.remoteDB);
6046
6116
  this.applyDesignDocs().catch((error) => {
6047
6117
  log3(`Error in applyDesignDocs background task: ${error}`);
@@ -6057,6 +6127,85 @@ Currently logged-in as ${this._username}.`
6057
6127
  });
6058
6128
  _BaseUser._initialized = true;
6059
6129
  }
6130
+ /**
6131
+ * Pull this account's documents into the local mirror, if that hasn't
6132
+ * happened on this device before.
6133
+ *
6134
+ * Runs between setDBandQ() and startSync() — after the handles exist, before
6135
+ * anything can observe an empty local DB or replicate one upward.
6136
+ *
6137
+ * Never throws: a failure is recorded as `failed` state and reported through
6138
+ * hydrationStatus(), because throwing here would abort init() and leave
6139
+ * BaseUser._initialized false forever (BaseUser.instance() polls it with no
6140
+ * ceiling). Callers decide what a failure means for them.
6141
+ */
6142
+ async hydrateLocalMirror() {
6143
+ if (this.localDB.name === this.remoteDB.name || !this.syncStrategy.hydrate) {
6144
+ this._hydration = { state: "not-required" };
6145
+ return;
6146
+ }
6147
+ if (await this.hasHydrationMarker()) {
6148
+ this._hydration = { state: "stale" };
6149
+ return;
6150
+ }
6151
+ this._hydration = { state: "hydrating" };
6152
+ const start = Date.now();
6153
+ try {
6154
+ const { docsWritten } = await withTimeout(
6155
+ this.syncStrategy.hydrate(this.localDB, this.remoteDB),
6156
+ HYDRATION_TIMEOUT_MS,
6157
+ `Hydration of local mirror for ${this._username}`
6158
+ );
6159
+ await this.writeHydrationMarker();
6160
+ this._hydration = {
6161
+ state: "hydrated",
6162
+ docsWritten,
6163
+ durationMs: Date.now() - start
6164
+ };
6165
+ log3(
6166
+ `Hydrated local mirror for ${this._username}: ${docsWritten} docs in ${this._hydration.durationMs}ms`
6167
+ );
6168
+ } catch (e) {
6169
+ this.syncStrategy.stopSync?.();
6170
+ this._hydration = {
6171
+ state: "failed",
6172
+ durationMs: Date.now() - start,
6173
+ error: e instanceof Error ? e.message : String(e)
6174
+ };
6175
+ logger.error(`Failed to hydrate local mirror for ${this._username}:`, e);
6176
+ }
6177
+ }
6178
+ /**
6179
+ * Has a full pull completed on this device for the CURRENT account?
6180
+ *
6181
+ * The marker is a `_local/` document, which never replicates — so its
6182
+ * presence describes this device's mirror and cannot arrive from elsewhere.
6183
+ */
6184
+ async hasHydrationMarker() {
6185
+ try {
6186
+ const marker = await this.localDB.get(HYDRATION_MARKER_ID);
6187
+ return marker.username === this._username;
6188
+ } catch {
6189
+ return false;
6190
+ }
6191
+ }
6192
+ async writeHydrationMarker() {
6193
+ try {
6194
+ let existingRev;
6195
+ try {
6196
+ existingRev = (await this.localDB.get(HYDRATION_MARKER_ID))._rev;
6197
+ } catch {
6198
+ }
6199
+ await this.localDB.put({
6200
+ _id: HYDRATION_MARKER_ID,
6201
+ _rev: existingRev,
6202
+ username: this._username,
6203
+ hydratedAt: (/* @__PURE__ */ new Date()).toISOString()
6204
+ });
6205
+ } catch (e) {
6206
+ logger.warn(`Could not write hydration marker for ${this._username}: ${e}`);
6207
+ }
6208
+ }
6060
6209
  static designDocs = [
6061
6210
  {
6062
6211
  _id: "_design/reviewCards",
@@ -6393,6 +6542,11 @@ Currently logged-in as ${this._username}.`
6393
6542
  } catch (e) {
6394
6543
  const err = e;
6395
6544
  if (err.status === 404) {
6545
+ if (!this.canMaterializeDefaults()) {
6546
+ throw new Error(
6547
+ `Cannot read strategy state '${strategyKey}' for ${this._username}: the local mirror is unavailable (hydration state '${this._hydration.state}').`
6548
+ );
6549
+ }
6396
6550
  return null;
6397
6551
  }
6398
6552
  throw e;
@@ -6568,6 +6722,15 @@ var init_userOutcome = __esm({
6568
6722
  }
6569
6723
  });
6570
6724
 
6725
+ // src/core/types/hydration.ts
6726
+ var HYDRATION_MARKER_ID;
6727
+ var init_hydration = __esm({
6728
+ "src/core/types/hydration.ts"() {
6729
+ "use strict";
6730
+ HYDRATION_MARKER_ID = "_local/hydration";
6731
+ }
6732
+ });
6733
+
6571
6734
  // src/core/bulkImport/cardProcessor.ts
6572
6735
  var import_common16;
6573
6736
  var init_cardProcessor = __esm({
@@ -6937,6 +7100,7 @@ var init_core = __esm({
6937
7100
  init_user();
6938
7101
  init_strategyState();
6939
7102
  init_userOutcome();
7103
+ init_hydration();
6940
7104
  init_Loggable();
6941
7105
  init_util();
6942
7106
  init_navigators();