@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
|
@@ -5370,6 +5370,19 @@ var init_couch = __esm({
|
|
|
5370
5370
|
// src/impl/common/BaseUserDB.ts
|
|
5371
5371
|
import { Status as Status3 } from "@vue-skuilder/common";
|
|
5372
5372
|
import moment6 from "moment";
|
|
5373
|
+
function withTimeout(p, ms, label) {
|
|
5374
|
+
let timer;
|
|
5375
|
+
return Promise.race([
|
|
5376
|
+
p,
|
|
5377
|
+
new Promise((_resolve, reject) => {
|
|
5378
|
+
timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
|
|
5379
|
+
})
|
|
5380
|
+
]).finally(() => {
|
|
5381
|
+
if (timer !== void 0) {
|
|
5382
|
+
clearTimeout(timer);
|
|
5383
|
+
}
|
|
5384
|
+
});
|
|
5385
|
+
}
|
|
5373
5386
|
function accomodateGuest() {
|
|
5374
5387
|
logger.log("[funnel] accomodateGuest() called");
|
|
5375
5388
|
if (typeof localStorage === "undefined") {
|
|
@@ -5543,7 +5556,7 @@ async function dropUserFromClassroom(user, classID) {
|
|
|
5543
5556
|
async function getUserClassrooms(user) {
|
|
5544
5557
|
return getOrCreateClassroomRegistrationsDoc(user);
|
|
5545
5558
|
}
|
|
5546
|
-
var log3, BaseUser, userCoursesDoc, userClassroomsDoc;
|
|
5559
|
+
var log3, HYDRATION_TIMEOUT_MS, BaseUser, userCoursesDoc, userClassroomsDoc;
|
|
5547
5560
|
var init_BaseUserDB = __esm({
|
|
5548
5561
|
"src/impl/common/BaseUserDB.ts"() {
|
|
5549
5562
|
"use strict";
|
|
@@ -5558,6 +5571,7 @@ var init_BaseUserDB = __esm({
|
|
|
5558
5571
|
log3 = (s) => {
|
|
5559
5572
|
logger.info(s);
|
|
5560
5573
|
};
|
|
5574
|
+
HYDRATION_TIMEOUT_MS = 15e3;
|
|
5561
5575
|
BaseUser = class _BaseUser {
|
|
5562
5576
|
static _instance;
|
|
5563
5577
|
static _initialized = false;
|
|
@@ -5586,6 +5600,49 @@ var init_BaseUserDB = __esm({
|
|
|
5586
5600
|
writeDB;
|
|
5587
5601
|
// Database to use for write operations (local-first approach)
|
|
5588
5602
|
updateQueue;
|
|
5603
|
+
_hydration = { state: "not-required" };
|
|
5604
|
+
/** In-flight hydration, so concurrent callers can wait for a real answer. */
|
|
5605
|
+
_hydrationPromise = null;
|
|
5606
|
+
/**
|
|
5607
|
+
* How far the local mirror can be trusted, RIGHT NOW. See
|
|
5608
|
+
* {@link UserHydrationStatus}.
|
|
5609
|
+
*
|
|
5610
|
+
* This is a snapshot, and during login it can legitimately read
|
|
5611
|
+
* `hydrating` — prefer awaitHydration() anywhere a decision hangs on the
|
|
5612
|
+
* answer.
|
|
5613
|
+
*/
|
|
5614
|
+
hydrationStatus() {
|
|
5615
|
+
return { ...this._hydration };
|
|
5616
|
+
}
|
|
5617
|
+
/**
|
|
5618
|
+
* The hydration status once it has settled — never `hydrating`.
|
|
5619
|
+
*
|
|
5620
|
+
* Resolves immediately unless a pull is in flight. Exists for callers that
|
|
5621
|
+
* must decide something (a route guard, a first-run branch) and would
|
|
5622
|
+
* otherwise sample `hydrating` and guess. Since init() is awaited by both
|
|
5623
|
+
* app startup and login(), that only happens when something navigates
|
|
5624
|
+
* concurrently with a login still in progress — a window that stretches to
|
|
5625
|
+
* HYDRATION_TIMEOUT_MS on a slow connection.
|
|
5626
|
+
*/
|
|
5627
|
+
async awaitHydration() {
|
|
5628
|
+
try {
|
|
5629
|
+
await this._hydrationPromise;
|
|
5630
|
+
} catch {
|
|
5631
|
+
}
|
|
5632
|
+
return this.hydrationStatus();
|
|
5633
|
+
}
|
|
5634
|
+
/**
|
|
5635
|
+
* Whether a missing document may be treated as genuinely absent, allowing
|
|
5636
|
+
* callers to create it with defaults.
|
|
5637
|
+
*
|
|
5638
|
+
* False only when hydration failed or is still running: a 404 then may just
|
|
5639
|
+
* mean "not pulled down yet", and materializing a default would both lie to
|
|
5640
|
+
* the user and, once live sync starts, push a rev-1 document that loses to
|
|
5641
|
+
* the real one — silently discarding it.
|
|
5642
|
+
*/
|
|
5643
|
+
canMaterializeDefaults() {
|
|
5644
|
+
return this._hydration.state !== "failed" && this._hydration.state !== "hydrating";
|
|
5645
|
+
}
|
|
5589
5646
|
async createAccount(username, password) {
|
|
5590
5647
|
if (!this.syncStrategy.canCreateAccount()) {
|
|
5591
5648
|
throw new Error("Account creation not supported by current sync strategy");
|
|
@@ -5703,6 +5760,11 @@ Currently logged-in as ${this._username}.`
|
|
|
5703
5760
|
} catch (e) {
|
|
5704
5761
|
const err = e;
|
|
5705
5762
|
if (err.status === 404) {
|
|
5763
|
+
if (!this.canMaterializeDefaults()) {
|
|
5764
|
+
throw new Error(
|
|
5765
|
+
`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.`
|
|
5766
|
+
);
|
|
5767
|
+
}
|
|
5706
5768
|
await this.localDB.put({
|
|
5707
5769
|
_id: _BaseUser.DOC_IDS.COURSE_REGISTRATIONS,
|
|
5708
5770
|
courses: [],
|
|
@@ -5945,6 +6007,11 @@ Currently logged-in as ${this._username}.`
|
|
|
5945
6007
|
} catch (e) {
|
|
5946
6008
|
const err = e;
|
|
5947
6009
|
if (err.name && err.name === "not_found") {
|
|
6010
|
+
if (!this.canMaterializeDefaults()) {
|
|
6011
|
+
throw new Error(
|
|
6012
|
+
`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.`
|
|
6013
|
+
);
|
|
6014
|
+
}
|
|
5948
6015
|
await this.localDB.put(defaultConfig);
|
|
5949
6016
|
return this.getConfig();
|
|
5950
6017
|
} else {
|
|
@@ -6018,7 +6085,10 @@ Currently logged-in as ${this._username}.`
|
|
|
6018
6085
|
_BaseUser._initialized = true;
|
|
6019
6086
|
return;
|
|
6020
6087
|
}
|
|
6088
|
+
this.syncStrategy.stopSync?.();
|
|
6021
6089
|
this.setDBandQ();
|
|
6090
|
+
this._hydrationPromise = this.hydrateLocalMirror();
|
|
6091
|
+
await this._hydrationPromise;
|
|
6022
6092
|
this.syncStrategy.startSync(this.localDB, this.remoteDB);
|
|
6023
6093
|
this.applyDesignDocs().catch((error) => {
|
|
6024
6094
|
log3(`Error in applyDesignDocs background task: ${error}`);
|
|
@@ -6034,6 +6104,85 @@ Currently logged-in as ${this._username}.`
|
|
|
6034
6104
|
});
|
|
6035
6105
|
_BaseUser._initialized = true;
|
|
6036
6106
|
}
|
|
6107
|
+
/**
|
|
6108
|
+
* Pull this account's documents into the local mirror, if that hasn't
|
|
6109
|
+
* happened on this device before.
|
|
6110
|
+
*
|
|
6111
|
+
* Runs between setDBandQ() and startSync() — after the handles exist, before
|
|
6112
|
+
* anything can observe an empty local DB or replicate one upward.
|
|
6113
|
+
*
|
|
6114
|
+
* Never throws: a failure is recorded as `failed` state and reported through
|
|
6115
|
+
* hydrationStatus(), because throwing here would abort init() and leave
|
|
6116
|
+
* BaseUser._initialized false forever (BaseUser.instance() polls it with no
|
|
6117
|
+
* ceiling). Callers decide what a failure means for them.
|
|
6118
|
+
*/
|
|
6119
|
+
async hydrateLocalMirror() {
|
|
6120
|
+
if (this.localDB.name === this.remoteDB.name || !this.syncStrategy.hydrate) {
|
|
6121
|
+
this._hydration = { state: "not-required" };
|
|
6122
|
+
return;
|
|
6123
|
+
}
|
|
6124
|
+
if (await this.hasHydrationMarker()) {
|
|
6125
|
+
this._hydration = { state: "stale" };
|
|
6126
|
+
return;
|
|
6127
|
+
}
|
|
6128
|
+
this._hydration = { state: "hydrating" };
|
|
6129
|
+
const start = Date.now();
|
|
6130
|
+
try {
|
|
6131
|
+
const { docsWritten } = await withTimeout(
|
|
6132
|
+
this.syncStrategy.hydrate(this.localDB, this.remoteDB),
|
|
6133
|
+
HYDRATION_TIMEOUT_MS,
|
|
6134
|
+
`Hydration of local mirror for ${this._username}`
|
|
6135
|
+
);
|
|
6136
|
+
await this.writeHydrationMarker();
|
|
6137
|
+
this._hydration = {
|
|
6138
|
+
state: "hydrated",
|
|
6139
|
+
docsWritten,
|
|
6140
|
+
durationMs: Date.now() - start
|
|
6141
|
+
};
|
|
6142
|
+
log3(
|
|
6143
|
+
`Hydrated local mirror for ${this._username}: ${docsWritten} docs in ${this._hydration.durationMs}ms`
|
|
6144
|
+
);
|
|
6145
|
+
} catch (e) {
|
|
6146
|
+
this.syncStrategy.stopSync?.();
|
|
6147
|
+
this._hydration = {
|
|
6148
|
+
state: "failed",
|
|
6149
|
+
durationMs: Date.now() - start,
|
|
6150
|
+
error: e instanceof Error ? e.message : String(e)
|
|
6151
|
+
};
|
|
6152
|
+
logger.error(`Failed to hydrate local mirror for ${this._username}:`, e);
|
|
6153
|
+
}
|
|
6154
|
+
}
|
|
6155
|
+
/**
|
|
6156
|
+
* Has a full pull completed on this device for the CURRENT account?
|
|
6157
|
+
*
|
|
6158
|
+
* The marker is a `_local/` document, which never replicates — so its
|
|
6159
|
+
* presence describes this device's mirror and cannot arrive from elsewhere.
|
|
6160
|
+
*/
|
|
6161
|
+
async hasHydrationMarker() {
|
|
6162
|
+
try {
|
|
6163
|
+
const marker = await this.localDB.get(HYDRATION_MARKER_ID);
|
|
6164
|
+
return marker.username === this._username;
|
|
6165
|
+
} catch {
|
|
6166
|
+
return false;
|
|
6167
|
+
}
|
|
6168
|
+
}
|
|
6169
|
+
async writeHydrationMarker() {
|
|
6170
|
+
try {
|
|
6171
|
+
let existingRev;
|
|
6172
|
+
try {
|
|
6173
|
+
existingRev = (await this.localDB.get(HYDRATION_MARKER_ID))._rev;
|
|
6174
|
+
} catch {
|
|
6175
|
+
}
|
|
6176
|
+
await this.localDB.put({
|
|
6177
|
+
_id: HYDRATION_MARKER_ID,
|
|
6178
|
+
_rev: existingRev,
|
|
6179
|
+
username: this._username,
|
|
6180
|
+
hydratedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6181
|
+
});
|
|
6182
|
+
} catch (e) {
|
|
6183
|
+
logger.warn(`Could not write hydration marker for ${this._username}: ${e}`);
|
|
6184
|
+
}
|
|
6185
|
+
}
|
|
6037
6186
|
static designDocs = [
|
|
6038
6187
|
{
|
|
6039
6188
|
_id: "_design/reviewCards",
|
|
@@ -6370,6 +6519,11 @@ Currently logged-in as ${this._username}.`
|
|
|
6370
6519
|
} catch (e) {
|
|
6371
6520
|
const err = e;
|
|
6372
6521
|
if (err.status === 404) {
|
|
6522
|
+
if (!this.canMaterializeDefaults()) {
|
|
6523
|
+
throw new Error(
|
|
6524
|
+
`Cannot read strategy state '${strategyKey}' for ${this._username}: the local mirror is unavailable (hydration state '${this._hydration.state}').`
|
|
6525
|
+
);
|
|
6526
|
+
}
|
|
6373
6527
|
return null;
|
|
6374
6528
|
}
|
|
6375
6529
|
throw e;
|
|
@@ -6543,6 +6697,15 @@ var init_userOutcome = __esm({
|
|
|
6543
6697
|
}
|
|
6544
6698
|
});
|
|
6545
6699
|
|
|
6700
|
+
// src/core/types/hydration.ts
|
|
6701
|
+
var HYDRATION_MARKER_ID;
|
|
6702
|
+
var init_hydration = __esm({
|
|
6703
|
+
"src/core/types/hydration.ts"() {
|
|
6704
|
+
"use strict";
|
|
6705
|
+
HYDRATION_MARKER_ID = "_local/hydration";
|
|
6706
|
+
}
|
|
6707
|
+
});
|
|
6708
|
+
|
|
6546
6709
|
// src/core/bulkImport/cardProcessor.ts
|
|
6547
6710
|
import { Status as Status4 } from "@vue-skuilder/common";
|
|
6548
6711
|
var init_cardProcessor = __esm({
|
|
@@ -6911,6 +7074,7 @@ var init_core = __esm({
|
|
|
6911
7074
|
init_user();
|
|
6912
7075
|
init_strategyState();
|
|
6913
7076
|
init_userOutcome();
|
|
7077
|
+
init_hydration();
|
|
6914
7078
|
init_Loggable();
|
|
6915
7079
|
init_util();
|
|
6916
7080
|
init_navigators();
|