@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
|
@@ -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-CBqZCoGU.cjs';
|
|
2
|
+
import { D as DataLayerProvider } from '../../dataLayerProvider-BBA8tJNx.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-CudEz5Tm.js';
|
|
2
|
+
import { D as DataLayerProvider } from '../../dataLayerProvider-C9WgkBzR.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,29 @@ 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
|
+
/**
|
|
5628
|
+
* How far the local mirror can be trusted. See {@link UserHydrationStatus}.
|
|
5629
|
+
*
|
|
5630
|
+
* Consumers that would otherwise read a 404 as "new user" — onboarding
|
|
5631
|
+
* gates, first-run experiences, progress dashboards — should check for
|
|
5632
|
+
* `failed` and present a retry affordance rather than an empty state.
|
|
5633
|
+
*/
|
|
5634
|
+
hydrationStatus() {
|
|
5635
|
+
return { ...this._hydration };
|
|
5636
|
+
}
|
|
5637
|
+
/**
|
|
5638
|
+
* Whether a missing document may be treated as genuinely absent, allowing
|
|
5639
|
+
* callers to create it with defaults.
|
|
5640
|
+
*
|
|
5641
|
+
* False only when hydration failed or is still running: a 404 then may just
|
|
5642
|
+
* mean "not pulled down yet", and materializing a default would both lie to
|
|
5643
|
+
* the user and, once live sync starts, push a rev-1 document that loses to
|
|
5644
|
+
* the real one — silently discarding it.
|
|
5645
|
+
*/
|
|
5646
|
+
canMaterializeDefaults() {
|
|
5647
|
+
return this._hydration.state !== "failed" && this._hydration.state !== "hydrating";
|
|
5648
|
+
}
|
|
5612
5649
|
async createAccount(username, password) {
|
|
5613
5650
|
if (!this.syncStrategy.canCreateAccount()) {
|
|
5614
5651
|
throw new Error("Account creation not supported by current sync strategy");
|
|
@@ -5726,6 +5763,11 @@ Currently logged-in as ${this._username}.`
|
|
|
5726
5763
|
} catch (e) {
|
|
5727
5764
|
const err = e;
|
|
5728
5765
|
if (err.status === 404) {
|
|
5766
|
+
if (!this.canMaterializeDefaults()) {
|
|
5767
|
+
throw new Error(
|
|
5768
|
+
`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.`
|
|
5769
|
+
);
|
|
5770
|
+
}
|
|
5729
5771
|
await this.localDB.put({
|
|
5730
5772
|
_id: _BaseUser.DOC_IDS.COURSE_REGISTRATIONS,
|
|
5731
5773
|
courses: [],
|
|
@@ -5968,6 +6010,11 @@ Currently logged-in as ${this._username}.`
|
|
|
5968
6010
|
} catch (e) {
|
|
5969
6011
|
const err = e;
|
|
5970
6012
|
if (err.name && err.name === "not_found") {
|
|
6013
|
+
if (!this.canMaterializeDefaults()) {
|
|
6014
|
+
throw new Error(
|
|
6015
|
+
`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.`
|
|
6016
|
+
);
|
|
6017
|
+
}
|
|
5971
6018
|
await this.localDB.put(defaultConfig);
|
|
5972
6019
|
return this.getConfig();
|
|
5973
6020
|
} else {
|
|
@@ -6041,7 +6088,9 @@ Currently logged-in as ${this._username}.`
|
|
|
6041
6088
|
_BaseUser._initialized = true;
|
|
6042
6089
|
return;
|
|
6043
6090
|
}
|
|
6091
|
+
this.syncStrategy.stopSync?.();
|
|
6044
6092
|
this.setDBandQ();
|
|
6093
|
+
await this.hydrateLocalMirror();
|
|
6045
6094
|
this.syncStrategy.startSync(this.localDB, this.remoteDB);
|
|
6046
6095
|
this.applyDesignDocs().catch((error) => {
|
|
6047
6096
|
log3(`Error in applyDesignDocs background task: ${error}`);
|
|
@@ -6057,6 +6106,85 @@ Currently logged-in as ${this._username}.`
|
|
|
6057
6106
|
});
|
|
6058
6107
|
_BaseUser._initialized = true;
|
|
6059
6108
|
}
|
|
6109
|
+
/**
|
|
6110
|
+
* Pull this account's documents into the local mirror, if that hasn't
|
|
6111
|
+
* happened on this device before.
|
|
6112
|
+
*
|
|
6113
|
+
* Runs between setDBandQ() and startSync() — after the handles exist, before
|
|
6114
|
+
* anything can observe an empty local DB or replicate one upward.
|
|
6115
|
+
*
|
|
6116
|
+
* Never throws: a failure is recorded as `failed` state and reported through
|
|
6117
|
+
* hydrationStatus(), because throwing here would abort init() and leave
|
|
6118
|
+
* BaseUser._initialized false forever (BaseUser.instance() polls it with no
|
|
6119
|
+
* ceiling). Callers decide what a failure means for them.
|
|
6120
|
+
*/
|
|
6121
|
+
async hydrateLocalMirror() {
|
|
6122
|
+
if (this.localDB.name === this.remoteDB.name || !this.syncStrategy.hydrate) {
|
|
6123
|
+
this._hydration = { state: "not-required" };
|
|
6124
|
+
return;
|
|
6125
|
+
}
|
|
6126
|
+
if (await this.hasHydrationMarker()) {
|
|
6127
|
+
this._hydration = { state: "stale" };
|
|
6128
|
+
return;
|
|
6129
|
+
}
|
|
6130
|
+
this._hydration = { state: "hydrating" };
|
|
6131
|
+
const start = Date.now();
|
|
6132
|
+
try {
|
|
6133
|
+
const { docsWritten } = await withTimeout(
|
|
6134
|
+
this.syncStrategy.hydrate(this.localDB, this.remoteDB),
|
|
6135
|
+
HYDRATION_TIMEOUT_MS,
|
|
6136
|
+
`Hydration of local mirror for ${this._username}`
|
|
6137
|
+
);
|
|
6138
|
+
await this.writeHydrationMarker();
|
|
6139
|
+
this._hydration = {
|
|
6140
|
+
state: "hydrated",
|
|
6141
|
+
docsWritten,
|
|
6142
|
+
durationMs: Date.now() - start
|
|
6143
|
+
};
|
|
6144
|
+
log3(
|
|
6145
|
+
`Hydrated local mirror for ${this._username}: ${docsWritten} docs in ${this._hydration.durationMs}ms`
|
|
6146
|
+
);
|
|
6147
|
+
} catch (e) {
|
|
6148
|
+
this.syncStrategy.stopSync?.();
|
|
6149
|
+
this._hydration = {
|
|
6150
|
+
state: "failed",
|
|
6151
|
+
durationMs: Date.now() - start,
|
|
6152
|
+
error: e instanceof Error ? e.message : String(e)
|
|
6153
|
+
};
|
|
6154
|
+
logger.error(`Failed to hydrate local mirror for ${this._username}:`, e);
|
|
6155
|
+
}
|
|
6156
|
+
}
|
|
6157
|
+
/**
|
|
6158
|
+
* Has a full pull completed on this device for the CURRENT account?
|
|
6159
|
+
*
|
|
6160
|
+
* The marker is a `_local/` document, which never replicates — so its
|
|
6161
|
+
* presence describes this device's mirror and cannot arrive from elsewhere.
|
|
6162
|
+
*/
|
|
6163
|
+
async hasHydrationMarker() {
|
|
6164
|
+
try {
|
|
6165
|
+
const marker = await this.localDB.get(HYDRATION_MARKER_ID);
|
|
6166
|
+
return marker.username === this._username;
|
|
6167
|
+
} catch {
|
|
6168
|
+
return false;
|
|
6169
|
+
}
|
|
6170
|
+
}
|
|
6171
|
+
async writeHydrationMarker() {
|
|
6172
|
+
try {
|
|
6173
|
+
let existingRev;
|
|
6174
|
+
try {
|
|
6175
|
+
existingRev = (await this.localDB.get(HYDRATION_MARKER_ID))._rev;
|
|
6176
|
+
} catch {
|
|
6177
|
+
}
|
|
6178
|
+
await this.localDB.put({
|
|
6179
|
+
_id: HYDRATION_MARKER_ID,
|
|
6180
|
+
_rev: existingRev,
|
|
6181
|
+
username: this._username,
|
|
6182
|
+
hydratedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6183
|
+
});
|
|
6184
|
+
} catch (e) {
|
|
6185
|
+
logger.warn(`Could not write hydration marker for ${this._username}: ${e}`);
|
|
6186
|
+
}
|
|
6187
|
+
}
|
|
6060
6188
|
static designDocs = [
|
|
6061
6189
|
{
|
|
6062
6190
|
_id: "_design/reviewCards",
|
|
@@ -6393,6 +6521,11 @@ Currently logged-in as ${this._username}.`
|
|
|
6393
6521
|
} catch (e) {
|
|
6394
6522
|
const err = e;
|
|
6395
6523
|
if (err.status === 404) {
|
|
6524
|
+
if (!this.canMaterializeDefaults()) {
|
|
6525
|
+
throw new Error(
|
|
6526
|
+
`Cannot read strategy state '${strategyKey}' for ${this._username}: the local mirror is unavailable (hydration state '${this._hydration.state}').`
|
|
6527
|
+
);
|
|
6528
|
+
}
|
|
6396
6529
|
return null;
|
|
6397
6530
|
}
|
|
6398
6531
|
throw e;
|
|
@@ -6568,6 +6701,15 @@ var init_userOutcome = __esm({
|
|
|
6568
6701
|
}
|
|
6569
6702
|
});
|
|
6570
6703
|
|
|
6704
|
+
// src/core/types/hydration.ts
|
|
6705
|
+
var HYDRATION_MARKER_ID;
|
|
6706
|
+
var init_hydration = __esm({
|
|
6707
|
+
"src/core/types/hydration.ts"() {
|
|
6708
|
+
"use strict";
|
|
6709
|
+
HYDRATION_MARKER_ID = "_local/hydration";
|
|
6710
|
+
}
|
|
6711
|
+
});
|
|
6712
|
+
|
|
6571
6713
|
// src/core/bulkImport/cardProcessor.ts
|
|
6572
6714
|
var import_common16;
|
|
6573
6715
|
var init_cardProcessor = __esm({
|
|
@@ -6937,6 +7079,7 @@ var init_core = __esm({
|
|
|
6937
7079
|
init_user();
|
|
6938
7080
|
init_strategyState();
|
|
6939
7081
|
init_userOutcome();
|
|
7082
|
+
init_hydration();
|
|
6940
7083
|
init_Loggable();
|
|
6941
7084
|
init_util();
|
|
6942
7085
|
init_navigators();
|