@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
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { U as UserDBInterface, C as CourseDBInterface, b as CoursesDBInterface, c as ClassroomDBInterface, A as AdminDBInterface, a as UserDBReader, d as CourseInfo, S as StudySessionItem, D as DataLayerResult, e as ContentNavigationStrategyData, f as ContentNavigator, R as ReplanHints, G as GeneratorResult } from '../../contentSource-
|
|
2
|
-
import { D as DataLayerProvider } from '../../dataLayerProvider-
|
|
1
|
+
import { U as UserDBInterface, C as CourseDBInterface, b as CoursesDBInterface, c as ClassroomDBInterface, A as AdminDBInterface, a as UserDBReader, d as CourseInfo, S as StudySessionItem, D as DataLayerResult, e as ContentNavigationStrategyData, f as ContentNavigator, R as ReplanHints, G as GeneratorResult } from '../../contentSource-D-LQYFyx.cjs';
|
|
2
|
+
import { D as DataLayerProvider } from '../../dataLayerProvider-Dd2UcCif.cjs';
|
|
3
3
|
import { S as StaticCourseManifest } from '../../types-BFUa1pa3.cjs';
|
|
4
4
|
import { CourseConfig, CourseElo, DataShape } from '@vue-skuilder/common';
|
|
5
5
|
import { S as SkuilderCourseData, Q as QualifiedCardID, T as TagStub, a as Tag } from '../../types-legacy-4tlwHnXo.cjs';
|
|
6
|
-
import { S as SyncStrategy, A as AccountCreationResult, a as AuthenticationResult } from '../../SyncStrategy-
|
|
6
|
+
import { S as SyncStrategy, A as AccountCreationResult, a as AuthenticationResult } from '../../SyncStrategy-BJMZq3WO.cjs';
|
|
7
7
|
import 'moment';
|
|
8
8
|
|
|
9
9
|
interface SkuilderManifest {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { U as UserDBInterface, C as CourseDBInterface, b as CoursesDBInterface, c as ClassroomDBInterface, A as AdminDBInterface, a as UserDBReader, d as CourseInfo, S as StudySessionItem, D as DataLayerResult, e as ContentNavigationStrategyData, f as ContentNavigator, R as ReplanHints, G as GeneratorResult } from '../../contentSource-
|
|
2
|
-
import { D as DataLayerProvider } from '../../dataLayerProvider-
|
|
1
|
+
import { U as UserDBInterface, C as CourseDBInterface, b as CoursesDBInterface, c as ClassroomDBInterface, A as AdminDBInterface, a as UserDBReader, d as CourseInfo, S as StudySessionItem, D as DataLayerResult, e as ContentNavigationStrategyData, f as ContentNavigator, R as ReplanHints, G as GeneratorResult } from '../../contentSource-BDRX_YYJ.js';
|
|
2
|
+
import { D as DataLayerProvider } from '../../dataLayerProvider-Dpshuql4.js';
|
|
3
3
|
import { S as StaticCourseManifest } from '../../types-CHgpWQAY.js';
|
|
4
4
|
import { CourseConfig, CourseElo, DataShape } from '@vue-skuilder/common';
|
|
5
5
|
import { S as SkuilderCourseData, Q as QualifiedCardID, T as TagStub, a as Tag } from '../../types-legacy-4tlwHnXo.js';
|
|
6
|
-
import { S as SyncStrategy, A as AccountCreationResult, a as AuthenticationResult } from '../../SyncStrategy-
|
|
6
|
+
import { S as SyncStrategy, A as AccountCreationResult, a as AuthenticationResult } from '../../SyncStrategy-BJMZq3WO.js';
|
|
7
7
|
import 'moment';
|
|
8
8
|
|
|
9
9
|
interface SkuilderManifest {
|
|
@@ -5391,6 +5391,19 @@ var init_couch = __esm({
|
|
|
5391
5391
|
});
|
|
5392
5392
|
|
|
5393
5393
|
// src/impl/common/BaseUserDB.ts
|
|
5394
|
+
function withTimeout(p, ms, label) {
|
|
5395
|
+
let timer;
|
|
5396
|
+
return Promise.race([
|
|
5397
|
+
p,
|
|
5398
|
+
new Promise((_resolve, reject) => {
|
|
5399
|
+
timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
|
|
5400
|
+
})
|
|
5401
|
+
]).finally(() => {
|
|
5402
|
+
if (timer !== void 0) {
|
|
5403
|
+
clearTimeout(timer);
|
|
5404
|
+
}
|
|
5405
|
+
});
|
|
5406
|
+
}
|
|
5394
5407
|
function accomodateGuest() {
|
|
5395
5408
|
logger.log("[funnel] accomodateGuest() called");
|
|
5396
5409
|
if (typeof localStorage === "undefined") {
|
|
@@ -5564,7 +5577,7 @@ async function dropUserFromClassroom(user, classID) {
|
|
|
5564
5577
|
async function getUserClassrooms(user) {
|
|
5565
5578
|
return getOrCreateClassroomRegistrationsDoc(user);
|
|
5566
5579
|
}
|
|
5567
|
-
var import_common12, import_moment6, log3, BaseUser, userCoursesDoc, userClassroomsDoc;
|
|
5580
|
+
var import_common12, import_moment6, log3, HYDRATION_TIMEOUT_MS, BaseUser, userCoursesDoc, userClassroomsDoc;
|
|
5568
5581
|
var init_BaseUserDB = __esm({
|
|
5569
5582
|
"src/impl/common/BaseUserDB.ts"() {
|
|
5570
5583
|
"use strict";
|
|
@@ -5581,6 +5594,7 @@ var init_BaseUserDB = __esm({
|
|
|
5581
5594
|
log3 = (s) => {
|
|
5582
5595
|
logger.info(s);
|
|
5583
5596
|
};
|
|
5597
|
+
HYDRATION_TIMEOUT_MS = 15e3;
|
|
5584
5598
|
BaseUser = class _BaseUser {
|
|
5585
5599
|
static _instance;
|
|
5586
5600
|
static _initialized = false;
|
|
@@ -5609,6 +5623,49 @@ var init_BaseUserDB = __esm({
|
|
|
5609
5623
|
writeDB;
|
|
5610
5624
|
// Database to use for write operations (local-first approach)
|
|
5611
5625
|
updateQueue;
|
|
5626
|
+
_hydration = { state: "not-required" };
|
|
5627
|
+
/** In-flight hydration, so concurrent callers can wait for a real answer. */
|
|
5628
|
+
_hydrationPromise = null;
|
|
5629
|
+
/**
|
|
5630
|
+
* How far the local mirror can be trusted, RIGHT NOW. See
|
|
5631
|
+
* {@link UserHydrationStatus}.
|
|
5632
|
+
*
|
|
5633
|
+
* This is a snapshot, and during login it can legitimately read
|
|
5634
|
+
* `hydrating` — prefer awaitHydration() anywhere a decision hangs on the
|
|
5635
|
+
* answer.
|
|
5636
|
+
*/
|
|
5637
|
+
hydrationStatus() {
|
|
5638
|
+
return { ...this._hydration };
|
|
5639
|
+
}
|
|
5640
|
+
/**
|
|
5641
|
+
* The hydration status once it has settled — never `hydrating`.
|
|
5642
|
+
*
|
|
5643
|
+
* Resolves immediately unless a pull is in flight. Exists for callers that
|
|
5644
|
+
* must decide something (a route guard, a first-run branch) and would
|
|
5645
|
+
* otherwise sample `hydrating` and guess. Since init() is awaited by both
|
|
5646
|
+
* app startup and login(), that only happens when something navigates
|
|
5647
|
+
* concurrently with a login still in progress — a window that stretches to
|
|
5648
|
+
* HYDRATION_TIMEOUT_MS on a slow connection.
|
|
5649
|
+
*/
|
|
5650
|
+
async awaitHydration() {
|
|
5651
|
+
try {
|
|
5652
|
+
await this._hydrationPromise;
|
|
5653
|
+
} catch {
|
|
5654
|
+
}
|
|
5655
|
+
return this.hydrationStatus();
|
|
5656
|
+
}
|
|
5657
|
+
/**
|
|
5658
|
+
* Whether a missing document may be treated as genuinely absent, allowing
|
|
5659
|
+
* callers to create it with defaults.
|
|
5660
|
+
*
|
|
5661
|
+
* False only when hydration failed or is still running: a 404 then may just
|
|
5662
|
+
* mean "not pulled down yet", and materializing a default would both lie to
|
|
5663
|
+
* the user and, once live sync starts, push a rev-1 document that loses to
|
|
5664
|
+
* the real one — silently discarding it.
|
|
5665
|
+
*/
|
|
5666
|
+
canMaterializeDefaults() {
|
|
5667
|
+
return this._hydration.state !== "failed" && this._hydration.state !== "hydrating";
|
|
5668
|
+
}
|
|
5612
5669
|
async createAccount(username, password) {
|
|
5613
5670
|
if (!this.syncStrategy.canCreateAccount()) {
|
|
5614
5671
|
throw new Error("Account creation not supported by current sync strategy");
|
|
@@ -5726,6 +5783,11 @@ Currently logged-in as ${this._username}.`
|
|
|
5726
5783
|
} catch (e) {
|
|
5727
5784
|
const err = e;
|
|
5728
5785
|
if (err.status === 404) {
|
|
5786
|
+
if (!this.canMaterializeDefaults()) {
|
|
5787
|
+
throw new Error(
|
|
5788
|
+
`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.`
|
|
5789
|
+
);
|
|
5790
|
+
}
|
|
5729
5791
|
await this.localDB.put({
|
|
5730
5792
|
_id: _BaseUser.DOC_IDS.COURSE_REGISTRATIONS,
|
|
5731
5793
|
courses: [],
|
|
@@ -5968,6 +6030,11 @@ Currently logged-in as ${this._username}.`
|
|
|
5968
6030
|
} catch (e) {
|
|
5969
6031
|
const err = e;
|
|
5970
6032
|
if (err.name && err.name === "not_found") {
|
|
6033
|
+
if (!this.canMaterializeDefaults()) {
|
|
6034
|
+
throw new Error(
|
|
6035
|
+
`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.`
|
|
6036
|
+
);
|
|
6037
|
+
}
|
|
5971
6038
|
await this.localDB.put(defaultConfig);
|
|
5972
6039
|
return this.getConfig();
|
|
5973
6040
|
} else {
|
|
@@ -6041,7 +6108,10 @@ Currently logged-in as ${this._username}.`
|
|
|
6041
6108
|
_BaseUser._initialized = true;
|
|
6042
6109
|
return;
|
|
6043
6110
|
}
|
|
6111
|
+
this.syncStrategy.stopSync?.();
|
|
6044
6112
|
this.setDBandQ();
|
|
6113
|
+
this._hydrationPromise = this.hydrateLocalMirror();
|
|
6114
|
+
await this._hydrationPromise;
|
|
6045
6115
|
this.syncStrategy.startSync(this.localDB, this.remoteDB);
|
|
6046
6116
|
this.applyDesignDocs().catch((error) => {
|
|
6047
6117
|
log3(`Error in applyDesignDocs background task: ${error}`);
|
|
@@ -6057,6 +6127,85 @@ Currently logged-in as ${this._username}.`
|
|
|
6057
6127
|
});
|
|
6058
6128
|
_BaseUser._initialized = true;
|
|
6059
6129
|
}
|
|
6130
|
+
/**
|
|
6131
|
+
* Pull this account's documents into the local mirror, if that hasn't
|
|
6132
|
+
* happened on this device before.
|
|
6133
|
+
*
|
|
6134
|
+
* Runs between setDBandQ() and startSync() — after the handles exist, before
|
|
6135
|
+
* anything can observe an empty local DB or replicate one upward.
|
|
6136
|
+
*
|
|
6137
|
+
* Never throws: a failure is recorded as `failed` state and reported through
|
|
6138
|
+
* hydrationStatus(), because throwing here would abort init() and leave
|
|
6139
|
+
* BaseUser._initialized false forever (BaseUser.instance() polls it with no
|
|
6140
|
+
* ceiling). Callers decide what a failure means for them.
|
|
6141
|
+
*/
|
|
6142
|
+
async hydrateLocalMirror() {
|
|
6143
|
+
if (this.localDB.name === this.remoteDB.name || !this.syncStrategy.hydrate) {
|
|
6144
|
+
this._hydration = { state: "not-required" };
|
|
6145
|
+
return;
|
|
6146
|
+
}
|
|
6147
|
+
if (await this.hasHydrationMarker()) {
|
|
6148
|
+
this._hydration = { state: "stale" };
|
|
6149
|
+
return;
|
|
6150
|
+
}
|
|
6151
|
+
this._hydration = { state: "hydrating" };
|
|
6152
|
+
const start = Date.now();
|
|
6153
|
+
try {
|
|
6154
|
+
const { docsWritten } = await withTimeout(
|
|
6155
|
+
this.syncStrategy.hydrate(this.localDB, this.remoteDB),
|
|
6156
|
+
HYDRATION_TIMEOUT_MS,
|
|
6157
|
+
`Hydration of local mirror for ${this._username}`
|
|
6158
|
+
);
|
|
6159
|
+
await this.writeHydrationMarker();
|
|
6160
|
+
this._hydration = {
|
|
6161
|
+
state: "hydrated",
|
|
6162
|
+
docsWritten,
|
|
6163
|
+
durationMs: Date.now() - start
|
|
6164
|
+
};
|
|
6165
|
+
log3(
|
|
6166
|
+
`Hydrated local mirror for ${this._username}: ${docsWritten} docs in ${this._hydration.durationMs}ms`
|
|
6167
|
+
);
|
|
6168
|
+
} catch (e) {
|
|
6169
|
+
this.syncStrategy.stopSync?.();
|
|
6170
|
+
this._hydration = {
|
|
6171
|
+
state: "failed",
|
|
6172
|
+
durationMs: Date.now() - start,
|
|
6173
|
+
error: e instanceof Error ? e.message : String(e)
|
|
6174
|
+
};
|
|
6175
|
+
logger.error(`Failed to hydrate local mirror for ${this._username}:`, e);
|
|
6176
|
+
}
|
|
6177
|
+
}
|
|
6178
|
+
/**
|
|
6179
|
+
* Has a full pull completed on this device for the CURRENT account?
|
|
6180
|
+
*
|
|
6181
|
+
* The marker is a `_local/` document, which never replicates — so its
|
|
6182
|
+
* presence describes this device's mirror and cannot arrive from elsewhere.
|
|
6183
|
+
*/
|
|
6184
|
+
async hasHydrationMarker() {
|
|
6185
|
+
try {
|
|
6186
|
+
const marker = await this.localDB.get(HYDRATION_MARKER_ID);
|
|
6187
|
+
return marker.username === this._username;
|
|
6188
|
+
} catch {
|
|
6189
|
+
return false;
|
|
6190
|
+
}
|
|
6191
|
+
}
|
|
6192
|
+
async writeHydrationMarker() {
|
|
6193
|
+
try {
|
|
6194
|
+
let existingRev;
|
|
6195
|
+
try {
|
|
6196
|
+
existingRev = (await this.localDB.get(HYDRATION_MARKER_ID))._rev;
|
|
6197
|
+
} catch {
|
|
6198
|
+
}
|
|
6199
|
+
await this.localDB.put({
|
|
6200
|
+
_id: HYDRATION_MARKER_ID,
|
|
6201
|
+
_rev: existingRev,
|
|
6202
|
+
username: this._username,
|
|
6203
|
+
hydratedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6204
|
+
});
|
|
6205
|
+
} catch (e) {
|
|
6206
|
+
logger.warn(`Could not write hydration marker for ${this._username}: ${e}`);
|
|
6207
|
+
}
|
|
6208
|
+
}
|
|
6060
6209
|
static designDocs = [
|
|
6061
6210
|
{
|
|
6062
6211
|
_id: "_design/reviewCards",
|
|
@@ -6393,6 +6542,11 @@ Currently logged-in as ${this._username}.`
|
|
|
6393
6542
|
} catch (e) {
|
|
6394
6543
|
const err = e;
|
|
6395
6544
|
if (err.status === 404) {
|
|
6545
|
+
if (!this.canMaterializeDefaults()) {
|
|
6546
|
+
throw new Error(
|
|
6547
|
+
`Cannot read strategy state '${strategyKey}' for ${this._username}: the local mirror is unavailable (hydration state '${this._hydration.state}').`
|
|
6548
|
+
);
|
|
6549
|
+
}
|
|
6396
6550
|
return null;
|
|
6397
6551
|
}
|
|
6398
6552
|
throw e;
|
|
@@ -6568,6 +6722,15 @@ var init_userOutcome = __esm({
|
|
|
6568
6722
|
}
|
|
6569
6723
|
});
|
|
6570
6724
|
|
|
6725
|
+
// src/core/types/hydration.ts
|
|
6726
|
+
var HYDRATION_MARKER_ID;
|
|
6727
|
+
var init_hydration = __esm({
|
|
6728
|
+
"src/core/types/hydration.ts"() {
|
|
6729
|
+
"use strict";
|
|
6730
|
+
HYDRATION_MARKER_ID = "_local/hydration";
|
|
6731
|
+
}
|
|
6732
|
+
});
|
|
6733
|
+
|
|
6571
6734
|
// src/core/bulkImport/cardProcessor.ts
|
|
6572
6735
|
var import_common16;
|
|
6573
6736
|
var init_cardProcessor = __esm({
|
|
@@ -6937,6 +7100,7 @@ var init_core = __esm({
|
|
|
6937
7100
|
init_user();
|
|
6938
7101
|
init_strategyState();
|
|
6939
7102
|
init_userOutcome();
|
|
7103
|
+
init_hydration();
|
|
6940
7104
|
init_Loggable();
|
|
6941
7105
|
init_util();
|
|
6942
7106
|
init_navigators();
|