@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
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,29 @@ 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
|
+
/**
|
|
6903
|
+
* How far the local mirror can be trusted. See {@link UserHydrationStatus}.
|
|
6904
|
+
*
|
|
6905
|
+
* Consumers that would otherwise read a 404 as "new user" — onboarding
|
|
6906
|
+
* gates, first-run experiences, progress dashboards — should check for
|
|
6907
|
+
* `failed` and present a retry affordance rather than an empty state.
|
|
6908
|
+
*/
|
|
6909
|
+
hydrationStatus() {
|
|
6910
|
+
return { ...this._hydration };
|
|
6911
|
+
}
|
|
6912
|
+
/**
|
|
6913
|
+
* Whether a missing document may be treated as genuinely absent, allowing
|
|
6914
|
+
* callers to create it with defaults.
|
|
6915
|
+
*
|
|
6916
|
+
* False only when hydration failed or is still running: a 404 then may just
|
|
6917
|
+
* mean "not pulled down yet", and materializing a default would both lie to
|
|
6918
|
+
* the user and, once live sync starts, push a rev-1 document that loses to
|
|
6919
|
+
* the real one — silently discarding it.
|
|
6920
|
+
*/
|
|
6921
|
+
canMaterializeDefaults() {
|
|
6922
|
+
return this._hydration.state !== "failed" && this._hydration.state !== "hydrating";
|
|
6923
|
+
}
|
|
6887
6924
|
async createAccount(username, password) {
|
|
6888
6925
|
if (!this.syncStrategy.canCreateAccount()) {
|
|
6889
6926
|
throw new Error("Account creation not supported by current sync strategy");
|
|
@@ -7001,6 +7038,11 @@ Currently logged-in as ${this._username}.`
|
|
|
7001
7038
|
} catch (e) {
|
|
7002
7039
|
const err = e;
|
|
7003
7040
|
if (err.status === 404) {
|
|
7041
|
+
if (!this.canMaterializeDefaults()) {
|
|
7042
|
+
throw new Error(
|
|
7043
|
+
`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.`
|
|
7044
|
+
);
|
|
7045
|
+
}
|
|
7004
7046
|
await this.localDB.put({
|
|
7005
7047
|
_id: _BaseUser.DOC_IDS.COURSE_REGISTRATIONS,
|
|
7006
7048
|
courses: [],
|
|
@@ -7243,6 +7285,11 @@ Currently logged-in as ${this._username}.`
|
|
|
7243
7285
|
} catch (e) {
|
|
7244
7286
|
const err = e;
|
|
7245
7287
|
if (err.name && err.name === "not_found") {
|
|
7288
|
+
if (!this.canMaterializeDefaults()) {
|
|
7289
|
+
throw new Error(
|
|
7290
|
+
`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.`
|
|
7291
|
+
);
|
|
7292
|
+
}
|
|
7246
7293
|
await this.localDB.put(defaultConfig);
|
|
7247
7294
|
return this.getConfig();
|
|
7248
7295
|
} else {
|
|
@@ -7316,7 +7363,9 @@ Currently logged-in as ${this._username}.`
|
|
|
7316
7363
|
_BaseUser._initialized = true;
|
|
7317
7364
|
return;
|
|
7318
7365
|
}
|
|
7366
|
+
this.syncStrategy.stopSync?.();
|
|
7319
7367
|
this.setDBandQ();
|
|
7368
|
+
await this.hydrateLocalMirror();
|
|
7320
7369
|
this.syncStrategy.startSync(this.localDB, this.remoteDB);
|
|
7321
7370
|
this.applyDesignDocs().catch((error) => {
|
|
7322
7371
|
log3(`Error in applyDesignDocs background task: ${error}`);
|
|
@@ -7332,6 +7381,85 @@ Currently logged-in as ${this._username}.`
|
|
|
7332
7381
|
});
|
|
7333
7382
|
_BaseUser._initialized = true;
|
|
7334
7383
|
}
|
|
7384
|
+
/**
|
|
7385
|
+
* Pull this account's documents into the local mirror, if that hasn't
|
|
7386
|
+
* happened on this device before.
|
|
7387
|
+
*
|
|
7388
|
+
* Runs between setDBandQ() and startSync() — after the handles exist, before
|
|
7389
|
+
* anything can observe an empty local DB or replicate one upward.
|
|
7390
|
+
*
|
|
7391
|
+
* Never throws: a failure is recorded as `failed` state and reported through
|
|
7392
|
+
* hydrationStatus(), because throwing here would abort init() and leave
|
|
7393
|
+
* BaseUser._initialized false forever (BaseUser.instance() polls it with no
|
|
7394
|
+
* ceiling). Callers decide what a failure means for them.
|
|
7395
|
+
*/
|
|
7396
|
+
async hydrateLocalMirror() {
|
|
7397
|
+
if (this.localDB.name === this.remoteDB.name || !this.syncStrategy.hydrate) {
|
|
7398
|
+
this._hydration = { state: "not-required" };
|
|
7399
|
+
return;
|
|
7400
|
+
}
|
|
7401
|
+
if (await this.hasHydrationMarker()) {
|
|
7402
|
+
this._hydration = { state: "stale" };
|
|
7403
|
+
return;
|
|
7404
|
+
}
|
|
7405
|
+
this._hydration = { state: "hydrating" };
|
|
7406
|
+
const start = Date.now();
|
|
7407
|
+
try {
|
|
7408
|
+
const { docsWritten } = await withTimeout(
|
|
7409
|
+
this.syncStrategy.hydrate(this.localDB, this.remoteDB),
|
|
7410
|
+
HYDRATION_TIMEOUT_MS,
|
|
7411
|
+
`Hydration of local mirror for ${this._username}`
|
|
7412
|
+
);
|
|
7413
|
+
await this.writeHydrationMarker();
|
|
7414
|
+
this._hydration = {
|
|
7415
|
+
state: "hydrated",
|
|
7416
|
+
docsWritten,
|
|
7417
|
+
durationMs: Date.now() - start
|
|
7418
|
+
};
|
|
7419
|
+
log3(
|
|
7420
|
+
`Hydrated local mirror for ${this._username}: ${docsWritten} docs in ${this._hydration.durationMs}ms`
|
|
7421
|
+
);
|
|
7422
|
+
} catch (e) {
|
|
7423
|
+
this.syncStrategy.stopSync?.();
|
|
7424
|
+
this._hydration = {
|
|
7425
|
+
state: "failed",
|
|
7426
|
+
durationMs: Date.now() - start,
|
|
7427
|
+
error: e instanceof Error ? e.message : String(e)
|
|
7428
|
+
};
|
|
7429
|
+
logger.error(`Failed to hydrate local mirror for ${this._username}:`, e);
|
|
7430
|
+
}
|
|
7431
|
+
}
|
|
7432
|
+
/**
|
|
7433
|
+
* Has a full pull completed on this device for the CURRENT account?
|
|
7434
|
+
*
|
|
7435
|
+
* The marker is a `_local/` document, which never replicates — so its
|
|
7436
|
+
* presence describes this device's mirror and cannot arrive from elsewhere.
|
|
7437
|
+
*/
|
|
7438
|
+
async hasHydrationMarker() {
|
|
7439
|
+
try {
|
|
7440
|
+
const marker = await this.localDB.get(HYDRATION_MARKER_ID);
|
|
7441
|
+
return marker.username === this._username;
|
|
7442
|
+
} catch {
|
|
7443
|
+
return false;
|
|
7444
|
+
}
|
|
7445
|
+
}
|
|
7446
|
+
async writeHydrationMarker() {
|
|
7447
|
+
try {
|
|
7448
|
+
let existingRev;
|
|
7449
|
+
try {
|
|
7450
|
+
existingRev = (await this.localDB.get(HYDRATION_MARKER_ID))._rev;
|
|
7451
|
+
} catch {
|
|
7452
|
+
}
|
|
7453
|
+
await this.localDB.put({
|
|
7454
|
+
_id: HYDRATION_MARKER_ID,
|
|
7455
|
+
_rev: existingRev,
|
|
7456
|
+
username: this._username,
|
|
7457
|
+
hydratedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
7458
|
+
});
|
|
7459
|
+
} catch (e) {
|
|
7460
|
+
logger.warn(`Could not write hydration marker for ${this._username}: ${e}`);
|
|
7461
|
+
}
|
|
7462
|
+
}
|
|
7335
7463
|
static designDocs = [
|
|
7336
7464
|
{
|
|
7337
7465
|
_id: "_design/reviewCards",
|
|
@@ -7668,6 +7796,11 @@ Currently logged-in as ${this._username}.`
|
|
|
7668
7796
|
} catch (e) {
|
|
7669
7797
|
const err = e;
|
|
7670
7798
|
if (err.status === 404) {
|
|
7799
|
+
if (!this.canMaterializeDefaults()) {
|
|
7800
|
+
throw new Error(
|
|
7801
|
+
`Cannot read strategy state '${strategyKey}' for ${this._username}: the local mirror is unavailable (hydration state '${this._hydration.state}').`
|
|
7802
|
+
);
|
|
7803
|
+
}
|
|
7671
7804
|
return null;
|
|
7672
7805
|
}
|
|
7673
7806
|
throw e;
|
|
@@ -8015,6 +8148,15 @@ var init_userOutcome = __esm({
|
|
|
8015
8148
|
}
|
|
8016
8149
|
});
|
|
8017
8150
|
|
|
8151
|
+
// src/core/types/hydration.ts
|
|
8152
|
+
var HYDRATION_MARKER_ID;
|
|
8153
|
+
var init_hydration = __esm({
|
|
8154
|
+
"src/core/types/hydration.ts"() {
|
|
8155
|
+
"use strict";
|
|
8156
|
+
HYDRATION_MARKER_ID = "_local/hydration";
|
|
8157
|
+
}
|
|
8158
|
+
});
|
|
8159
|
+
|
|
8018
8160
|
// src/core/bulkImport/cardProcessor.ts
|
|
8019
8161
|
import { Status as Status4 } from "@vue-skuilder/common";
|
|
8020
8162
|
async function importParsedCards(parsedCards, courseDB, config) {
|
|
@@ -8493,6 +8635,7 @@ var init_core = __esm({
|
|
|
8493
8635
|
init_user();
|
|
8494
8636
|
init_strategyState();
|
|
8495
8637
|
init_userOutcome();
|
|
8638
|
+
init_hydration();
|
|
8496
8639
|
init_Loggable();
|
|
8497
8640
|
init_util();
|
|
8498
8641
|
init_navigators();
|
|
@@ -8509,6 +8652,7 @@ export {
|
|
|
8509
8652
|
DocType,
|
|
8510
8653
|
DocTypePrefixes,
|
|
8511
8654
|
GuestUsername,
|
|
8655
|
+
HYDRATION_MARKER_ID,
|
|
8512
8656
|
Loggable,
|
|
8513
8657
|
NavigatorRole,
|
|
8514
8658
|
NavigatorRoles,
|