@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,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-Brz42x7n.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-B1p-vdz7.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-Brz42x7n.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-Brz42x7n.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-CyATpyLQ.cjs';
7
+ import { S as SyncStrategy, A as AccountCreationResult, a as AuthenticationResult } from '../../SyncStrategy-BJMZq3WO.cjs';
8
8
 
9
9
  type NamespacedDatashape = string;
10
10
  interface DataShape55 {
@@ -497,8 +497,21 @@ declare class CourseSyncService {
497
497
  */
498
498
  declare class CouchDBSyncStrategy implements SyncStrategy {
499
499
  private syncHandle?;
500
+ /** In-flight one-shot hydration pull, so a timed-out pull can be abandoned. */
501
+ private hydrationHandle?;
500
502
  setupRemoteDB(username: string): PouchDB.Database;
501
503
  getWriteDB(username: string): PouchDB.Database;
504
+ /**
505
+ * One-shot remote → local pull. Resolves once the local mirror is caught up.
506
+ *
507
+ * Pull only: pushing here would upload whatever the local DB happens to hold
508
+ * before we know what the remote already has, which is the conflict-leaf
509
+ * problem hydration exists to avoid. Local changes go up when startSync()
510
+ * takes over.
511
+ */
512
+ hydrate(localDB: PouchDB.Database, remoteDB: PouchDB.Database): Promise<{
513
+ docsWritten: number;
514
+ }>;
502
515
  startSync(localDB: PouchDB.Database, remoteDB: PouchDB.Database): void;
503
516
  stopSync?(): void;
504
517
  canCreateAccount(): 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-B1p-vdz7.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-B1p-vdz7.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-CyATpyLQ.js';
7
+ import { S as SyncStrategy, A as AccountCreationResult, a as AuthenticationResult } from '../../SyncStrategy-BJMZq3WO.js';
8
8
 
9
9
  type NamespacedDatashape = string;
10
10
  interface DataShape55 {
@@ -497,8 +497,21 @@ declare class CourseSyncService {
497
497
  */
498
498
  declare class CouchDBSyncStrategy implements SyncStrategy {
499
499
  private syncHandle?;
500
+ /** In-flight one-shot hydration pull, so a timed-out pull can be abandoned. */
501
+ private hydrationHandle?;
500
502
  setupRemoteDB(username: string): PouchDB.Database;
501
503
  getWriteDB(username: string): PouchDB.Database;
504
+ /**
505
+ * One-shot remote → local pull. Resolves once the local mirror is caught up.
506
+ *
507
+ * Pull only: pushing here would upload whatever the local DB happens to hold
508
+ * before we know what the remote already has, which is the conflict-leaf
509
+ * problem hydration exists to avoid. Local changes go up when startSync()
510
+ * takes over.
511
+ */
512
+ hydrate(localDB: PouchDB.Database, remoteDB: PouchDB.Database): Promise<{
513
+ docsWritten: number;
514
+ }>;
502
515
  startSync(localDB: PouchDB.Database, remoteDB: PouchDB.Database): void;
503
516
  stopSync?(): void;
504
517
  canCreateAccount(): boolean;
@@ -6748,6 +6748,15 @@ var init_userOutcome = __esm({
6748
6748
  }
6749
6749
  });
6750
6750
 
6751
+ // src/core/types/hydration.ts
6752
+ var HYDRATION_MARKER_ID;
6753
+ var init_hydration = __esm({
6754
+ "src/core/types/hydration.ts"() {
6755
+ "use strict";
6756
+ HYDRATION_MARKER_ID = "_local/hydration";
6757
+ }
6758
+ });
6759
+
6751
6760
  // src/core/util/index.ts
6752
6761
  function getCardHistoryID(courseID, cardID) {
6753
6762
  return `${DocTypePrefixes["CARDRECORD" /* CARDRECORD */]}-${courseID}-${cardID}`;
@@ -7245,6 +7254,7 @@ var init_core = __esm({
7245
7254
  init_user();
7246
7255
  init_strategyState();
7247
7256
  init_userOutcome();
7257
+ init_hydration();
7248
7258
  init_Loggable();
7249
7259
  init_util();
7250
7260
  init_navigators();
@@ -7318,6 +7328,19 @@ var init_user_course_relDB = __esm({
7318
7328
  });
7319
7329
 
7320
7330
  // src/impl/common/BaseUserDB.ts
7331
+ function withTimeout(p, ms, label) {
7332
+ let timer;
7333
+ return Promise.race([
7334
+ p,
7335
+ new Promise((_resolve, reject) => {
7336
+ timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
7337
+ })
7338
+ ]).finally(() => {
7339
+ if (timer !== void 0) {
7340
+ clearTimeout(timer);
7341
+ }
7342
+ });
7343
+ }
7321
7344
  function accomodateGuest() {
7322
7345
  logger.log("[funnel] accomodateGuest() called");
7323
7346
  if (typeof localStorage === "undefined") {
@@ -7491,7 +7514,7 @@ async function dropUserFromClassroom(user, classID) {
7491
7514
  async function getUserClassrooms(user) {
7492
7515
  return getOrCreateClassroomRegistrationsDoc(user);
7493
7516
  }
7494
- var import_common13, import_moment5, log3, BaseUser, userCoursesDoc, userClassroomsDoc;
7517
+ var import_common13, import_moment5, log3, HYDRATION_TIMEOUT_MS, BaseUser, userCoursesDoc, userClassroomsDoc;
7495
7518
  var init_BaseUserDB = __esm({
7496
7519
  "src/impl/common/BaseUserDB.ts"() {
7497
7520
  "use strict";
@@ -7508,6 +7531,7 @@ var init_BaseUserDB = __esm({
7508
7531
  log3 = (s) => {
7509
7532
  logger.info(s);
7510
7533
  };
7534
+ HYDRATION_TIMEOUT_MS = 15e3;
7511
7535
  BaseUser = class _BaseUser {
7512
7536
  static _instance;
7513
7537
  static _initialized = false;
@@ -7536,6 +7560,49 @@ var init_BaseUserDB = __esm({
7536
7560
  writeDB;
7537
7561
  // Database to use for write operations (local-first approach)
7538
7562
  updateQueue;
7563
+ _hydration = { state: "not-required" };
7564
+ /** In-flight hydration, so concurrent callers can wait for a real answer. */
7565
+ _hydrationPromise = null;
7566
+ /**
7567
+ * How far the local mirror can be trusted, RIGHT NOW. See
7568
+ * {@link UserHydrationStatus}.
7569
+ *
7570
+ * This is a snapshot, and during login it can legitimately read
7571
+ * `hydrating` — prefer awaitHydration() anywhere a decision hangs on the
7572
+ * answer.
7573
+ */
7574
+ hydrationStatus() {
7575
+ return { ...this._hydration };
7576
+ }
7577
+ /**
7578
+ * The hydration status once it has settled — never `hydrating`.
7579
+ *
7580
+ * Resolves immediately unless a pull is in flight. Exists for callers that
7581
+ * must decide something (a route guard, a first-run branch) and would
7582
+ * otherwise sample `hydrating` and guess. Since init() is awaited by both
7583
+ * app startup and login(), that only happens when something navigates
7584
+ * concurrently with a login still in progress — a window that stretches to
7585
+ * HYDRATION_TIMEOUT_MS on a slow connection.
7586
+ */
7587
+ async awaitHydration() {
7588
+ try {
7589
+ await this._hydrationPromise;
7590
+ } catch {
7591
+ }
7592
+ return this.hydrationStatus();
7593
+ }
7594
+ /**
7595
+ * Whether a missing document may be treated as genuinely absent, allowing
7596
+ * callers to create it with defaults.
7597
+ *
7598
+ * False only when hydration failed or is still running: a 404 then may just
7599
+ * mean "not pulled down yet", and materializing a default would both lie to
7600
+ * the user and, once live sync starts, push a rev-1 document that loses to
7601
+ * the real one — silently discarding it.
7602
+ */
7603
+ canMaterializeDefaults() {
7604
+ return this._hydration.state !== "failed" && this._hydration.state !== "hydrating";
7605
+ }
7539
7606
  async createAccount(username, password) {
7540
7607
  if (!this.syncStrategy.canCreateAccount()) {
7541
7608
  throw new Error("Account creation not supported by current sync strategy");
@@ -7653,6 +7720,11 @@ Currently logged-in as ${this._username}.`
7653
7720
  } catch (e) {
7654
7721
  const err = e;
7655
7722
  if (err.status === 404) {
7723
+ if (!this.canMaterializeDefaults()) {
7724
+ throw new Error(
7725
+ `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.`
7726
+ );
7727
+ }
7656
7728
  await this.localDB.put({
7657
7729
  _id: _BaseUser.DOC_IDS.COURSE_REGISTRATIONS,
7658
7730
  courses: [],
@@ -7895,6 +7967,11 @@ Currently logged-in as ${this._username}.`
7895
7967
  } catch (e) {
7896
7968
  const err = e;
7897
7969
  if (err.name && err.name === "not_found") {
7970
+ if (!this.canMaterializeDefaults()) {
7971
+ throw new Error(
7972
+ `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.`
7973
+ );
7974
+ }
7898
7975
  await this.localDB.put(defaultConfig);
7899
7976
  return this.getConfig();
7900
7977
  } else {
@@ -7968,7 +8045,10 @@ Currently logged-in as ${this._username}.`
7968
8045
  _BaseUser._initialized = true;
7969
8046
  return;
7970
8047
  }
8048
+ this.syncStrategy.stopSync?.();
7971
8049
  this.setDBandQ();
8050
+ this._hydrationPromise = this.hydrateLocalMirror();
8051
+ await this._hydrationPromise;
7972
8052
  this.syncStrategy.startSync(this.localDB, this.remoteDB);
7973
8053
  this.applyDesignDocs().catch((error) => {
7974
8054
  log3(`Error in applyDesignDocs background task: ${error}`);
@@ -7984,6 +8064,85 @@ Currently logged-in as ${this._username}.`
7984
8064
  });
7985
8065
  _BaseUser._initialized = true;
7986
8066
  }
8067
+ /**
8068
+ * Pull this account's documents into the local mirror, if that hasn't
8069
+ * happened on this device before.
8070
+ *
8071
+ * Runs between setDBandQ() and startSync() — after the handles exist, before
8072
+ * anything can observe an empty local DB or replicate one upward.
8073
+ *
8074
+ * Never throws: a failure is recorded as `failed` state and reported through
8075
+ * hydrationStatus(), because throwing here would abort init() and leave
8076
+ * BaseUser._initialized false forever (BaseUser.instance() polls it with no
8077
+ * ceiling). Callers decide what a failure means for them.
8078
+ */
8079
+ async hydrateLocalMirror() {
8080
+ if (this.localDB.name === this.remoteDB.name || !this.syncStrategy.hydrate) {
8081
+ this._hydration = { state: "not-required" };
8082
+ return;
8083
+ }
8084
+ if (await this.hasHydrationMarker()) {
8085
+ this._hydration = { state: "stale" };
8086
+ return;
8087
+ }
8088
+ this._hydration = { state: "hydrating" };
8089
+ const start = Date.now();
8090
+ try {
8091
+ const { docsWritten } = await withTimeout(
8092
+ this.syncStrategy.hydrate(this.localDB, this.remoteDB),
8093
+ HYDRATION_TIMEOUT_MS,
8094
+ `Hydration of local mirror for ${this._username}`
8095
+ );
8096
+ await this.writeHydrationMarker();
8097
+ this._hydration = {
8098
+ state: "hydrated",
8099
+ docsWritten,
8100
+ durationMs: Date.now() - start
8101
+ };
8102
+ log3(
8103
+ `Hydrated local mirror for ${this._username}: ${docsWritten} docs in ${this._hydration.durationMs}ms`
8104
+ );
8105
+ } catch (e) {
8106
+ this.syncStrategy.stopSync?.();
8107
+ this._hydration = {
8108
+ state: "failed",
8109
+ durationMs: Date.now() - start,
8110
+ error: e instanceof Error ? e.message : String(e)
8111
+ };
8112
+ logger.error(`Failed to hydrate local mirror for ${this._username}:`, e);
8113
+ }
8114
+ }
8115
+ /**
8116
+ * Has a full pull completed on this device for the CURRENT account?
8117
+ *
8118
+ * The marker is a `_local/` document, which never replicates — so its
8119
+ * presence describes this device's mirror and cannot arrive from elsewhere.
8120
+ */
8121
+ async hasHydrationMarker() {
8122
+ try {
8123
+ const marker = await this.localDB.get(HYDRATION_MARKER_ID);
8124
+ return marker.username === this._username;
8125
+ } catch {
8126
+ return false;
8127
+ }
8128
+ }
8129
+ async writeHydrationMarker() {
8130
+ try {
8131
+ let existingRev;
8132
+ try {
8133
+ existingRev = (await this.localDB.get(HYDRATION_MARKER_ID))._rev;
8134
+ } catch {
8135
+ }
8136
+ await this.localDB.put({
8137
+ _id: HYDRATION_MARKER_ID,
8138
+ _rev: existingRev,
8139
+ username: this._username,
8140
+ hydratedAt: (/* @__PURE__ */ new Date()).toISOString()
8141
+ });
8142
+ } catch (e) {
8143
+ logger.warn(`Could not write hydration marker for ${this._username}: ${e}`);
8144
+ }
8145
+ }
7987
8146
  static designDocs = [
7988
8147
  {
7989
8148
  _id: "_design/reviewCards",
@@ -8320,6 +8479,11 @@ Currently logged-in as ${this._username}.`
8320
8479
  } catch (e) {
8321
8480
  const err = e;
8322
8481
  if (err.status === 404) {
8482
+ if (!this.canMaterializeDefaults()) {
8483
+ throw new Error(
8484
+ `Cannot read strategy state '${strategyKey}' for ${this._username}: the local mirror is unavailable (hydration state '${this._hydration.state}').`
8485
+ );
8486
+ }
8323
8487
  return null;
8324
8488
  }
8325
8489
  throw e;
@@ -8844,6 +9008,8 @@ var init_CouchDBSyncStrategy = __esm({
8844
9008
  CouchDBSyncStrategy = class {
8845
9009
  syncHandle;
8846
9010
  // Handle to cancel sync if needed
9011
+ /** In-flight one-shot hydration pull, so a timed-out pull can be abandoned. */
9012
+ hydrationHandle;
8847
9013
  setupRemoteDB(username) {
8848
9014
  if (username === GuestUsername || username.startsWith(GuestUsername)) {
8849
9015
  return getLocalUserDB(username);
@@ -8858,8 +9024,26 @@ var init_CouchDBSyncStrategy = __esm({
8858
9024
  return this.getUserDB(username);
8859
9025
  }
8860
9026
  }
9027
+ /**
9028
+ * One-shot remote → local pull. Resolves once the local mirror is caught up.
9029
+ *
9030
+ * Pull only: pushing here would upload whatever the local DB happens to hold
9031
+ * before we know what the remote already has, which is the conflict-leaf
9032
+ * problem hydration exists to avoid. Local changes go up when startSync()
9033
+ * takes over.
9034
+ */
9035
+ async hydrate(localDB, remoteDB) {
9036
+ const replication = pouchdb_setup_default.replicate(remoteDB, localDB, {});
9037
+ this.hydrationHandle = replication;
9038
+ try {
9039
+ const info = await replication;
9040
+ return { docsWritten: info.docs_written };
9041
+ } finally {
9042
+ this.hydrationHandle = void 0;
9043
+ }
9044
+ }
8861
9045
  startSync(localDB, remoteDB) {
8862
- if (localDB !== remoteDB) {
9046
+ if (localDB.name !== remoteDB.name) {
8863
9047
  this.syncHandle = pouchdb_setup_default.sync(localDB, remoteDB, {
8864
9048
  live: true,
8865
9049
  retry: true
@@ -8867,8 +9051,20 @@ var init_CouchDBSyncStrategy = __esm({
8867
9051
  }
8868
9052
  }
8869
9053
  stopSync() {
9054
+ if (this.hydrationHandle) {
9055
+ try {
9056
+ this.hydrationHandle.cancel();
9057
+ } catch (e) {
9058
+ logger.warn(`Failed to cancel hydration pull (continuing anyway): ${e}`);
9059
+ }
9060
+ this.hydrationHandle = void 0;
9061
+ }
8870
9062
  if (this.syncHandle) {
8871
- this.syncHandle.cancel();
9063
+ try {
9064
+ this.syncHandle.cancel();
9065
+ } catch (e) {
9066
+ logger.warn(`Failed to cancel user sync (continuing anyway): ${e}`);
9067
+ }
8872
9068
  this.syncHandle = void 0;
8873
9069
  }
8874
9070
  }