@burdenoff/microfe-technospam 2026.1101.4 → 2026.1102.1
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/index.js +5 -3
- package/dist/technospam/TechnoSpamRoutes.d.ts.map +1 -1
- package/dist/technospam/TechnoSpamRoutes.js +91 -90
- package/dist/technospam/TechnoSpamRoutes.js.map +1 -1
- package/dist/technospam/api/technospamApi.d.ts +21 -7
- package/dist/technospam/api/technospamApi.d.ts.map +1 -1
- package/dist/technospam/api/technospamApi.js +317 -284
- package/dist/technospam/api/technospamApi.js.map +1 -1
- package/dist/technospam/components/CertificateVerifyLink.d.ts +11 -0
- package/dist/technospam/components/CertificateVerifyLink.d.ts.map +1 -0
- package/dist/technospam/components/CertificateVerifyLink.js +63 -0
- package/dist/technospam/components/CertificateVerifyLink.js.map +1 -0
- package/dist/technospam/components/index.d.ts +1 -0
- package/dist/technospam/components/index.d.ts.map +1 -1
- package/dist/technospam/components/index.js +1 -0
- package/dist/technospam/components/lesson/OfflineProgressSync.d.ts +9 -0
- package/dist/technospam/components/lesson/OfflineProgressSync.d.ts.map +1 -0
- package/dist/technospam/components/lesson/OfflineProgressSync.js +20 -0
- package/dist/technospam/components/lesson/OfflineProgressSync.js.map +1 -0
- package/dist/technospam/components/lesson/SaveCourseOffline.d.ts +17 -0
- package/dist/technospam/components/lesson/SaveCourseOffline.d.ts.map +1 -0
- package/dist/technospam/components/lesson/SaveCourseOffline.js +73 -0
- package/dist/technospam/components/lesson/SaveCourseOffline.js.map +1 -0
- package/dist/technospam/components/lesson/SavedCourseOutline.d.ts +17 -0
- package/dist/technospam/components/lesson/SavedCourseOutline.d.ts.map +1 -0
- package/dist/technospam/components/lesson/SavedCourseOutline.js +77 -0
- package/dist/technospam/components/lesson/SavedCourseOutline.js.map +1 -0
- package/dist/technospam/hooks/useOfflineLessons.d.ts +26 -0
- package/dist/technospam/hooks/useOfflineLessons.d.ts.map +1 -0
- package/dist/technospam/hooks/useOfflineLessons.js +70 -0
- package/dist/technospam/hooks/useOfflineLessons.js.map +1 -0
- package/dist/technospam/index.d.ts +2 -0
- package/dist/technospam/index.d.ts.map +1 -1
- package/dist/technospam/index.js +2 -0
- package/dist/technospam/lib/certificateVerify.d.ts +71 -0
- package/dist/technospam/lib/certificateVerify.d.ts.map +1 -0
- package/dist/technospam/lib/certificateVerify.js +55 -0
- package/dist/technospam/lib/certificateVerify.js.map +1 -0
- package/dist/technospam/lib/offlineLessons.d.ts +97 -0
- package/dist/technospam/lib/offlineLessons.d.ts.map +1 -0
- package/dist/technospam/lib/offlineLessons.js +168 -0
- package/dist/technospam/lib/offlineLessons.js.map +1 -0
- package/dist/technospam/pages/CoursesCourseDetailsPagePage.d.ts.map +1 -1
- package/dist/technospam/pages/CoursesCourseDetailsPagePage.js +241 -198
- package/dist/technospam/pages/CoursesCourseDetailsPagePage.js.map +1 -1
- package/dist/technospam/pages/CoursesLessonPlayerPagePage.d.ts.map +1 -1
- package/dist/technospam/pages/CoursesLessonPlayerPagePage.js +216 -138
- package/dist/technospam/pages/CoursesLessonPlayerPagePage.js.map +1 -1
- package/dist/technospam/pages/CoursesMyLearningPagePage.d.ts.map +1 -1
- package/dist/technospam/pages/CoursesMyLearningPagePage.js +104 -96
- package/dist/technospam/pages/CoursesMyLearningPagePage.js.map +1 -1
- package/dist/technospam/pages/public/CertificateVerifyPage.d.ts +4 -0
- package/dist/technospam/pages/public/CertificateVerifyPage.d.ts.map +1 -0
- package/dist/technospam/pages/public/CertificateVerifyPage.js +257 -0
- package/dist/technospam/pages/public/CertificateVerifyPage.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
//#region src/technospam/lib/offlineLessons.ts
|
|
2
|
+
var e = "technospam.offline.v1";
|
|
3
|
+
function t() {
|
|
4
|
+
try {
|
|
5
|
+
return typeof window < "u" && window.localStorage ? window.localStorage : null;
|
|
6
|
+
} catch {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
function n(e, t) {
|
|
11
|
+
if (!e) return null;
|
|
12
|
+
try {
|
|
13
|
+
let n = e.getItem(t);
|
|
14
|
+
return n ? JSON.parse(n) : null;
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function r(e, t, n) {
|
|
20
|
+
if (!e) return !1;
|
|
21
|
+
try {
|
|
22
|
+
return e.setItem(t, JSON.stringify(n)), !0;
|
|
23
|
+
} catch {
|
|
24
|
+
return !1;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function i(e, t) {
|
|
28
|
+
if (e) try {
|
|
29
|
+
e.removeItem(t);
|
|
30
|
+
} catch {}
|
|
31
|
+
}
|
|
32
|
+
function a(e, t) {
|
|
33
|
+
return e && t ? `${e}:${t}` : "";
|
|
34
|
+
}
|
|
35
|
+
var o = (t) => `${e}:index:${t}`, s = (t) => `${e}:outbox:${t}`, c = (t, n, r) => `${e}:lesson:${t}:${n}:${r}`;
|
|
36
|
+
function l(e, t) {
|
|
37
|
+
let r = n(e, o(t));
|
|
38
|
+
return Array.isArray(r) ? r.filter((e) => typeof e == "string") : [];
|
|
39
|
+
}
|
|
40
|
+
function u() {
|
|
41
|
+
return typeof navigator < "u" && navigator.onLine === !1;
|
|
42
|
+
}
|
|
43
|
+
function d(e) {
|
|
44
|
+
let t = e?.tecLesson, n = e?.tecCourse;
|
|
45
|
+
return !!t && !!n && !t.isLocked && t.courseId === n.id;
|
|
46
|
+
}
|
|
47
|
+
function f(e, n, a, s = t(), u = /* @__PURE__ */ new Date()) {
|
|
48
|
+
if (!e || !d(n)) return !1;
|
|
49
|
+
let { tecLesson: f, tecCourse: p } = n, m = {
|
|
50
|
+
savedAt: u.toISOString(),
|
|
51
|
+
lesson: {
|
|
52
|
+
...f,
|
|
53
|
+
file: null
|
|
54
|
+
},
|
|
55
|
+
course: p,
|
|
56
|
+
enrollment: a ? {
|
|
57
|
+
id: a.id,
|
|
58
|
+
progressPct: a.progressPct
|
|
59
|
+
} : null
|
|
60
|
+
};
|
|
61
|
+
if (!r(s, c(e, p.id, f.id), m)) return !1;
|
|
62
|
+
let h = `${p.id}:${f.id}`, g = l(s, e).filter((e) => e !== h);
|
|
63
|
+
for (g.push(h); g.length > 150;) {
|
|
64
|
+
let t = g.shift();
|
|
65
|
+
if (t) {
|
|
66
|
+
let [n = "", r = ""] = t.split(":");
|
|
67
|
+
i(s, c(e, n, r));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return r(s, o(e), g), !0;
|
|
71
|
+
}
|
|
72
|
+
function p(e, r, i, a = t()) {
|
|
73
|
+
if (!e || !r || !i) return null;
|
|
74
|
+
let o = n(a, c(e, r, i));
|
|
75
|
+
return !o?.lesson || !o.course || o.lesson.id !== i || o.lesson.courseId !== r || o.course.id !== r ? null : o;
|
|
76
|
+
}
|
|
77
|
+
function m(e, n, i, a = t()) {
|
|
78
|
+
let o = p(e, n, i, a);
|
|
79
|
+
!o || o.lesson.completedByMe || r(a, c(e, n, i), {
|
|
80
|
+
...o,
|
|
81
|
+
lesson: {
|
|
82
|
+
...o.lesson,
|
|
83
|
+
completedByMe: !0
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
function h() {
|
|
88
|
+
try {
|
|
89
|
+
return typeof window < "u" && window.localStorage ? window.localStorage : null;
|
|
90
|
+
} catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function g(t, n = h()) {
|
|
95
|
+
if (!n || !t) return 0;
|
|
96
|
+
let r = [];
|
|
97
|
+
try {
|
|
98
|
+
for (let i = 0; i < n.length; i++) {
|
|
99
|
+
let a = n.key(i);
|
|
100
|
+
if (!a || !a.startsWith(`${e}:`)) continue;
|
|
101
|
+
let [, o, , s] = a.split(":");
|
|
102
|
+
o !== "lesson" && o !== "index" || s !== t && r.push(a);
|
|
103
|
+
}
|
|
104
|
+
} catch {
|
|
105
|
+
return 0;
|
|
106
|
+
}
|
|
107
|
+
for (let e of r) i(n, e);
|
|
108
|
+
return r.length;
|
|
109
|
+
}
|
|
110
|
+
function _(e, n, r = t()) {
|
|
111
|
+
if (!e || !n) return [];
|
|
112
|
+
let i = `${n}:`;
|
|
113
|
+
return l(r, e).filter((e) => e.startsWith(i)).map((e) => e.slice(i.length));
|
|
114
|
+
}
|
|
115
|
+
function v(e, n, r = t()) {
|
|
116
|
+
let i = _(e, n, r).map((t) => p(e, n, t, r)).filter((e) => e !== null);
|
|
117
|
+
if (i.length === 0) return null;
|
|
118
|
+
let a = i[i.length - 1];
|
|
119
|
+
return {
|
|
120
|
+
course: a.course,
|
|
121
|
+
lessons: i.map((e) => e.lesson).sort((e, t) => e.position - t.position),
|
|
122
|
+
enrollment: a.enrollment,
|
|
123
|
+
savedAt: a.savedAt
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function y(e, n, a = t()) {
|
|
127
|
+
if (!e || !n) return;
|
|
128
|
+
let s = `${n}:`, u = l(a, e);
|
|
129
|
+
for (let t of u) t.startsWith(s) && i(a, c(e, n, t.slice(s.length)));
|
|
130
|
+
r(a, o(e), u.filter((e) => !e.startsWith(s)));
|
|
131
|
+
}
|
|
132
|
+
function b(e, r = t()) {
|
|
133
|
+
if (!e) return [];
|
|
134
|
+
let i = n(r, s(e));
|
|
135
|
+
return Array.isArray(i) ? i.filter((e) => typeof e == "object" && !!e && typeof e.lessonId == "string" && typeof e.courseId == "string") : [];
|
|
136
|
+
}
|
|
137
|
+
function x(e, n, i, a = t(), o = /* @__PURE__ */ new Date()) {
|
|
138
|
+
if (!e || !n || !i) return !1;
|
|
139
|
+
let c = b(e, a);
|
|
140
|
+
return c.some((e) => e.lessonId === i) ? !0 : (c.push({
|
|
141
|
+
lessonId: i,
|
|
142
|
+
courseId: n,
|
|
143
|
+
queuedAt: o.toISOString()
|
|
144
|
+
}), r(a, s(e), c));
|
|
145
|
+
}
|
|
146
|
+
async function S(e, n, a = t()) {
|
|
147
|
+
let o = b(e, a), c = /* @__PURE__ */ new Set(), l = 0, u = 0;
|
|
148
|
+
for (let e of o) {
|
|
149
|
+
let t;
|
|
150
|
+
try {
|
|
151
|
+
t = await n(e);
|
|
152
|
+
} catch {
|
|
153
|
+
t = "retry";
|
|
154
|
+
}
|
|
155
|
+
if (t === "retry") break;
|
|
156
|
+
c.add(e.lessonId), t === "sent" ? l += 1 : u += 1;
|
|
157
|
+
}
|
|
158
|
+
let d = b(e, a).filter((e) => !c.has(e.lessonId));
|
|
159
|
+
return d.length === 0 ? i(a, s(e)) : r(a, s(e), d), {
|
|
160
|
+
sent: l,
|
|
161
|
+
rejected: u,
|
|
162
|
+
remaining: d.length
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
//#endregion
|
|
166
|
+
export { S as flushCompletions, u as isDeviceOffline, m as markSnapshotCompleted, a as offlineScope, b as pendingCompletions, g as purgeOtherUsersLessons, x as queueLessonCompletion, p as readLessonSnapshot, v as readSavedCourseOutline, y as removeSavedCourse, f as saveLessonSnapshot, _ as savedLessonIds };
|
|
167
|
+
|
|
168
|
+
//# sourceMappingURL=offlineLessons.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"offlineLessons.js","names":[],"sources":["../../../src/technospam/lib/offlineLessons.ts"],"sourcesContent":["/**\n * Offline lesson reading and progress sync (BOFF-4418).\n *\n * The TechnoSpam mobile app ships the web bundle inside the native shell, so\n * the app itself opens without a connection; what it lacks offline is lesson\n * data. This module keeps that data on the device:\n *\n * - Lesson snapshots: every lesson a learner opens (and every lesson of a\n * course they choose to save for offline) is stored with its course\n * outline and the learner's enrollment progress, so the lesson player can\n * render it again without the network. Only unlocked lessons are kept —\n * a locked lesson has no content to read and must never be cached.\n * - A progress outbox: a lesson marked complete while offline is queued and\n * sent to `tecCompleteLesson` once the device is back online. The server\n * call is idempotent, so a replay after a partial flush is harmless.\n *\n * Storage is `localStorage`, scoped by workspace AND signed-in user (see\n * {@link offlineScope}), so on a shared device one learner's queued progress\n * is never sent under another learner's session and one learner's saved\n * (possibly premium) lessons are never shown to another. When a different\n * user opens the app, {@link purgeOtherUsersLessons} removes the previous\n * user's saved lessons. Lessons are text (markdown,\n * code, quiz prompts), so a capped LRU of snapshots stays far below the quota;\n * video files are never cached and need a connection. Every storage access is\n * guarded: private browsing or a full quota degrades to \"nothing saved\",\n * never to an error in the lesson player.\n */\nimport type { LessonPlayerData, TecCourse, TecLesson } from '../api/technospamApi';\n\nconst PREFIX = 'technospam.offline.v1';\n/** Most lesson snapshots kept per scope (workspace + user); the least recently saved are evicted first. */\nexport const MAX_SAVED_LESSONS = 150;\n\nexport interface SnapshotEnrollment {\n id: string;\n progressPct: number;\n}\n\nexport interface LessonSnapshot {\n savedAt: string;\n lesson: TecLesson;\n course: TecCourse;\n enrollment: SnapshotEnrollment | null;\n}\n\nexport interface QueuedCompletion {\n lessonId: string;\n courseId: string;\n queuedAt: string;\n}\n\n/** The subset of the Web Storage API this module needs (injectable for tests). */\nexport type KeyValueStorage = Pick<Storage, 'getItem' | 'setItem' | 'removeItem'>;\n\nfunction defaultStorage(): KeyValueStorage | null {\n try {\n return typeof window !== 'undefined' && window.localStorage ? window.localStorage : null;\n } catch {\n return null;\n }\n}\n\nfunction readJson<T>(storage: KeyValueStorage | null, key: string): T | null {\n if (!storage) return null;\n try {\n const raw = storage.getItem(key);\n return raw ? (JSON.parse(raw) as T) : null;\n } catch {\n return null;\n }\n}\n\nfunction writeJson(storage: KeyValueStorage | null, key: string, value: unknown): boolean {\n if (!storage) return false;\n try {\n storage.setItem(key, JSON.stringify(value));\n return true;\n } catch {\n return false;\n }\n}\n\nfunction removeKey(storage: KeyValueStorage | null, key: string): void {\n if (!storage) return;\n try {\n storage.removeItem(key);\n } catch {\n /* storage unavailable: nothing to remove */\n }\n}\n\n/**\n * Storage scope for one learner in one workspace. Empty (nothing is read or\n * stored) until both are known, so data is never written under a partial key.\n */\nexport function offlineScope(\n workspaceId: string | null | undefined,\n userId: string | null | undefined\n): string {\n return workspaceId && userId ? `${workspaceId}:${userId}` : '';\n}\n\nconst indexKey = (scope: string): string => `${PREFIX}:index:${scope}`;\nconst outboxKey = (scope: string): string => `${PREFIX}:outbox:${scope}`;\nconst snapshotKey = (scope: string, courseId: string, lessonId: string): string =>\n `${PREFIX}:lesson:${scope}:${courseId}:${lessonId}`;\n\n/** Snapshot index entry: `${courseId}:${lessonId}`, most recently saved last. */\nfunction readIndex(storage: KeyValueStorage | null, scope: string): string[] {\n const index = readJson<unknown>(storage, indexKey(scope));\n return Array.isArray(index)\n ? index.filter((entry): entry is string => typeof entry === 'string')\n : [];\n}\n\n/** True when the browser reports no network connection. */\nexport function isDeviceOffline(): boolean {\n return typeof navigator !== 'undefined' && navigator.onLine === false;\n}\n\n/** Whether a lesson payload can be kept for offline reading. */\nexport function isSnapshotable(data: LessonPlayerData | undefined | null): data is {\n tecLesson: TecLesson;\n tecCourse: TecCourse;\n} {\n const lesson = data?.tecLesson;\n const course = data?.tecCourse;\n return !!lesson && !!course && !lesson.isLocked && lesson.courseId === course.id;\n}\n\n/**\n * Store (or refresh) one lesson snapshot. A signed download URL expires, so\n * the video file reference is dropped: an offline video cannot play anyway.\n * Returns false when nothing was stored (locked lesson or storage refused).\n */\nexport function saveLessonSnapshot(\n scope: string,\n data: LessonPlayerData,\n enrollment: SnapshotEnrollment | null,\n storage: KeyValueStorage | null = defaultStorage(),\n now: Date = new Date()\n): boolean {\n if (!scope || !isSnapshotable(data)) return false;\n const { tecLesson: lesson, tecCourse: course } = data;\n const snapshot: LessonSnapshot = {\n savedAt: now.toISOString(),\n lesson: { ...lesson, file: null },\n course,\n enrollment: enrollment ? { id: enrollment.id, progressPct: enrollment.progressPct } : null,\n };\n if (!writeJson(storage, snapshotKey(scope, course.id, lesson.id), snapshot)) return false;\n\n const entry = `${course.id}:${lesson.id}`;\n const index = readIndex(storage, scope).filter((e) => e !== entry);\n index.push(entry);\n while (index.length > MAX_SAVED_LESSONS) {\n const evicted = index.shift();\n if (evicted) {\n const [evictedCourse = '', evictedLesson = ''] = evicted.split(':');\n removeKey(storage, snapshotKey(scope, evictedCourse, evictedLesson));\n }\n }\n writeJson(storage, indexKey(scope), index);\n return true;\n}\n\n/** Read one lesson snapshot, or null when it was never saved. */\nexport function readLessonSnapshot(\n scope: string,\n courseId: string,\n lessonId: string,\n storage: KeyValueStorage | null = defaultStorage()\n): LessonSnapshot | null {\n if (!scope || !courseId || !lessonId) return null;\n const snapshot = readJson<LessonSnapshot>(storage, snapshotKey(scope, courseId, lessonId));\n if (!snapshot?.lesson || !snapshot.course) return null;\n if (snapshot.lesson.id !== lessonId || snapshot.lesson.courseId !== courseId) return null;\n if (snapshot.course.id !== courseId) return null;\n return snapshot;\n}\n\n/**\n * Mark a saved lesson as completed by the learner, so reopening it offline\n * shows it as done even before the queued completion reaches the server.\n */\nexport function markSnapshotCompleted(\n scope: string,\n courseId: string,\n lessonId: string,\n storage: KeyValueStorage | null = defaultStorage()\n): void {\n const snapshot = readLessonSnapshot(scope, courseId, lessonId, storage);\n if (!snapshot || snapshot.lesson.completedByMe) return;\n writeJson(storage, snapshotKey(scope, courseId, lessonId), {\n ...snapshot,\n lesson: { ...snapshot.lesson, completedByMe: true },\n });\n}\n\n/** The parts of the Web Storage API needed to enumerate keys. */\nexport type EnumerableStorage = KeyValueStorage & Pick<Storage, 'key' | 'length'>;\n\nfunction defaultEnumerableStorage(): EnumerableStorage | null {\n try {\n return typeof window !== 'undefined' && window.localStorage ? window.localStorage : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Remove every saved lesson (snapshots and their index) that belongs to a user\n * other than `userId`. Queued completions are kept: they hold only lesson ids,\n * are scoped to their own user and sync when that user signs in again.\n * Returns how many keys were removed.\n */\nexport function purgeOtherUsersLessons(\n userId: string,\n storage: EnumerableStorage | null = defaultEnumerableStorage()\n): number {\n if (!storage || !userId) return 0;\n const doomed: string[] = [];\n try {\n for (let i = 0; i < storage.length; i++) {\n const key = storage.key(i);\n if (!key || !key.startsWith(`${PREFIX}:`)) continue;\n // `${PREFIX}:${kind}:${workspaceId}:${userId}...`\n const [, kind, , owner] = key.split(':');\n if (kind !== 'lesson' && kind !== 'index') continue;\n if (owner !== userId) doomed.push(key);\n }\n } catch {\n return 0;\n }\n for (const key of doomed) removeKey(storage, key);\n return doomed.length;\n}\n\n/** Ids of the lessons of one course saved on this device. */\nexport function savedLessonIds(\n scope: string,\n courseId: string,\n storage: KeyValueStorage | null = defaultStorage()\n): string[] {\n if (!scope || !courseId) return [];\n const prefix = `${courseId}:`;\n return readIndex(storage, scope)\n .filter((entry) => entry.startsWith(prefix))\n .map((entry) => entry.slice(prefix.length));\n}\n\n/** A course rebuilt from the lessons saved on this device. */\nexport interface SavedCourseOutline {\n course: TecCourse;\n /** Saved lessons of the course, in curriculum order. */\n lessons: TecLesson[];\n /** Enrollment progress as of the most recently saved lesson, if any. */\n enrollment: SnapshotEnrollment | null;\n savedAt: string;\n}\n\n/**\n * Rebuild a course outline from its saved lesson snapshots, so the course\n * page can list the saved lessons when it cannot reach the server (a cold\n * start offline has no Apollo cache). The course header comes from the most\n * recently saved snapshot. Returns null when no lesson of the course is saved.\n */\nexport function readSavedCourseOutline(\n scope: string,\n courseId: string,\n storage: KeyValueStorage | null = defaultStorage()\n): SavedCourseOutline | null {\n const snapshots = savedLessonIds(scope, courseId, storage)\n .map((lessonId) => readLessonSnapshot(scope, courseId, lessonId, storage))\n .filter((snapshot): snapshot is LessonSnapshot => snapshot !== null);\n if (snapshots.length === 0) return null;\n // The index lists the most recently saved entry last.\n const latest = snapshots[snapshots.length - 1] as LessonSnapshot;\n return {\n course: latest.course,\n lessons: snapshots.map((s) => s.lesson).sort((a, b) => a.position - b.position),\n enrollment: latest.enrollment,\n savedAt: latest.savedAt,\n };\n}\n\n/** Remove every saved lesson of one course from this device. */\nexport function removeSavedCourse(\n scope: string,\n courseId: string,\n storage: KeyValueStorage | null = defaultStorage()\n): void {\n if (!scope || !courseId) return;\n const prefix = `${courseId}:`;\n const index = readIndex(storage, scope);\n for (const entry of index) {\n if (entry.startsWith(prefix))\n removeKey(storage, snapshotKey(scope, courseId, entry.slice(prefix.length)));\n }\n writeJson(\n storage,\n indexKey(scope),\n index.filter((entry) => !entry.startsWith(prefix))\n );\n}\n\n/** Completions waiting to be sent. */\nexport function pendingCompletions(\n scope: string,\n storage: KeyValueStorage | null = defaultStorage()\n): QueuedCompletion[] {\n if (!scope) return [];\n const queue = readJson<unknown>(storage, outboxKey(scope));\n if (!Array.isArray(queue)) return [];\n return queue.filter(\n (item): item is QueuedCompletion =>\n typeof item === 'object' &&\n item !== null &&\n typeof (item as QueuedCompletion).lessonId === 'string' &&\n typeof (item as QueuedCompletion).courseId === 'string'\n );\n}\n\n/** Queue a lesson completion for the next sync. Queuing the same lesson twice is a no-op. */\nexport function queueLessonCompletion(\n scope: string,\n courseId: string,\n lessonId: string,\n storage: KeyValueStorage | null = defaultStorage(),\n now: Date = new Date()\n): boolean {\n if (!scope || !courseId || !lessonId) return false;\n const queue = pendingCompletions(scope, storage);\n if (queue.some((item) => item.lessonId === lessonId)) return true;\n queue.push({ lessonId, courseId, queuedAt: now.toISOString() });\n return writeJson(storage, outboxKey(scope), queue);\n}\n\n/**\n * Outcome of sending one queued completion: `sent` and `rejected` (the server\n * refused it for good, e.g. the lesson was removed) both leave the queue;\n * `retry` (no connection, gateway error) stays for the next sync.\n */\nexport type CompletionSendOutcome = 'sent' | 'rejected' | 'retry';\n\nexport interface FlushResult {\n sent: number;\n rejected: number;\n remaining: number;\n}\n\n/**\n * Send queued completions in order. Stops at the first `retry`, since that\n * means the connection is not usable yet, and keeps it and everything after\n * it queued.\n */\nexport async function flushCompletions(\n scope: string,\n send: (item: QueuedCompletion) => Promise<CompletionSendOutcome>,\n storage: KeyValueStorage | null = defaultStorage()\n): Promise<FlushResult> {\n const queue = pendingCompletions(scope, storage);\n const settled = new Set<string>();\n let sent = 0;\n let rejected = 0;\n for (const item of queue) {\n let outcome: CompletionSendOutcome;\n try {\n outcome = await send(item);\n } catch {\n outcome = 'retry';\n }\n if (outcome === 'retry') break;\n settled.add(item.lessonId);\n if (outcome === 'sent') sent += 1;\n else rejected += 1;\n }\n // Re-read the outbox: a completion queued while a request was in flight must\n // survive. Only the entries this flush settled are removed.\n const remaining = pendingCompletions(scope, storage).filter(\n (item) => !settled.has(item.lessonId)\n );\n if (remaining.length === 0) removeKey(storage, outboxKey(scope));\n else writeJson(storage, outboxKey(scope), remaining);\n return { sent, rejected, remaining: remaining.length };\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CoursesCourseDetailsPagePage.d.ts","sourceRoot":"","sources":["../../../src/technospam/pages/CoursesCourseDetailsPagePage.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"CoursesCourseDetailsPagePage.d.ts","sourceRoot":"","sources":["../../../src/technospam/pages/CoursesCourseDetailsPagePage.tsx"],"names":[],"mappings":"AAoSA,wBAAgB,4BAA4B,gCA4V3C"}
|