@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/index.mjs
CHANGED
|
@@ -7338,6 +7338,8 @@ var init_CouchDBSyncStrategy = __esm({
|
|
|
7338
7338
|
CouchDBSyncStrategy = class {
|
|
7339
7339
|
syncHandle;
|
|
7340
7340
|
// Handle to cancel sync if needed
|
|
7341
|
+
/** In-flight one-shot hydration pull, so a timed-out pull can be abandoned. */
|
|
7342
|
+
hydrationHandle;
|
|
7341
7343
|
setupRemoteDB(username) {
|
|
7342
7344
|
if (username === GuestUsername || username.startsWith(GuestUsername)) {
|
|
7343
7345
|
return getLocalUserDB(username);
|
|
@@ -7352,8 +7354,26 @@ var init_CouchDBSyncStrategy = __esm({
|
|
|
7352
7354
|
return this.getUserDB(username);
|
|
7353
7355
|
}
|
|
7354
7356
|
}
|
|
7357
|
+
/**
|
|
7358
|
+
* One-shot remote → local pull. Resolves once the local mirror is caught up.
|
|
7359
|
+
*
|
|
7360
|
+
* Pull only: pushing here would upload whatever the local DB happens to hold
|
|
7361
|
+
* before we know what the remote already has, which is the conflict-leaf
|
|
7362
|
+
* problem hydration exists to avoid. Local changes go up when startSync()
|
|
7363
|
+
* takes over.
|
|
7364
|
+
*/
|
|
7365
|
+
async hydrate(localDB, remoteDB) {
|
|
7366
|
+
const replication = pouchdb_setup_default.replicate(remoteDB, localDB, {});
|
|
7367
|
+
this.hydrationHandle = replication;
|
|
7368
|
+
try {
|
|
7369
|
+
const info = await replication;
|
|
7370
|
+
return { docsWritten: info.docs_written };
|
|
7371
|
+
} finally {
|
|
7372
|
+
this.hydrationHandle = void 0;
|
|
7373
|
+
}
|
|
7374
|
+
}
|
|
7355
7375
|
startSync(localDB, remoteDB) {
|
|
7356
|
-
if (localDB !== remoteDB) {
|
|
7376
|
+
if (localDB.name !== remoteDB.name) {
|
|
7357
7377
|
this.syncHandle = pouchdb_setup_default.sync(localDB, remoteDB, {
|
|
7358
7378
|
live: true,
|
|
7359
7379
|
retry: true
|
|
@@ -7361,8 +7381,20 @@ var init_CouchDBSyncStrategy = __esm({
|
|
|
7361
7381
|
}
|
|
7362
7382
|
}
|
|
7363
7383
|
stopSync() {
|
|
7384
|
+
if (this.hydrationHandle) {
|
|
7385
|
+
try {
|
|
7386
|
+
this.hydrationHandle.cancel();
|
|
7387
|
+
} catch (e) {
|
|
7388
|
+
logger.warn(`Failed to cancel hydration pull (continuing anyway): ${e}`);
|
|
7389
|
+
}
|
|
7390
|
+
this.hydrationHandle = void 0;
|
|
7391
|
+
}
|
|
7364
7392
|
if (this.syncHandle) {
|
|
7365
|
-
|
|
7393
|
+
try {
|
|
7394
|
+
this.syncHandle.cancel();
|
|
7395
|
+
} catch (e) {
|
|
7396
|
+
logger.warn(`Failed to cancel user sync (continuing anyway): ${e}`);
|
|
7397
|
+
}
|
|
7366
7398
|
this.syncHandle = void 0;
|
|
7367
7399
|
}
|
|
7368
7400
|
}
|
|
@@ -7633,6 +7665,19 @@ var init_couch = __esm({
|
|
|
7633
7665
|
// src/impl/common/BaseUserDB.ts
|
|
7634
7666
|
import { Status as Status3 } from "@vue-skuilder/common";
|
|
7635
7667
|
import moment6 from "moment";
|
|
7668
|
+
function withTimeout(p, ms, label) {
|
|
7669
|
+
let timer;
|
|
7670
|
+
return Promise.race([
|
|
7671
|
+
p,
|
|
7672
|
+
new Promise((_resolve, reject) => {
|
|
7673
|
+
timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
|
|
7674
|
+
})
|
|
7675
|
+
]).finally(() => {
|
|
7676
|
+
if (timer !== void 0) {
|
|
7677
|
+
clearTimeout(timer);
|
|
7678
|
+
}
|
|
7679
|
+
});
|
|
7680
|
+
}
|
|
7636
7681
|
function accomodateGuest() {
|
|
7637
7682
|
logger.log("[funnel] accomodateGuest() called");
|
|
7638
7683
|
if (typeof localStorage === "undefined") {
|
|
@@ -7806,7 +7851,7 @@ async function dropUserFromClassroom(user, classID) {
|
|
|
7806
7851
|
async function getUserClassrooms(user) {
|
|
7807
7852
|
return getOrCreateClassroomRegistrationsDoc(user);
|
|
7808
7853
|
}
|
|
7809
|
-
var log4, BaseUser, userCoursesDoc, userClassroomsDoc;
|
|
7854
|
+
var log4, HYDRATION_TIMEOUT_MS, BaseUser, userCoursesDoc, userClassroomsDoc;
|
|
7810
7855
|
var init_BaseUserDB = __esm({
|
|
7811
7856
|
"src/impl/common/BaseUserDB.ts"() {
|
|
7812
7857
|
"use strict";
|
|
@@ -7821,6 +7866,7 @@ var init_BaseUserDB = __esm({
|
|
|
7821
7866
|
log4 = (s) => {
|
|
7822
7867
|
logger.info(s);
|
|
7823
7868
|
};
|
|
7869
|
+
HYDRATION_TIMEOUT_MS = 15e3;
|
|
7824
7870
|
BaseUser = class _BaseUser {
|
|
7825
7871
|
static _instance;
|
|
7826
7872
|
static _initialized = false;
|
|
@@ -7849,6 +7895,29 @@ var init_BaseUserDB = __esm({
|
|
|
7849
7895
|
writeDB;
|
|
7850
7896
|
// Database to use for write operations (local-first approach)
|
|
7851
7897
|
updateQueue;
|
|
7898
|
+
_hydration = { state: "not-required" };
|
|
7899
|
+
/**
|
|
7900
|
+
* How far the local mirror can be trusted. See {@link UserHydrationStatus}.
|
|
7901
|
+
*
|
|
7902
|
+
* Consumers that would otherwise read a 404 as "new user" — onboarding
|
|
7903
|
+
* gates, first-run experiences, progress dashboards — should check for
|
|
7904
|
+
* `failed` and present a retry affordance rather than an empty state.
|
|
7905
|
+
*/
|
|
7906
|
+
hydrationStatus() {
|
|
7907
|
+
return { ...this._hydration };
|
|
7908
|
+
}
|
|
7909
|
+
/**
|
|
7910
|
+
* Whether a missing document may be treated as genuinely absent, allowing
|
|
7911
|
+
* callers to create it with defaults.
|
|
7912
|
+
*
|
|
7913
|
+
* False only when hydration failed or is still running: a 404 then may just
|
|
7914
|
+
* mean "not pulled down yet", and materializing a default would both lie to
|
|
7915
|
+
* the user and, once live sync starts, push a rev-1 document that loses to
|
|
7916
|
+
* the real one — silently discarding it.
|
|
7917
|
+
*/
|
|
7918
|
+
canMaterializeDefaults() {
|
|
7919
|
+
return this._hydration.state !== "failed" && this._hydration.state !== "hydrating";
|
|
7920
|
+
}
|
|
7852
7921
|
async createAccount(username, password) {
|
|
7853
7922
|
if (!this.syncStrategy.canCreateAccount()) {
|
|
7854
7923
|
throw new Error("Account creation not supported by current sync strategy");
|
|
@@ -7966,6 +8035,11 @@ Currently logged-in as ${this._username}.`
|
|
|
7966
8035
|
} catch (e) {
|
|
7967
8036
|
const err = e;
|
|
7968
8037
|
if (err.status === 404) {
|
|
8038
|
+
if (!this.canMaterializeDefaults()) {
|
|
8039
|
+
throw new Error(
|
|
8040
|
+
`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.`
|
|
8041
|
+
);
|
|
8042
|
+
}
|
|
7969
8043
|
await this.localDB.put({
|
|
7970
8044
|
_id: _BaseUser.DOC_IDS.COURSE_REGISTRATIONS,
|
|
7971
8045
|
courses: [],
|
|
@@ -8208,6 +8282,11 @@ Currently logged-in as ${this._username}.`
|
|
|
8208
8282
|
} catch (e) {
|
|
8209
8283
|
const err = e;
|
|
8210
8284
|
if (err.name && err.name === "not_found") {
|
|
8285
|
+
if (!this.canMaterializeDefaults()) {
|
|
8286
|
+
throw new Error(
|
|
8287
|
+
`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.`
|
|
8288
|
+
);
|
|
8289
|
+
}
|
|
8211
8290
|
await this.localDB.put(defaultConfig);
|
|
8212
8291
|
return this.getConfig();
|
|
8213
8292
|
} else {
|
|
@@ -8281,7 +8360,9 @@ Currently logged-in as ${this._username}.`
|
|
|
8281
8360
|
_BaseUser._initialized = true;
|
|
8282
8361
|
return;
|
|
8283
8362
|
}
|
|
8363
|
+
this.syncStrategy.stopSync?.();
|
|
8284
8364
|
this.setDBandQ();
|
|
8365
|
+
await this.hydrateLocalMirror();
|
|
8285
8366
|
this.syncStrategy.startSync(this.localDB, this.remoteDB);
|
|
8286
8367
|
this.applyDesignDocs().catch((error) => {
|
|
8287
8368
|
log4(`Error in applyDesignDocs background task: ${error}`);
|
|
@@ -8297,6 +8378,85 @@ Currently logged-in as ${this._username}.`
|
|
|
8297
8378
|
});
|
|
8298
8379
|
_BaseUser._initialized = true;
|
|
8299
8380
|
}
|
|
8381
|
+
/**
|
|
8382
|
+
* Pull this account's documents into the local mirror, if that hasn't
|
|
8383
|
+
* happened on this device before.
|
|
8384
|
+
*
|
|
8385
|
+
* Runs between setDBandQ() and startSync() — after the handles exist, before
|
|
8386
|
+
* anything can observe an empty local DB or replicate one upward.
|
|
8387
|
+
*
|
|
8388
|
+
* Never throws: a failure is recorded as `failed` state and reported through
|
|
8389
|
+
* hydrationStatus(), because throwing here would abort init() and leave
|
|
8390
|
+
* BaseUser._initialized false forever (BaseUser.instance() polls it with no
|
|
8391
|
+
* ceiling). Callers decide what a failure means for them.
|
|
8392
|
+
*/
|
|
8393
|
+
async hydrateLocalMirror() {
|
|
8394
|
+
if (this.localDB.name === this.remoteDB.name || !this.syncStrategy.hydrate) {
|
|
8395
|
+
this._hydration = { state: "not-required" };
|
|
8396
|
+
return;
|
|
8397
|
+
}
|
|
8398
|
+
if (await this.hasHydrationMarker()) {
|
|
8399
|
+
this._hydration = { state: "stale" };
|
|
8400
|
+
return;
|
|
8401
|
+
}
|
|
8402
|
+
this._hydration = { state: "hydrating" };
|
|
8403
|
+
const start = Date.now();
|
|
8404
|
+
try {
|
|
8405
|
+
const { docsWritten } = await withTimeout(
|
|
8406
|
+
this.syncStrategy.hydrate(this.localDB, this.remoteDB),
|
|
8407
|
+
HYDRATION_TIMEOUT_MS,
|
|
8408
|
+
`Hydration of local mirror for ${this._username}`
|
|
8409
|
+
);
|
|
8410
|
+
await this.writeHydrationMarker();
|
|
8411
|
+
this._hydration = {
|
|
8412
|
+
state: "hydrated",
|
|
8413
|
+
docsWritten,
|
|
8414
|
+
durationMs: Date.now() - start
|
|
8415
|
+
};
|
|
8416
|
+
log4(
|
|
8417
|
+
`Hydrated local mirror for ${this._username}: ${docsWritten} docs in ${this._hydration.durationMs}ms`
|
|
8418
|
+
);
|
|
8419
|
+
} catch (e) {
|
|
8420
|
+
this.syncStrategy.stopSync?.();
|
|
8421
|
+
this._hydration = {
|
|
8422
|
+
state: "failed",
|
|
8423
|
+
durationMs: Date.now() - start,
|
|
8424
|
+
error: e instanceof Error ? e.message : String(e)
|
|
8425
|
+
};
|
|
8426
|
+
logger.error(`Failed to hydrate local mirror for ${this._username}:`, e);
|
|
8427
|
+
}
|
|
8428
|
+
}
|
|
8429
|
+
/**
|
|
8430
|
+
* Has a full pull completed on this device for the CURRENT account?
|
|
8431
|
+
*
|
|
8432
|
+
* The marker is a `_local/` document, which never replicates — so its
|
|
8433
|
+
* presence describes this device's mirror and cannot arrive from elsewhere.
|
|
8434
|
+
*/
|
|
8435
|
+
async hasHydrationMarker() {
|
|
8436
|
+
try {
|
|
8437
|
+
const marker = await this.localDB.get(HYDRATION_MARKER_ID);
|
|
8438
|
+
return marker.username === this._username;
|
|
8439
|
+
} catch {
|
|
8440
|
+
return false;
|
|
8441
|
+
}
|
|
8442
|
+
}
|
|
8443
|
+
async writeHydrationMarker() {
|
|
8444
|
+
try {
|
|
8445
|
+
let existingRev;
|
|
8446
|
+
try {
|
|
8447
|
+
existingRev = (await this.localDB.get(HYDRATION_MARKER_ID))._rev;
|
|
8448
|
+
} catch {
|
|
8449
|
+
}
|
|
8450
|
+
await this.localDB.put({
|
|
8451
|
+
_id: HYDRATION_MARKER_ID,
|
|
8452
|
+
_rev: existingRev,
|
|
8453
|
+
username: this._username,
|
|
8454
|
+
hydratedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
8455
|
+
});
|
|
8456
|
+
} catch (e) {
|
|
8457
|
+
logger.warn(`Could not write hydration marker for ${this._username}: ${e}`);
|
|
8458
|
+
}
|
|
8459
|
+
}
|
|
8300
8460
|
static designDocs = [
|
|
8301
8461
|
{
|
|
8302
8462
|
_id: "_design/reviewCards",
|
|
@@ -8633,6 +8793,11 @@ Currently logged-in as ${this._username}.`
|
|
|
8633
8793
|
} catch (e) {
|
|
8634
8794
|
const err = e;
|
|
8635
8795
|
if (err.status === 404) {
|
|
8796
|
+
if (!this.canMaterializeDefaults()) {
|
|
8797
|
+
throw new Error(
|
|
8798
|
+
`Cannot read strategy state '${strategyKey}' for ${this._username}: the local mirror is unavailable (hydration state '${this._hydration.state}').`
|
|
8799
|
+
);
|
|
8800
|
+
}
|
|
8636
8801
|
return null;
|
|
8637
8802
|
}
|
|
8638
8803
|
throw e;
|
|
@@ -10152,6 +10317,15 @@ var init_userOutcome = __esm({
|
|
|
10152
10317
|
}
|
|
10153
10318
|
});
|
|
10154
10319
|
|
|
10320
|
+
// src/core/types/hydration.ts
|
|
10321
|
+
var HYDRATION_MARKER_ID;
|
|
10322
|
+
var init_hydration = __esm({
|
|
10323
|
+
"src/core/types/hydration.ts"() {
|
|
10324
|
+
"use strict";
|
|
10325
|
+
HYDRATION_MARKER_ID = "_local/hydration";
|
|
10326
|
+
}
|
|
10327
|
+
});
|
|
10328
|
+
|
|
10155
10329
|
// src/core/bulkImport/cardProcessor.ts
|
|
10156
10330
|
import { Status as Status5 } from "@vue-skuilder/common";
|
|
10157
10331
|
async function importParsedCards(parsedCards, courseDB, config) {
|
|
@@ -10631,6 +10805,7 @@ var init_core = __esm({
|
|
|
10631
10805
|
init_user();
|
|
10632
10806
|
init_strategyState();
|
|
10633
10807
|
init_userOutcome();
|
|
10808
|
+
init_hydration();
|
|
10634
10809
|
init_Loggable();
|
|
10635
10810
|
init_util();
|
|
10636
10811
|
init_navigators();
|
|
@@ -15336,6 +15511,7 @@ export {
|
|
|
15336
15511
|
ENV,
|
|
15337
15512
|
FileSystemError,
|
|
15338
15513
|
GuestUsername,
|
|
15514
|
+
HYDRATION_MARKER_ID,
|
|
15339
15515
|
Loggable,
|
|
15340
15516
|
NOT_SET,
|
|
15341
15517
|
NavigatorRole,
|