@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
package/dist/core/index.mjs
CHANGED
|
@@ -6749,6 +6749,19 @@ var init_couch = __esm({
|
|
|
6749
6749
|
// src/impl/common/BaseUserDB.ts
|
|
6750
6750
|
import { Status as Status3 } from "@vue-skuilder/common";
|
|
6751
6751
|
import moment6 from "moment";
|
|
6752
|
+
function withTimeout(p, ms, label) {
|
|
6753
|
+
let timer;
|
|
6754
|
+
return Promise.race([
|
|
6755
|
+
p,
|
|
6756
|
+
new Promise((_resolve, reject) => {
|
|
6757
|
+
timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
|
|
6758
|
+
})
|
|
6759
|
+
]).finally(() => {
|
|
6760
|
+
if (timer !== void 0) {
|
|
6761
|
+
clearTimeout(timer);
|
|
6762
|
+
}
|
|
6763
|
+
});
|
|
6764
|
+
}
|
|
6752
6765
|
async function getOrCreateClassroomRegistrationsDoc(user) {
|
|
6753
6766
|
let ret;
|
|
6754
6767
|
try {
|
|
@@ -6841,7 +6854,7 @@ async function dropUserFromClassroom(user, classID) {
|
|
|
6841
6854
|
async function getUserClassrooms(user) {
|
|
6842
6855
|
return getOrCreateClassroomRegistrationsDoc(user);
|
|
6843
6856
|
}
|
|
6844
|
-
var log3, BaseUser, userCoursesDoc, userClassroomsDoc;
|
|
6857
|
+
var log3, HYDRATION_TIMEOUT_MS, BaseUser, userCoursesDoc, userClassroomsDoc;
|
|
6845
6858
|
var init_BaseUserDB = __esm({
|
|
6846
6859
|
"src/impl/common/BaseUserDB.ts"() {
|
|
6847
6860
|
"use strict";
|
|
@@ -6856,6 +6869,7 @@ var init_BaseUserDB = __esm({
|
|
|
6856
6869
|
log3 = (s) => {
|
|
6857
6870
|
logger.info(s);
|
|
6858
6871
|
};
|
|
6872
|
+
HYDRATION_TIMEOUT_MS = 15e3;
|
|
6859
6873
|
BaseUser = class _BaseUser {
|
|
6860
6874
|
static _instance;
|
|
6861
6875
|
static _initialized = false;
|
|
@@ -6884,6 +6898,49 @@ var init_BaseUserDB = __esm({
|
|
|
6884
6898
|
writeDB;
|
|
6885
6899
|
// Database to use for write operations (local-first approach)
|
|
6886
6900
|
updateQueue;
|
|
6901
|
+
_hydration = { state: "not-required" };
|
|
6902
|
+
/** In-flight hydration, so concurrent callers can wait for a real answer. */
|
|
6903
|
+
_hydrationPromise = null;
|
|
6904
|
+
/**
|
|
6905
|
+
* How far the local mirror can be trusted, RIGHT NOW. See
|
|
6906
|
+
* {@link UserHydrationStatus}.
|
|
6907
|
+
*
|
|
6908
|
+
* This is a snapshot, and during login it can legitimately read
|
|
6909
|
+
* `hydrating` — prefer awaitHydration() anywhere a decision hangs on the
|
|
6910
|
+
* answer.
|
|
6911
|
+
*/
|
|
6912
|
+
hydrationStatus() {
|
|
6913
|
+
return { ...this._hydration };
|
|
6914
|
+
}
|
|
6915
|
+
/**
|
|
6916
|
+
* The hydration status once it has settled — never `hydrating`.
|
|
6917
|
+
*
|
|
6918
|
+
* Resolves immediately unless a pull is in flight. Exists for callers that
|
|
6919
|
+
* must decide something (a route guard, a first-run branch) and would
|
|
6920
|
+
* otherwise sample `hydrating` and guess. Since init() is awaited by both
|
|
6921
|
+
* app startup and login(), that only happens when something navigates
|
|
6922
|
+
* concurrently with a login still in progress — a window that stretches to
|
|
6923
|
+
* HYDRATION_TIMEOUT_MS on a slow connection.
|
|
6924
|
+
*/
|
|
6925
|
+
async awaitHydration() {
|
|
6926
|
+
try {
|
|
6927
|
+
await this._hydrationPromise;
|
|
6928
|
+
} catch {
|
|
6929
|
+
}
|
|
6930
|
+
return this.hydrationStatus();
|
|
6931
|
+
}
|
|
6932
|
+
/**
|
|
6933
|
+
* Whether a missing document may be treated as genuinely absent, allowing
|
|
6934
|
+
* callers to create it with defaults.
|
|
6935
|
+
*
|
|
6936
|
+
* False only when hydration failed or is still running: a 404 then may just
|
|
6937
|
+
* mean "not pulled down yet", and materializing a default would both lie to
|
|
6938
|
+
* the user and, once live sync starts, push a rev-1 document that loses to
|
|
6939
|
+
* the real one — silently discarding it.
|
|
6940
|
+
*/
|
|
6941
|
+
canMaterializeDefaults() {
|
|
6942
|
+
return this._hydration.state !== "failed" && this._hydration.state !== "hydrating";
|
|
6943
|
+
}
|
|
6887
6944
|
async createAccount(username, password) {
|
|
6888
6945
|
if (!this.syncStrategy.canCreateAccount()) {
|
|
6889
6946
|
throw new Error("Account creation not supported by current sync strategy");
|
|
@@ -7001,6 +7058,11 @@ Currently logged-in as ${this._username}.`
|
|
|
7001
7058
|
} catch (e) {
|
|
7002
7059
|
const err = e;
|
|
7003
7060
|
if (err.status === 404) {
|
|
7061
|
+
if (!this.canMaterializeDefaults()) {
|
|
7062
|
+
throw new Error(
|
|
7063
|
+
`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.`
|
|
7064
|
+
);
|
|
7065
|
+
}
|
|
7004
7066
|
await this.localDB.put({
|
|
7005
7067
|
_id: _BaseUser.DOC_IDS.COURSE_REGISTRATIONS,
|
|
7006
7068
|
courses: [],
|
|
@@ -7243,6 +7305,11 @@ Currently logged-in as ${this._username}.`
|
|
|
7243
7305
|
} catch (e) {
|
|
7244
7306
|
const err = e;
|
|
7245
7307
|
if (err.name && err.name === "not_found") {
|
|
7308
|
+
if (!this.canMaterializeDefaults()) {
|
|
7309
|
+
throw new Error(
|
|
7310
|
+
`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.`
|
|
7311
|
+
);
|
|
7312
|
+
}
|
|
7246
7313
|
await this.localDB.put(defaultConfig);
|
|
7247
7314
|
return this.getConfig();
|
|
7248
7315
|
} else {
|
|
@@ -7316,7 +7383,10 @@ Currently logged-in as ${this._username}.`
|
|
|
7316
7383
|
_BaseUser._initialized = true;
|
|
7317
7384
|
return;
|
|
7318
7385
|
}
|
|
7386
|
+
this.syncStrategy.stopSync?.();
|
|
7319
7387
|
this.setDBandQ();
|
|
7388
|
+
this._hydrationPromise = this.hydrateLocalMirror();
|
|
7389
|
+
await this._hydrationPromise;
|
|
7320
7390
|
this.syncStrategy.startSync(this.localDB, this.remoteDB);
|
|
7321
7391
|
this.applyDesignDocs().catch((error) => {
|
|
7322
7392
|
log3(`Error in applyDesignDocs background task: ${error}`);
|
|
@@ -7332,6 +7402,85 @@ Currently logged-in as ${this._username}.`
|
|
|
7332
7402
|
});
|
|
7333
7403
|
_BaseUser._initialized = true;
|
|
7334
7404
|
}
|
|
7405
|
+
/**
|
|
7406
|
+
* Pull this account's documents into the local mirror, if that hasn't
|
|
7407
|
+
* happened on this device before.
|
|
7408
|
+
*
|
|
7409
|
+
* Runs between setDBandQ() and startSync() — after the handles exist, before
|
|
7410
|
+
* anything can observe an empty local DB or replicate one upward.
|
|
7411
|
+
*
|
|
7412
|
+
* Never throws: a failure is recorded as `failed` state and reported through
|
|
7413
|
+
* hydrationStatus(), because throwing here would abort init() and leave
|
|
7414
|
+
* BaseUser._initialized false forever (BaseUser.instance() polls it with no
|
|
7415
|
+
* ceiling). Callers decide what a failure means for them.
|
|
7416
|
+
*/
|
|
7417
|
+
async hydrateLocalMirror() {
|
|
7418
|
+
if (this.localDB.name === this.remoteDB.name || !this.syncStrategy.hydrate) {
|
|
7419
|
+
this._hydration = { state: "not-required" };
|
|
7420
|
+
return;
|
|
7421
|
+
}
|
|
7422
|
+
if (await this.hasHydrationMarker()) {
|
|
7423
|
+
this._hydration = { state: "stale" };
|
|
7424
|
+
return;
|
|
7425
|
+
}
|
|
7426
|
+
this._hydration = { state: "hydrating" };
|
|
7427
|
+
const start = Date.now();
|
|
7428
|
+
try {
|
|
7429
|
+
const { docsWritten } = await withTimeout(
|
|
7430
|
+
this.syncStrategy.hydrate(this.localDB, this.remoteDB),
|
|
7431
|
+
HYDRATION_TIMEOUT_MS,
|
|
7432
|
+
`Hydration of local mirror for ${this._username}`
|
|
7433
|
+
);
|
|
7434
|
+
await this.writeHydrationMarker();
|
|
7435
|
+
this._hydration = {
|
|
7436
|
+
state: "hydrated",
|
|
7437
|
+
docsWritten,
|
|
7438
|
+
durationMs: Date.now() - start
|
|
7439
|
+
};
|
|
7440
|
+
log3(
|
|
7441
|
+
`Hydrated local mirror for ${this._username}: ${docsWritten} docs in ${this._hydration.durationMs}ms`
|
|
7442
|
+
);
|
|
7443
|
+
} catch (e) {
|
|
7444
|
+
this.syncStrategy.stopSync?.();
|
|
7445
|
+
this._hydration = {
|
|
7446
|
+
state: "failed",
|
|
7447
|
+
durationMs: Date.now() - start,
|
|
7448
|
+
error: e instanceof Error ? e.message : String(e)
|
|
7449
|
+
};
|
|
7450
|
+
logger.error(`Failed to hydrate local mirror for ${this._username}:`, e);
|
|
7451
|
+
}
|
|
7452
|
+
}
|
|
7453
|
+
/**
|
|
7454
|
+
* Has a full pull completed on this device for the CURRENT account?
|
|
7455
|
+
*
|
|
7456
|
+
* The marker is a `_local/` document, which never replicates — so its
|
|
7457
|
+
* presence describes this device's mirror and cannot arrive from elsewhere.
|
|
7458
|
+
*/
|
|
7459
|
+
async hasHydrationMarker() {
|
|
7460
|
+
try {
|
|
7461
|
+
const marker = await this.localDB.get(HYDRATION_MARKER_ID);
|
|
7462
|
+
return marker.username === this._username;
|
|
7463
|
+
} catch {
|
|
7464
|
+
return false;
|
|
7465
|
+
}
|
|
7466
|
+
}
|
|
7467
|
+
async writeHydrationMarker() {
|
|
7468
|
+
try {
|
|
7469
|
+
let existingRev;
|
|
7470
|
+
try {
|
|
7471
|
+
existingRev = (await this.localDB.get(HYDRATION_MARKER_ID))._rev;
|
|
7472
|
+
} catch {
|
|
7473
|
+
}
|
|
7474
|
+
await this.localDB.put({
|
|
7475
|
+
_id: HYDRATION_MARKER_ID,
|
|
7476
|
+
_rev: existingRev,
|
|
7477
|
+
username: this._username,
|
|
7478
|
+
hydratedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
7479
|
+
});
|
|
7480
|
+
} catch (e) {
|
|
7481
|
+
logger.warn(`Could not write hydration marker for ${this._username}: ${e}`);
|
|
7482
|
+
}
|
|
7483
|
+
}
|
|
7335
7484
|
static designDocs = [
|
|
7336
7485
|
{
|
|
7337
7486
|
_id: "_design/reviewCards",
|
|
@@ -7668,6 +7817,11 @@ Currently logged-in as ${this._username}.`
|
|
|
7668
7817
|
} catch (e) {
|
|
7669
7818
|
const err = e;
|
|
7670
7819
|
if (err.status === 404) {
|
|
7820
|
+
if (!this.canMaterializeDefaults()) {
|
|
7821
|
+
throw new Error(
|
|
7822
|
+
`Cannot read strategy state '${strategyKey}' for ${this._username}: the local mirror is unavailable (hydration state '${this._hydration.state}').`
|
|
7823
|
+
);
|
|
7824
|
+
}
|
|
7671
7825
|
return null;
|
|
7672
7826
|
}
|
|
7673
7827
|
throw e;
|
|
@@ -8015,6 +8169,15 @@ var init_userOutcome = __esm({
|
|
|
8015
8169
|
}
|
|
8016
8170
|
});
|
|
8017
8171
|
|
|
8172
|
+
// src/core/types/hydration.ts
|
|
8173
|
+
var HYDRATION_MARKER_ID;
|
|
8174
|
+
var init_hydration = __esm({
|
|
8175
|
+
"src/core/types/hydration.ts"() {
|
|
8176
|
+
"use strict";
|
|
8177
|
+
HYDRATION_MARKER_ID = "_local/hydration";
|
|
8178
|
+
}
|
|
8179
|
+
});
|
|
8180
|
+
|
|
8018
8181
|
// src/core/bulkImport/cardProcessor.ts
|
|
8019
8182
|
import { Status as Status4 } from "@vue-skuilder/common";
|
|
8020
8183
|
async function importParsedCards(parsedCards, courseDB, config) {
|
|
@@ -8493,6 +8656,7 @@ var init_core = __esm({
|
|
|
8493
8656
|
init_user();
|
|
8494
8657
|
init_strategyState();
|
|
8495
8658
|
init_userOutcome();
|
|
8659
|
+
init_hydration();
|
|
8496
8660
|
init_Loggable();
|
|
8497
8661
|
init_util();
|
|
8498
8662
|
init_navigators();
|
|
@@ -8509,6 +8673,7 @@ export {
|
|
|
8509
8673
|
DocType,
|
|
8510
8674
|
DocTypePrefixes,
|
|
8511
8675
|
GuestUsername,
|
|
8676
|
+
HYDRATION_MARKER_ID,
|
|
8512
8677
|
Loggable,
|
|
8513
8678
|
NavigatorRole,
|
|
8514
8679
|
NavigatorRoles,
|