@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.
- 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-B1p-vdz7.d.ts → contentSource-BDRX_YYJ.d.ts} +97 -1
- package/dist/{contentSource-Brz42x7n.d.cts → contentSource-D-LQYFyx.d.cts} +97 -1
- package/dist/core/index.d.cts +3 -3
- package/dist/core/index.d.ts +3 -3
- package/dist/core/index.js +167 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +166 -1
- package/dist/core/index.mjs.map +1 -1
- package/dist/{dataLayerProvider-CpwpT1rM.d.cts → dataLayerProvider-Dd2UcCif.d.cts} +1 -1
- package/dist/{dataLayerProvider-BWayUIoK.d.ts → dataLayerProvider-Dpshuql4.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 +199 -3
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +199 -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 +165 -1
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +165 -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 +201 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +200 -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 +24 -0
- package/src/core/types/hydration.ts +78 -0
- package/src/impl/common/BaseUserDB.ts +233 -2
- package/src/impl/common/SyncStrategy.ts +19 -0
- package/src/impl/couch/CouchDBSyncStrategy.ts +54 -4
- 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-
|
|
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-
|
|
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-
|
|
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-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-
|
|
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-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-
|
|
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,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
|
-
|
|
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
|
}
|