@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
|
@@ -6730,6 +6730,15 @@ var init_userOutcome = __esm({
|
|
|
6730
6730
|
}
|
|
6731
6731
|
});
|
|
6732
6732
|
|
|
6733
|
+
// src/core/types/hydration.ts
|
|
6734
|
+
var HYDRATION_MARKER_ID;
|
|
6735
|
+
var init_hydration = __esm({
|
|
6736
|
+
"src/core/types/hydration.ts"() {
|
|
6737
|
+
"use strict";
|
|
6738
|
+
HYDRATION_MARKER_ID = "_local/hydration";
|
|
6739
|
+
}
|
|
6740
|
+
});
|
|
6741
|
+
|
|
6733
6742
|
// src/core/util/index.ts
|
|
6734
6743
|
function getCardHistoryID(courseID, cardID) {
|
|
6735
6744
|
return `${DocTypePrefixes["CARDRECORD" /* CARDRECORD */]}-${courseID}-${cardID}`;
|
|
@@ -7225,6 +7234,7 @@ var init_core = __esm({
|
|
|
7225
7234
|
init_user();
|
|
7226
7235
|
init_strategyState();
|
|
7227
7236
|
init_userOutcome();
|
|
7237
|
+
init_hydration();
|
|
7228
7238
|
init_Loggable();
|
|
7229
7239
|
init_util();
|
|
7230
7240
|
init_navigators();
|
|
@@ -7300,6 +7310,19 @@ var init_user_course_relDB = __esm({
|
|
|
7300
7310
|
// src/impl/common/BaseUserDB.ts
|
|
7301
7311
|
import { Status as Status3 } from "@vue-skuilder/common";
|
|
7302
7312
|
import moment5 from "moment";
|
|
7313
|
+
function withTimeout(p, ms, label) {
|
|
7314
|
+
let timer;
|
|
7315
|
+
return Promise.race([
|
|
7316
|
+
p,
|
|
7317
|
+
new Promise((_resolve, reject) => {
|
|
7318
|
+
timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
|
|
7319
|
+
})
|
|
7320
|
+
]).finally(() => {
|
|
7321
|
+
if (timer !== void 0) {
|
|
7322
|
+
clearTimeout(timer);
|
|
7323
|
+
}
|
|
7324
|
+
});
|
|
7325
|
+
}
|
|
7303
7326
|
function accomodateGuest() {
|
|
7304
7327
|
logger.log("[funnel] accomodateGuest() called");
|
|
7305
7328
|
if (typeof localStorage === "undefined") {
|
|
@@ -7473,7 +7496,7 @@ async function dropUserFromClassroom(user, classID) {
|
|
|
7473
7496
|
async function getUserClassrooms(user) {
|
|
7474
7497
|
return getOrCreateClassroomRegistrationsDoc(user);
|
|
7475
7498
|
}
|
|
7476
|
-
var log3, BaseUser, userCoursesDoc, userClassroomsDoc;
|
|
7499
|
+
var log3, HYDRATION_TIMEOUT_MS, BaseUser, userCoursesDoc, userClassroomsDoc;
|
|
7477
7500
|
var init_BaseUserDB = __esm({
|
|
7478
7501
|
"src/impl/common/BaseUserDB.ts"() {
|
|
7479
7502
|
"use strict";
|
|
@@ -7488,6 +7511,7 @@ var init_BaseUserDB = __esm({
|
|
|
7488
7511
|
log3 = (s) => {
|
|
7489
7512
|
logger.info(s);
|
|
7490
7513
|
};
|
|
7514
|
+
HYDRATION_TIMEOUT_MS = 15e3;
|
|
7491
7515
|
BaseUser = class _BaseUser {
|
|
7492
7516
|
static _instance;
|
|
7493
7517
|
static _initialized = false;
|
|
@@ -7516,6 +7540,29 @@ var init_BaseUserDB = __esm({
|
|
|
7516
7540
|
writeDB;
|
|
7517
7541
|
// Database to use for write operations (local-first approach)
|
|
7518
7542
|
updateQueue;
|
|
7543
|
+
_hydration = { state: "not-required" };
|
|
7544
|
+
/**
|
|
7545
|
+
* How far the local mirror can be trusted. See {@link UserHydrationStatus}.
|
|
7546
|
+
*
|
|
7547
|
+
* Consumers that would otherwise read a 404 as "new user" — onboarding
|
|
7548
|
+
* gates, first-run experiences, progress dashboards — should check for
|
|
7549
|
+
* `failed` and present a retry affordance rather than an empty state.
|
|
7550
|
+
*/
|
|
7551
|
+
hydrationStatus() {
|
|
7552
|
+
return { ...this._hydration };
|
|
7553
|
+
}
|
|
7554
|
+
/**
|
|
7555
|
+
* Whether a missing document may be treated as genuinely absent, allowing
|
|
7556
|
+
* callers to create it with defaults.
|
|
7557
|
+
*
|
|
7558
|
+
* False only when hydration failed or is still running: a 404 then may just
|
|
7559
|
+
* mean "not pulled down yet", and materializing a default would both lie to
|
|
7560
|
+
* the user and, once live sync starts, push a rev-1 document that loses to
|
|
7561
|
+
* the real one — silently discarding it.
|
|
7562
|
+
*/
|
|
7563
|
+
canMaterializeDefaults() {
|
|
7564
|
+
return this._hydration.state !== "failed" && this._hydration.state !== "hydrating";
|
|
7565
|
+
}
|
|
7519
7566
|
async createAccount(username, password) {
|
|
7520
7567
|
if (!this.syncStrategy.canCreateAccount()) {
|
|
7521
7568
|
throw new Error("Account creation not supported by current sync strategy");
|
|
@@ -7633,6 +7680,11 @@ Currently logged-in as ${this._username}.`
|
|
|
7633
7680
|
} catch (e) {
|
|
7634
7681
|
const err = e;
|
|
7635
7682
|
if (err.status === 404) {
|
|
7683
|
+
if (!this.canMaterializeDefaults()) {
|
|
7684
|
+
throw new Error(
|
|
7685
|
+
`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.`
|
|
7686
|
+
);
|
|
7687
|
+
}
|
|
7636
7688
|
await this.localDB.put({
|
|
7637
7689
|
_id: _BaseUser.DOC_IDS.COURSE_REGISTRATIONS,
|
|
7638
7690
|
courses: [],
|
|
@@ -7875,6 +7927,11 @@ Currently logged-in as ${this._username}.`
|
|
|
7875
7927
|
} catch (e) {
|
|
7876
7928
|
const err = e;
|
|
7877
7929
|
if (err.name && err.name === "not_found") {
|
|
7930
|
+
if (!this.canMaterializeDefaults()) {
|
|
7931
|
+
throw new Error(
|
|
7932
|
+
`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.`
|
|
7933
|
+
);
|
|
7934
|
+
}
|
|
7878
7935
|
await this.localDB.put(defaultConfig);
|
|
7879
7936
|
return this.getConfig();
|
|
7880
7937
|
} else {
|
|
@@ -7948,7 +8005,9 @@ Currently logged-in as ${this._username}.`
|
|
|
7948
8005
|
_BaseUser._initialized = true;
|
|
7949
8006
|
return;
|
|
7950
8007
|
}
|
|
8008
|
+
this.syncStrategy.stopSync?.();
|
|
7951
8009
|
this.setDBandQ();
|
|
8010
|
+
await this.hydrateLocalMirror();
|
|
7952
8011
|
this.syncStrategy.startSync(this.localDB, this.remoteDB);
|
|
7953
8012
|
this.applyDesignDocs().catch((error) => {
|
|
7954
8013
|
log3(`Error in applyDesignDocs background task: ${error}`);
|
|
@@ -7964,6 +8023,85 @@ Currently logged-in as ${this._username}.`
|
|
|
7964
8023
|
});
|
|
7965
8024
|
_BaseUser._initialized = true;
|
|
7966
8025
|
}
|
|
8026
|
+
/**
|
|
8027
|
+
* Pull this account's documents into the local mirror, if that hasn't
|
|
8028
|
+
* happened on this device before.
|
|
8029
|
+
*
|
|
8030
|
+
* Runs between setDBandQ() and startSync() — after the handles exist, before
|
|
8031
|
+
* anything can observe an empty local DB or replicate one upward.
|
|
8032
|
+
*
|
|
8033
|
+
* Never throws: a failure is recorded as `failed` state and reported through
|
|
8034
|
+
* hydrationStatus(), because throwing here would abort init() and leave
|
|
8035
|
+
* BaseUser._initialized false forever (BaseUser.instance() polls it with no
|
|
8036
|
+
* ceiling). Callers decide what a failure means for them.
|
|
8037
|
+
*/
|
|
8038
|
+
async hydrateLocalMirror() {
|
|
8039
|
+
if (this.localDB.name === this.remoteDB.name || !this.syncStrategy.hydrate) {
|
|
8040
|
+
this._hydration = { state: "not-required" };
|
|
8041
|
+
return;
|
|
8042
|
+
}
|
|
8043
|
+
if (await this.hasHydrationMarker()) {
|
|
8044
|
+
this._hydration = { state: "stale" };
|
|
8045
|
+
return;
|
|
8046
|
+
}
|
|
8047
|
+
this._hydration = { state: "hydrating" };
|
|
8048
|
+
const start = Date.now();
|
|
8049
|
+
try {
|
|
8050
|
+
const { docsWritten } = await withTimeout(
|
|
8051
|
+
this.syncStrategy.hydrate(this.localDB, this.remoteDB),
|
|
8052
|
+
HYDRATION_TIMEOUT_MS,
|
|
8053
|
+
`Hydration of local mirror for ${this._username}`
|
|
8054
|
+
);
|
|
8055
|
+
await this.writeHydrationMarker();
|
|
8056
|
+
this._hydration = {
|
|
8057
|
+
state: "hydrated",
|
|
8058
|
+
docsWritten,
|
|
8059
|
+
durationMs: Date.now() - start
|
|
8060
|
+
};
|
|
8061
|
+
log3(
|
|
8062
|
+
`Hydrated local mirror for ${this._username}: ${docsWritten} docs in ${this._hydration.durationMs}ms`
|
|
8063
|
+
);
|
|
8064
|
+
} catch (e) {
|
|
8065
|
+
this.syncStrategy.stopSync?.();
|
|
8066
|
+
this._hydration = {
|
|
8067
|
+
state: "failed",
|
|
8068
|
+
durationMs: Date.now() - start,
|
|
8069
|
+
error: e instanceof Error ? e.message : String(e)
|
|
8070
|
+
};
|
|
8071
|
+
logger.error(`Failed to hydrate local mirror for ${this._username}:`, e);
|
|
8072
|
+
}
|
|
8073
|
+
}
|
|
8074
|
+
/**
|
|
8075
|
+
* Has a full pull completed on this device for the CURRENT account?
|
|
8076
|
+
*
|
|
8077
|
+
* The marker is a `_local/` document, which never replicates — so its
|
|
8078
|
+
* presence describes this device's mirror and cannot arrive from elsewhere.
|
|
8079
|
+
*/
|
|
8080
|
+
async hasHydrationMarker() {
|
|
8081
|
+
try {
|
|
8082
|
+
const marker = await this.localDB.get(HYDRATION_MARKER_ID);
|
|
8083
|
+
return marker.username === this._username;
|
|
8084
|
+
} catch {
|
|
8085
|
+
return false;
|
|
8086
|
+
}
|
|
8087
|
+
}
|
|
8088
|
+
async writeHydrationMarker() {
|
|
8089
|
+
try {
|
|
8090
|
+
let existingRev;
|
|
8091
|
+
try {
|
|
8092
|
+
existingRev = (await this.localDB.get(HYDRATION_MARKER_ID))._rev;
|
|
8093
|
+
} catch {
|
|
8094
|
+
}
|
|
8095
|
+
await this.localDB.put({
|
|
8096
|
+
_id: HYDRATION_MARKER_ID,
|
|
8097
|
+
_rev: existingRev,
|
|
8098
|
+
username: this._username,
|
|
8099
|
+
hydratedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
8100
|
+
});
|
|
8101
|
+
} catch (e) {
|
|
8102
|
+
logger.warn(`Could not write hydration marker for ${this._username}: ${e}`);
|
|
8103
|
+
}
|
|
8104
|
+
}
|
|
7967
8105
|
static designDocs = [
|
|
7968
8106
|
{
|
|
7969
8107
|
_id: "_design/reviewCards",
|
|
@@ -8300,6 +8438,11 @@ Currently logged-in as ${this._username}.`
|
|
|
8300
8438
|
} catch (e) {
|
|
8301
8439
|
const err = e;
|
|
8302
8440
|
if (err.status === 404) {
|
|
8441
|
+
if (!this.canMaterializeDefaults()) {
|
|
8442
|
+
throw new Error(
|
|
8443
|
+
`Cannot read strategy state '${strategyKey}' for ${this._username}: the local mirror is unavailable (hydration state '${this._hydration.state}').`
|
|
8444
|
+
);
|
|
8445
|
+
}
|
|
8303
8446
|
return null;
|
|
8304
8447
|
}
|
|
8305
8448
|
throw e;
|
|
@@ -8823,6 +8966,8 @@ var init_CouchDBSyncStrategy = __esm({
|
|
|
8823
8966
|
CouchDBSyncStrategy = class {
|
|
8824
8967
|
syncHandle;
|
|
8825
8968
|
// Handle to cancel sync if needed
|
|
8969
|
+
/** In-flight one-shot hydration pull, so a timed-out pull can be abandoned. */
|
|
8970
|
+
hydrationHandle;
|
|
8826
8971
|
setupRemoteDB(username) {
|
|
8827
8972
|
if (username === GuestUsername || username.startsWith(GuestUsername)) {
|
|
8828
8973
|
return getLocalUserDB(username);
|
|
@@ -8837,8 +8982,26 @@ var init_CouchDBSyncStrategy = __esm({
|
|
|
8837
8982
|
return this.getUserDB(username);
|
|
8838
8983
|
}
|
|
8839
8984
|
}
|
|
8985
|
+
/**
|
|
8986
|
+
* One-shot remote → local pull. Resolves once the local mirror is caught up.
|
|
8987
|
+
*
|
|
8988
|
+
* Pull only: pushing here would upload whatever the local DB happens to hold
|
|
8989
|
+
* before we know what the remote already has, which is the conflict-leaf
|
|
8990
|
+
* problem hydration exists to avoid. Local changes go up when startSync()
|
|
8991
|
+
* takes over.
|
|
8992
|
+
*/
|
|
8993
|
+
async hydrate(localDB, remoteDB) {
|
|
8994
|
+
const replication = pouchdb_setup_default.replicate(remoteDB, localDB, {});
|
|
8995
|
+
this.hydrationHandle = replication;
|
|
8996
|
+
try {
|
|
8997
|
+
const info = await replication;
|
|
8998
|
+
return { docsWritten: info.docs_written };
|
|
8999
|
+
} finally {
|
|
9000
|
+
this.hydrationHandle = void 0;
|
|
9001
|
+
}
|
|
9002
|
+
}
|
|
8840
9003
|
startSync(localDB, remoteDB) {
|
|
8841
|
-
if (localDB !== remoteDB) {
|
|
9004
|
+
if (localDB.name !== remoteDB.name) {
|
|
8842
9005
|
this.syncHandle = pouchdb_setup_default.sync(localDB, remoteDB, {
|
|
8843
9006
|
live: true,
|
|
8844
9007
|
retry: true
|
|
@@ -8846,8 +9009,20 @@ var init_CouchDBSyncStrategy = __esm({
|
|
|
8846
9009
|
}
|
|
8847
9010
|
}
|
|
8848
9011
|
stopSync() {
|
|
9012
|
+
if (this.hydrationHandle) {
|
|
9013
|
+
try {
|
|
9014
|
+
this.hydrationHandle.cancel();
|
|
9015
|
+
} catch (e) {
|
|
9016
|
+
logger.warn(`Failed to cancel hydration pull (continuing anyway): ${e}`);
|
|
9017
|
+
}
|
|
9018
|
+
this.hydrationHandle = void 0;
|
|
9019
|
+
}
|
|
8849
9020
|
if (this.syncHandle) {
|
|
8850
|
-
|
|
9021
|
+
try {
|
|
9022
|
+
this.syncHandle.cancel();
|
|
9023
|
+
} catch (e) {
|
|
9024
|
+
logger.warn(`Failed to cancel user sync (continuing anyway): ${e}`);
|
|
9025
|
+
}
|
|
8851
9026
|
this.syncHandle = void 0;
|
|
8852
9027
|
}
|
|
8853
9028
|
}
|