@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.
- package/dist/{SyncStrategy-CyATpyLQ.d.cts → SyncStrategy-BJMZq3WO.d.cts} +17 -0
- package/dist/{SyncStrategy-CyATpyLQ.d.ts → SyncStrategy-BJMZq3WO.d.ts} +17 -0
- package/dist/{contentSource-Brz42x7n.d.cts → contentSource-CBqZCoGU.d.cts} +85 -1
- package/dist/{contentSource-B1p-vdz7.d.ts → contentSource-CudEz5Tm.d.ts} +85 -1
- package/dist/core/index.d.cts +3 -3
- package/dist/core/index.d.ts +3 -3
- package/dist/core/index.js +146 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +145 -1
- package/dist/core/index.mjs.map +1 -1
- package/dist/{dataLayerProvider-CpwpT1rM.d.cts → dataLayerProvider-BBA8tJNx.d.cts} +1 -1
- package/dist/{dataLayerProvider-BWayUIoK.d.ts → dataLayerProvider-C9WgkBzR.d.ts} +1 -1
- package/dist/impl/couch/index.d.cts +16 -3
- package/dist/impl/couch/index.d.ts +16 -3
- package/dist/impl/couch/index.js +178 -3
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +178 -3
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.d.cts +3 -3
- package/dist/impl/static/index.d.ts +3 -3
- package/dist/impl/static/index.js +144 -1
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +144 -1
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +180 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +179 -3
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -3
- package/src/core/index.ts +1 -0
- package/src/core/interfaces/userDB.ts +11 -0
- package/src/core/types/hydration.ts +78 -0
- package/src/impl/common/BaseUserDB.ts +202 -2
- package/src/impl/common/SyncStrategy.ts +19 -0
- package/src/impl/couch/CouchDBSyncStrategy.ts +54 -4
- package/tests/impl/hydration.test.ts +281 -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-
|
|
1
|
+
import { U as UserDBInterface, a as UserDBReader, C as CourseDBInterface, b as CoursesDBInterface, c as ClassroomDBInterface, A as AdminDBInterface } from './contentSource-CBqZCoGU.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-
|
|
1
|
+
import { U as UserDBInterface, a as UserDBReader, C as CourseDBInterface, b as CoursesDBInterface, c as ClassroomDBInterface, A as AdminDBInterface } from './contentSource-CudEz5Tm.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-
|
|
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-
|
|
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';
|
|
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-
|
|
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-
|
|
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-
|
|
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';
|
|
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-
|
|
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;
|
package/dist/impl/couch/index.js
CHANGED
|
@@ -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,29 @@ 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
|
+
/**
|
|
7565
|
+
* How far the local mirror can be trusted. See {@link UserHydrationStatus}.
|
|
7566
|
+
*
|
|
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.
|
|
7570
|
+
*/
|
|
7571
|
+
hydrationStatus() {
|
|
7572
|
+
return { ...this._hydration };
|
|
7573
|
+
}
|
|
7574
|
+
/**
|
|
7575
|
+
* Whether a missing document may be treated as genuinely absent, allowing
|
|
7576
|
+
* callers to create it with defaults.
|
|
7577
|
+
*
|
|
7578
|
+
* False only when hydration failed or is still running: a 404 then may just
|
|
7579
|
+
* mean "not pulled down yet", and materializing a default would both lie to
|
|
7580
|
+
* the user and, once live sync starts, push a rev-1 document that loses to
|
|
7581
|
+
* the real one — silently discarding it.
|
|
7582
|
+
*/
|
|
7583
|
+
canMaterializeDefaults() {
|
|
7584
|
+
return this._hydration.state !== "failed" && this._hydration.state !== "hydrating";
|
|
7585
|
+
}
|
|
7539
7586
|
async createAccount(username, password) {
|
|
7540
7587
|
if (!this.syncStrategy.canCreateAccount()) {
|
|
7541
7588
|
throw new Error("Account creation not supported by current sync strategy");
|
|
@@ -7653,6 +7700,11 @@ Currently logged-in as ${this._username}.`
|
|
|
7653
7700
|
} catch (e) {
|
|
7654
7701
|
const err = e;
|
|
7655
7702
|
if (err.status === 404) {
|
|
7703
|
+
if (!this.canMaterializeDefaults()) {
|
|
7704
|
+
throw new Error(
|
|
7705
|
+
`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.`
|
|
7706
|
+
);
|
|
7707
|
+
}
|
|
7656
7708
|
await this.localDB.put({
|
|
7657
7709
|
_id: _BaseUser.DOC_IDS.COURSE_REGISTRATIONS,
|
|
7658
7710
|
courses: [],
|
|
@@ -7895,6 +7947,11 @@ Currently logged-in as ${this._username}.`
|
|
|
7895
7947
|
} catch (e) {
|
|
7896
7948
|
const err = e;
|
|
7897
7949
|
if (err.name && err.name === "not_found") {
|
|
7950
|
+
if (!this.canMaterializeDefaults()) {
|
|
7951
|
+
throw new Error(
|
|
7952
|
+
`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.`
|
|
7953
|
+
);
|
|
7954
|
+
}
|
|
7898
7955
|
await this.localDB.put(defaultConfig);
|
|
7899
7956
|
return this.getConfig();
|
|
7900
7957
|
} else {
|
|
@@ -7968,7 +8025,9 @@ Currently logged-in as ${this._username}.`
|
|
|
7968
8025
|
_BaseUser._initialized = true;
|
|
7969
8026
|
return;
|
|
7970
8027
|
}
|
|
8028
|
+
this.syncStrategy.stopSync?.();
|
|
7971
8029
|
this.setDBandQ();
|
|
8030
|
+
await this.hydrateLocalMirror();
|
|
7972
8031
|
this.syncStrategy.startSync(this.localDB, this.remoteDB);
|
|
7973
8032
|
this.applyDesignDocs().catch((error) => {
|
|
7974
8033
|
log3(`Error in applyDesignDocs background task: ${error}`);
|
|
@@ -7984,6 +8043,85 @@ Currently logged-in as ${this._username}.`
|
|
|
7984
8043
|
});
|
|
7985
8044
|
_BaseUser._initialized = true;
|
|
7986
8045
|
}
|
|
8046
|
+
/**
|
|
8047
|
+
* Pull this account's documents into the local mirror, if that hasn't
|
|
8048
|
+
* happened on this device before.
|
|
8049
|
+
*
|
|
8050
|
+
* Runs between setDBandQ() and startSync() — after the handles exist, before
|
|
8051
|
+
* anything can observe an empty local DB or replicate one upward.
|
|
8052
|
+
*
|
|
8053
|
+
* Never throws: a failure is recorded as `failed` state and reported through
|
|
8054
|
+
* hydrationStatus(), because throwing here would abort init() and leave
|
|
8055
|
+
* BaseUser._initialized false forever (BaseUser.instance() polls it with no
|
|
8056
|
+
* ceiling). Callers decide what a failure means for them.
|
|
8057
|
+
*/
|
|
8058
|
+
async hydrateLocalMirror() {
|
|
8059
|
+
if (this.localDB.name === this.remoteDB.name || !this.syncStrategy.hydrate) {
|
|
8060
|
+
this._hydration = { state: "not-required" };
|
|
8061
|
+
return;
|
|
8062
|
+
}
|
|
8063
|
+
if (await this.hasHydrationMarker()) {
|
|
8064
|
+
this._hydration = { state: "stale" };
|
|
8065
|
+
return;
|
|
8066
|
+
}
|
|
8067
|
+
this._hydration = { state: "hydrating" };
|
|
8068
|
+
const start = Date.now();
|
|
8069
|
+
try {
|
|
8070
|
+
const { docsWritten } = await withTimeout(
|
|
8071
|
+
this.syncStrategy.hydrate(this.localDB, this.remoteDB),
|
|
8072
|
+
HYDRATION_TIMEOUT_MS,
|
|
8073
|
+
`Hydration of local mirror for ${this._username}`
|
|
8074
|
+
);
|
|
8075
|
+
await this.writeHydrationMarker();
|
|
8076
|
+
this._hydration = {
|
|
8077
|
+
state: "hydrated",
|
|
8078
|
+
docsWritten,
|
|
8079
|
+
durationMs: Date.now() - start
|
|
8080
|
+
};
|
|
8081
|
+
log3(
|
|
8082
|
+
`Hydrated local mirror for ${this._username}: ${docsWritten} docs in ${this._hydration.durationMs}ms`
|
|
8083
|
+
);
|
|
8084
|
+
} catch (e) {
|
|
8085
|
+
this.syncStrategy.stopSync?.();
|
|
8086
|
+
this._hydration = {
|
|
8087
|
+
state: "failed",
|
|
8088
|
+
durationMs: Date.now() - start,
|
|
8089
|
+
error: e instanceof Error ? e.message : String(e)
|
|
8090
|
+
};
|
|
8091
|
+
logger.error(`Failed to hydrate local mirror for ${this._username}:`, e);
|
|
8092
|
+
}
|
|
8093
|
+
}
|
|
8094
|
+
/**
|
|
8095
|
+
* Has a full pull completed on this device for the CURRENT account?
|
|
8096
|
+
*
|
|
8097
|
+
* The marker is a `_local/` document, which never replicates — so its
|
|
8098
|
+
* presence describes this device's mirror and cannot arrive from elsewhere.
|
|
8099
|
+
*/
|
|
8100
|
+
async hasHydrationMarker() {
|
|
8101
|
+
try {
|
|
8102
|
+
const marker = await this.localDB.get(HYDRATION_MARKER_ID);
|
|
8103
|
+
return marker.username === this._username;
|
|
8104
|
+
} catch {
|
|
8105
|
+
return false;
|
|
8106
|
+
}
|
|
8107
|
+
}
|
|
8108
|
+
async writeHydrationMarker() {
|
|
8109
|
+
try {
|
|
8110
|
+
let existingRev;
|
|
8111
|
+
try {
|
|
8112
|
+
existingRev = (await this.localDB.get(HYDRATION_MARKER_ID))._rev;
|
|
8113
|
+
} catch {
|
|
8114
|
+
}
|
|
8115
|
+
await this.localDB.put({
|
|
8116
|
+
_id: HYDRATION_MARKER_ID,
|
|
8117
|
+
_rev: existingRev,
|
|
8118
|
+
username: this._username,
|
|
8119
|
+
hydratedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
8120
|
+
});
|
|
8121
|
+
} catch (e) {
|
|
8122
|
+
logger.warn(`Could not write hydration marker for ${this._username}: ${e}`);
|
|
8123
|
+
}
|
|
8124
|
+
}
|
|
7987
8125
|
static designDocs = [
|
|
7988
8126
|
{
|
|
7989
8127
|
_id: "_design/reviewCards",
|
|
@@ -8320,6 +8458,11 @@ Currently logged-in as ${this._username}.`
|
|
|
8320
8458
|
} catch (e) {
|
|
8321
8459
|
const err = e;
|
|
8322
8460
|
if (err.status === 404) {
|
|
8461
|
+
if (!this.canMaterializeDefaults()) {
|
|
8462
|
+
throw new Error(
|
|
8463
|
+
`Cannot read strategy state '${strategyKey}' for ${this._username}: the local mirror is unavailable (hydration state '${this._hydration.state}').`
|
|
8464
|
+
);
|
|
8465
|
+
}
|
|
8323
8466
|
return null;
|
|
8324
8467
|
}
|
|
8325
8468
|
throw e;
|
|
@@ -8844,6 +8987,8 @@ var init_CouchDBSyncStrategy = __esm({
|
|
|
8844
8987
|
CouchDBSyncStrategy = class {
|
|
8845
8988
|
syncHandle;
|
|
8846
8989
|
// Handle to cancel sync if needed
|
|
8990
|
+
/** In-flight one-shot hydration pull, so a timed-out pull can be abandoned. */
|
|
8991
|
+
hydrationHandle;
|
|
8847
8992
|
setupRemoteDB(username) {
|
|
8848
8993
|
if (username === GuestUsername || username.startsWith(GuestUsername)) {
|
|
8849
8994
|
return getLocalUserDB(username);
|
|
@@ -8858,8 +9003,26 @@ var init_CouchDBSyncStrategy = __esm({
|
|
|
8858
9003
|
return this.getUserDB(username);
|
|
8859
9004
|
}
|
|
8860
9005
|
}
|
|
9006
|
+
/**
|
|
9007
|
+
* One-shot remote → local pull. Resolves once the local mirror is caught up.
|
|
9008
|
+
*
|
|
9009
|
+
* Pull only: pushing here would upload whatever the local DB happens to hold
|
|
9010
|
+
* before we know what the remote already has, which is the conflict-leaf
|
|
9011
|
+
* problem hydration exists to avoid. Local changes go up when startSync()
|
|
9012
|
+
* takes over.
|
|
9013
|
+
*/
|
|
9014
|
+
async hydrate(localDB, remoteDB) {
|
|
9015
|
+
const replication = pouchdb_setup_default.replicate(remoteDB, localDB, {});
|
|
9016
|
+
this.hydrationHandle = replication;
|
|
9017
|
+
try {
|
|
9018
|
+
const info = await replication;
|
|
9019
|
+
return { docsWritten: info.docs_written };
|
|
9020
|
+
} finally {
|
|
9021
|
+
this.hydrationHandle = void 0;
|
|
9022
|
+
}
|
|
9023
|
+
}
|
|
8861
9024
|
startSync(localDB, remoteDB) {
|
|
8862
|
-
if (localDB !== remoteDB) {
|
|
9025
|
+
if (localDB.name !== remoteDB.name) {
|
|
8863
9026
|
this.syncHandle = pouchdb_setup_default.sync(localDB, remoteDB, {
|
|
8864
9027
|
live: true,
|
|
8865
9028
|
retry: true
|
|
@@ -8867,8 +9030,20 @@ var init_CouchDBSyncStrategy = __esm({
|
|
|
8867
9030
|
}
|
|
8868
9031
|
}
|
|
8869
9032
|
stopSync() {
|
|
9033
|
+
if (this.hydrationHandle) {
|
|
9034
|
+
try {
|
|
9035
|
+
this.hydrationHandle.cancel();
|
|
9036
|
+
} catch (e) {
|
|
9037
|
+
logger.warn(`Failed to cancel hydration pull (continuing anyway): ${e}`);
|
|
9038
|
+
}
|
|
9039
|
+
this.hydrationHandle = void 0;
|
|
9040
|
+
}
|
|
8870
9041
|
if (this.syncHandle) {
|
|
8871
|
-
|
|
9042
|
+
try {
|
|
9043
|
+
this.syncHandle.cancel();
|
|
9044
|
+
} catch (e) {
|
|
9045
|
+
logger.warn(`Failed to cancel user sync (continuing anyway): ${e}`);
|
|
9046
|
+
}
|
|
8872
9047
|
this.syncHandle = void 0;
|
|
8873
9048
|
}
|
|
8874
9049
|
}
|