@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
@@ -5370,6 +5370,19 @@ var init_couch = __esm({
5370
5370
  // src/impl/common/BaseUserDB.ts
5371
5371
  import { Status as Status3 } from "@vue-skuilder/common";
5372
5372
  import moment6 from "moment";
5373
+ function withTimeout(p, ms, label) {
5374
+ let timer;
5375
+ return Promise.race([
5376
+ p,
5377
+ new Promise((_resolve, reject) => {
5378
+ timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
5379
+ })
5380
+ ]).finally(() => {
5381
+ if (timer !== void 0) {
5382
+ clearTimeout(timer);
5383
+ }
5384
+ });
5385
+ }
5373
5386
  function accomodateGuest() {
5374
5387
  logger.log("[funnel] accomodateGuest() called");
5375
5388
  if (typeof localStorage === "undefined") {
@@ -5543,7 +5556,7 @@ async function dropUserFromClassroom(user, classID) {
5543
5556
  async function getUserClassrooms(user) {
5544
5557
  return getOrCreateClassroomRegistrationsDoc(user);
5545
5558
  }
5546
- var log3, BaseUser, userCoursesDoc, userClassroomsDoc;
5559
+ var log3, HYDRATION_TIMEOUT_MS, BaseUser, userCoursesDoc, userClassroomsDoc;
5547
5560
  var init_BaseUserDB = __esm({
5548
5561
  "src/impl/common/BaseUserDB.ts"() {
5549
5562
  "use strict";
@@ -5558,6 +5571,7 @@ var init_BaseUserDB = __esm({
5558
5571
  log3 = (s) => {
5559
5572
  logger.info(s);
5560
5573
  };
5574
+ HYDRATION_TIMEOUT_MS = 15e3;
5561
5575
  BaseUser = class _BaseUser {
5562
5576
  static _instance;
5563
5577
  static _initialized = false;
@@ -5586,6 +5600,29 @@ var init_BaseUserDB = __esm({
5586
5600
  writeDB;
5587
5601
  // Database to use for write operations (local-first approach)
5588
5602
  updateQueue;
5603
+ _hydration = { state: "not-required" };
5604
+ /**
5605
+ * How far the local mirror can be trusted. See {@link UserHydrationStatus}.
5606
+ *
5607
+ * Consumers that would otherwise read a 404 as "new user" — onboarding
5608
+ * gates, first-run experiences, progress dashboards — should check for
5609
+ * `failed` and present a retry affordance rather than an empty state.
5610
+ */
5611
+ hydrationStatus() {
5612
+ return { ...this._hydration };
5613
+ }
5614
+ /**
5615
+ * Whether a missing document may be treated as genuinely absent, allowing
5616
+ * callers to create it with defaults.
5617
+ *
5618
+ * False only when hydration failed or is still running: a 404 then may just
5619
+ * mean "not pulled down yet", and materializing a default would both lie to
5620
+ * the user and, once live sync starts, push a rev-1 document that loses to
5621
+ * the real one — silently discarding it.
5622
+ */
5623
+ canMaterializeDefaults() {
5624
+ return this._hydration.state !== "failed" && this._hydration.state !== "hydrating";
5625
+ }
5589
5626
  async createAccount(username, password) {
5590
5627
  if (!this.syncStrategy.canCreateAccount()) {
5591
5628
  throw new Error("Account creation not supported by current sync strategy");
@@ -5703,6 +5740,11 @@ Currently logged-in as ${this._username}.`
5703
5740
  } catch (e) {
5704
5741
  const err = e;
5705
5742
  if (err.status === 404) {
5743
+ if (!this.canMaterializeDefaults()) {
5744
+ throw new Error(
5745
+ `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.`
5746
+ );
5747
+ }
5706
5748
  await this.localDB.put({
5707
5749
  _id: _BaseUser.DOC_IDS.COURSE_REGISTRATIONS,
5708
5750
  courses: [],
@@ -5945,6 +5987,11 @@ Currently logged-in as ${this._username}.`
5945
5987
  } catch (e) {
5946
5988
  const err = e;
5947
5989
  if (err.name && err.name === "not_found") {
5990
+ if (!this.canMaterializeDefaults()) {
5991
+ throw new Error(
5992
+ `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.`
5993
+ );
5994
+ }
5948
5995
  await this.localDB.put(defaultConfig);
5949
5996
  return this.getConfig();
5950
5997
  } else {
@@ -6018,7 +6065,9 @@ Currently logged-in as ${this._username}.`
6018
6065
  _BaseUser._initialized = true;
6019
6066
  return;
6020
6067
  }
6068
+ this.syncStrategy.stopSync?.();
6021
6069
  this.setDBandQ();
6070
+ await this.hydrateLocalMirror();
6022
6071
  this.syncStrategy.startSync(this.localDB, this.remoteDB);
6023
6072
  this.applyDesignDocs().catch((error) => {
6024
6073
  log3(`Error in applyDesignDocs background task: ${error}`);
@@ -6034,6 +6083,85 @@ Currently logged-in as ${this._username}.`
6034
6083
  });
6035
6084
  _BaseUser._initialized = true;
6036
6085
  }
6086
+ /**
6087
+ * Pull this account's documents into the local mirror, if that hasn't
6088
+ * happened on this device before.
6089
+ *
6090
+ * Runs between setDBandQ() and startSync() — after the handles exist, before
6091
+ * anything can observe an empty local DB or replicate one upward.
6092
+ *
6093
+ * Never throws: a failure is recorded as `failed` state and reported through
6094
+ * hydrationStatus(), because throwing here would abort init() and leave
6095
+ * BaseUser._initialized false forever (BaseUser.instance() polls it with no
6096
+ * ceiling). Callers decide what a failure means for them.
6097
+ */
6098
+ async hydrateLocalMirror() {
6099
+ if (this.localDB.name === this.remoteDB.name || !this.syncStrategy.hydrate) {
6100
+ this._hydration = { state: "not-required" };
6101
+ return;
6102
+ }
6103
+ if (await this.hasHydrationMarker()) {
6104
+ this._hydration = { state: "stale" };
6105
+ return;
6106
+ }
6107
+ this._hydration = { state: "hydrating" };
6108
+ const start = Date.now();
6109
+ try {
6110
+ const { docsWritten } = await withTimeout(
6111
+ this.syncStrategy.hydrate(this.localDB, this.remoteDB),
6112
+ HYDRATION_TIMEOUT_MS,
6113
+ `Hydration of local mirror for ${this._username}`
6114
+ );
6115
+ await this.writeHydrationMarker();
6116
+ this._hydration = {
6117
+ state: "hydrated",
6118
+ docsWritten,
6119
+ durationMs: Date.now() - start
6120
+ };
6121
+ log3(
6122
+ `Hydrated local mirror for ${this._username}: ${docsWritten} docs in ${this._hydration.durationMs}ms`
6123
+ );
6124
+ } catch (e) {
6125
+ this.syncStrategy.stopSync?.();
6126
+ this._hydration = {
6127
+ state: "failed",
6128
+ durationMs: Date.now() - start,
6129
+ error: e instanceof Error ? e.message : String(e)
6130
+ };
6131
+ logger.error(`Failed to hydrate local mirror for ${this._username}:`, e);
6132
+ }
6133
+ }
6134
+ /**
6135
+ * Has a full pull completed on this device for the CURRENT account?
6136
+ *
6137
+ * The marker is a `_local/` document, which never replicates — so its
6138
+ * presence describes this device's mirror and cannot arrive from elsewhere.
6139
+ */
6140
+ async hasHydrationMarker() {
6141
+ try {
6142
+ const marker = await this.localDB.get(HYDRATION_MARKER_ID);
6143
+ return marker.username === this._username;
6144
+ } catch {
6145
+ return false;
6146
+ }
6147
+ }
6148
+ async writeHydrationMarker() {
6149
+ try {
6150
+ let existingRev;
6151
+ try {
6152
+ existingRev = (await this.localDB.get(HYDRATION_MARKER_ID))._rev;
6153
+ } catch {
6154
+ }
6155
+ await this.localDB.put({
6156
+ _id: HYDRATION_MARKER_ID,
6157
+ _rev: existingRev,
6158
+ username: this._username,
6159
+ hydratedAt: (/* @__PURE__ */ new Date()).toISOString()
6160
+ });
6161
+ } catch (e) {
6162
+ logger.warn(`Could not write hydration marker for ${this._username}: ${e}`);
6163
+ }
6164
+ }
6037
6165
  static designDocs = [
6038
6166
  {
6039
6167
  _id: "_design/reviewCards",
@@ -6370,6 +6498,11 @@ Currently logged-in as ${this._username}.`
6370
6498
  } catch (e) {
6371
6499
  const err = e;
6372
6500
  if (err.status === 404) {
6501
+ if (!this.canMaterializeDefaults()) {
6502
+ throw new Error(
6503
+ `Cannot read strategy state '${strategyKey}' for ${this._username}: the local mirror is unavailable (hydration state '${this._hydration.state}').`
6504
+ );
6505
+ }
6373
6506
  return null;
6374
6507
  }
6375
6508
  throw e;
@@ -6543,6 +6676,15 @@ var init_userOutcome = __esm({
6543
6676
  }
6544
6677
  });
6545
6678
 
6679
+ // src/core/types/hydration.ts
6680
+ var HYDRATION_MARKER_ID;
6681
+ var init_hydration = __esm({
6682
+ "src/core/types/hydration.ts"() {
6683
+ "use strict";
6684
+ HYDRATION_MARKER_ID = "_local/hydration";
6685
+ }
6686
+ });
6687
+
6546
6688
  // src/core/bulkImport/cardProcessor.ts
6547
6689
  import { Status as Status4 } from "@vue-skuilder/common";
6548
6690
  var init_cardProcessor = __esm({
@@ -6911,6 +7053,7 @@ var init_core = __esm({
6911
7053
  init_user();
6912
7054
  init_strategyState();
6913
7055
  init_userOutcome();
7056
+ init_hydration();
6914
7057
  init_Loggable();
6915
7058
  init_util();
6916
7059
  init_navigators();