musora-content-services 2.107.5 → 2.107.7
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/CHANGELOG.md +14 -0
- package/package.json +1 -1
- package/src/index.d.ts +2 -6
- package/src/index.js +2 -6
- package/src/services/content-org/learning-paths.ts +5 -5
- package/src/services/contentProgress.js +67 -34
- package/src/services/progress-events.js +0 -52
- package/src/services/sync/manager.ts +0 -4
- package/src/services/sync/repositories/base.ts +12 -8
- package/src/services/sync/repositories/content-progress.ts +11 -7
- package/src/services/sync/store/index.ts +13 -9
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
### [2.107.7](https://github.com/railroadmedia/musora-content-services/compare/v2.107.6...v2.107.7) (2025-12-29)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* daniel merged bad ([#675](https://github.com/railroadmedia/musora-content-services/issues/675)) ([1c5863b](https://github.com/railroadmedia/musora-content-services/commit/1c5863bc921967fe16875367d9589c8dcf4e0ac3))
|
|
11
|
+
|
|
12
|
+
### [2.107.6](https://github.com/railroadmedia/musora-content-services/compare/v2.107.5...v2.107.6) (2025-12-29)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* **TP-1051:** group contentProgress upsert pushes ([#665](https://github.com/railroadmedia/musora-content-services/issues/665)) ([27ff11f](https://github.com/railroadmedia/musora-content-services/commit/27ff11fb1b10075313e614cf895356a221f0c0d1))
|
|
18
|
+
|
|
5
19
|
### [2.107.5](https://github.com/railroadmedia/musora-content-services/compare/v2.107.4...v2.107.5) (2025-12-29)
|
|
6
20
|
|
|
7
21
|
|
package/package.json
CHANGED
package/src/index.d.ts
CHANGED
|
@@ -54,7 +54,7 @@ import {
|
|
|
54
54
|
getEnrichedLearningPaths,
|
|
55
55
|
getLearningPathLessonsByIds,
|
|
56
56
|
mapContentToParent,
|
|
57
|
-
|
|
57
|
+
onContentCompletedLearningPathActions,
|
|
58
58
|
resetAllLearningPaths,
|
|
59
59
|
startLearningPath,
|
|
60
60
|
updateDailySession
|
|
@@ -202,9 +202,7 @@ import {
|
|
|
202
202
|
} from './services/liveTesting.ts';
|
|
203
203
|
|
|
204
204
|
import {
|
|
205
|
-
emitContentCompleted,
|
|
206
205
|
emitProgressSaved,
|
|
207
|
-
onContentCompleted,
|
|
208
206
|
onProgressSaved
|
|
209
207
|
} from './services/progress-events.js';
|
|
210
208
|
|
|
@@ -484,7 +482,6 @@ declare module 'musora-content-services' {
|
|
|
484
482
|
deleteUserActivity,
|
|
485
483
|
duplicatePlaylist,
|
|
486
484
|
editComment,
|
|
487
|
-
emitContentCompleted,
|
|
488
485
|
emitProgressSaved,
|
|
489
486
|
enrollUserInGuidedCourse,
|
|
490
487
|
extractSanityUrl,
|
|
@@ -675,8 +672,7 @@ declare module 'musora-content-services' {
|
|
|
675
672
|
markNotificationAsUnread,
|
|
676
673
|
markThreadAsRead,
|
|
677
674
|
numberOfActiveUsers,
|
|
678
|
-
|
|
679
|
-
onContentCompletedLearningPathListener,
|
|
675
|
+
onContentCompletedLearningPathActions,
|
|
680
676
|
onProgressSaved,
|
|
681
677
|
openComment,
|
|
682
678
|
otherStats,
|
package/src/index.js
CHANGED
|
@@ -58,7 +58,7 @@ import {
|
|
|
58
58
|
getEnrichedLearningPaths,
|
|
59
59
|
getLearningPathLessonsByIds,
|
|
60
60
|
mapContentToParent,
|
|
61
|
-
|
|
61
|
+
onContentCompletedLearningPathActions,
|
|
62
62
|
resetAllLearningPaths,
|
|
63
63
|
startLearningPath,
|
|
64
64
|
updateDailySession
|
|
@@ -206,9 +206,7 @@ import {
|
|
|
206
206
|
} from './services/liveTesting.ts';
|
|
207
207
|
|
|
208
208
|
import {
|
|
209
|
-
emitContentCompleted,
|
|
210
209
|
emitProgressSaved,
|
|
211
|
-
onContentCompleted,
|
|
212
210
|
onProgressSaved
|
|
213
211
|
} from './services/progress-events.js';
|
|
214
212
|
|
|
@@ -483,7 +481,6 @@ export {
|
|
|
483
481
|
deleteUserActivity,
|
|
484
482
|
duplicatePlaylist,
|
|
485
483
|
editComment,
|
|
486
|
-
emitContentCompleted,
|
|
487
484
|
emitProgressSaved,
|
|
488
485
|
enrollUserInGuidedCourse,
|
|
489
486
|
extractSanityUrl,
|
|
@@ -674,8 +671,7 @@ export {
|
|
|
674
671
|
markNotificationAsUnread,
|
|
675
672
|
markThreadAsRead,
|
|
676
673
|
numberOfActiveUsers,
|
|
677
|
-
|
|
678
|
-
onContentCompletedLearningPathListener,
|
|
674
|
+
onContentCompletedLearningPathActions,
|
|
679
675
|
onProgressSaved,
|
|
680
676
|
openComment,
|
|
681
677
|
otherStats,
|
|
@@ -445,11 +445,11 @@ async function resetIfPossible(contentId: number, collection: CollectionParamete
|
|
|
445
445
|
return status !== '' ? await contentStatusReset(contentId, collection) : null
|
|
446
446
|
}
|
|
447
447
|
|
|
448
|
-
export async function
|
|
449
|
-
if (
|
|
450
|
-
if (
|
|
448
|
+
export async function onContentCompletedLearningPathActions(contentId: number, collection: CollectionObject|null) {
|
|
449
|
+
if (collection?.type !== COLLECTION_TYPE.LEARNING_PATH) return
|
|
450
|
+
if (contentId !== collection?.id) return
|
|
451
451
|
|
|
452
|
-
const learningPathId =
|
|
452
|
+
const learningPathId = contentId
|
|
453
453
|
const learningPath = await getEnrichedLearningPath(learningPathId)
|
|
454
454
|
|
|
455
455
|
const brand = learningPath.brand
|
|
@@ -470,5 +470,5 @@ export async function onContentCompletedLearningPathListener(event) {
|
|
|
470
470
|
await startLearningPath(brand, nextLearningPath.id)
|
|
471
471
|
const nextLearningPathData = await getEnrichedLearningPath(nextLearningPath.id)
|
|
472
472
|
|
|
473
|
-
await contentStatusReset(nextLearningPathData.intro_video.id)
|
|
473
|
+
await contentStatusReset(nextLearningPathData.intro_video.id, {skipPush: true})
|
|
474
474
|
}
|
|
@@ -3,8 +3,7 @@ import { db } from './sync'
|
|
|
3
3
|
import { COLLECTION_TYPE, STATE } from './sync/models/ContentProgress'
|
|
4
4
|
import { trackUserPractice, findIncompleteLesson } from './userActivity'
|
|
5
5
|
import { getNextLessonLessonParentTypes } from '../contentTypeConfig.js'
|
|
6
|
-
import {
|
|
7
|
-
import {getDailySession} from "./content-org/learning-paths.ts";
|
|
6
|
+
import {getDailySession, onContentCompletedLearningPathActions} from "./content-org/learning-paths.ts";
|
|
8
7
|
import {getToday} from "./dateUtils.js";
|
|
9
8
|
import { fetchBrandsByContentIds } from './sanity.js'
|
|
10
9
|
|
|
@@ -464,11 +463,12 @@ export async function contentStatusStarted(contentId, collection = null) {
|
|
|
464
463
|
false
|
|
465
464
|
)
|
|
466
465
|
}
|
|
467
|
-
export async function contentStatusReset(contentId, collection = null) {
|
|
468
|
-
return resetStatus(contentId, collection)
|
|
466
|
+
export async function contentStatusReset(contentId, collection = null, {skipPush = false} = {}) {
|
|
467
|
+
return resetStatus(contentId, collection, {skipPush})
|
|
469
468
|
}
|
|
470
469
|
|
|
471
|
-
async function saveContentProgress(contentId, collection, progress, currentSeconds) {
|
|
470
|
+
async function saveContentProgress(contentId, collection, progress, currentSeconds, {skipPush = false} = {}) {
|
|
471
|
+
const isLP = collection?.type === COLLECTION_TYPE.LEARNING_PATH
|
|
472
472
|
|
|
473
473
|
// filter out contentIds that are setting progress lower than existing
|
|
474
474
|
const contentIdProgress = await getProgressDataByIds([contentId], collection)
|
|
@@ -480,10 +480,9 @@ async function saveContentProgress(contentId, collection, progress, currentSecon
|
|
|
480
480
|
contentId,
|
|
481
481
|
collection,
|
|
482
482
|
progress,
|
|
483
|
-
currentSeconds
|
|
483
|
+
currentSeconds,
|
|
484
|
+
{skipPush: true}
|
|
484
485
|
)
|
|
485
|
-
if (progress === 100) emitContentCompleted(contentId, collection)
|
|
486
|
-
|
|
487
486
|
// note - previous implementation explicitly did not trickle progress to children here
|
|
488
487
|
// (only to siblings/parents via le bubbles)
|
|
489
488
|
|
|
@@ -500,27 +499,32 @@ async function saveContentProgress(contentId, collection, progress, currentSecon
|
|
|
500
499
|
}
|
|
501
500
|
|
|
502
501
|
// BE bubbling/trickling currently does not work, so we utilize non-tentative pushing when learning path collection
|
|
503
|
-
await db.contentProgress.recordProgressMany(bubbledProgresses, collection,
|
|
502
|
+
await db.contentProgress.recordProgressMany(bubbledProgresses, collection, {tentative: !isLP, skipPush: true})
|
|
504
503
|
|
|
505
|
-
if (
|
|
504
|
+
if (isLP) {
|
|
506
505
|
let exportIds = bubbledProgresses
|
|
507
506
|
exportIds[contentId] = progress
|
|
508
|
-
await duplicateLearningPathProgressToExternalContents(exportIds, collection, hierarchy)
|
|
507
|
+
await duplicateLearningPathProgressToExternalContents(exportIds, collection, hierarchy, {skipPush: true})
|
|
509
508
|
}
|
|
510
509
|
|
|
510
|
+
if (progress === 100) await onContentCompletedLearningPathActions(contentId, collection)
|
|
511
|
+
|
|
511
512
|
for (const [bubbledContentId, bubbledProgress] of Object.entries(bubbledProgresses)) {
|
|
512
513
|
if (bubbledProgress === 100) {
|
|
513
|
-
|
|
514
|
+
await onContentCompletedLearningPathActions(Number(bubbledContentId), collection)
|
|
514
515
|
}
|
|
515
516
|
}
|
|
517
|
+
|
|
518
|
+
if (!skipPush) db.contentProgress.requestPushUnsynced()
|
|
519
|
+
|
|
516
520
|
return response
|
|
517
521
|
}
|
|
518
522
|
|
|
519
|
-
async function setStartedOrCompletedStatus(contentId, collection, isCompleted) {
|
|
520
|
-
const
|
|
521
|
-
const response = await db.contentProgress.recordProgress(contentId, collection, progress)
|
|
523
|
+
async function setStartedOrCompletedStatus(contentId, collection, isCompleted, {skipPush = false} = {}) {
|
|
524
|
+
const isLP = collection?.type === COLLECTION_TYPE.LEARNING_PATH
|
|
522
525
|
|
|
523
|
-
|
|
526
|
+
const progress = isCompleted ? 100 : 0
|
|
527
|
+
const response = await db.contentProgress.recordProgress(contentId, collection, progress, null, {skipPush: true})
|
|
524
528
|
|
|
525
529
|
const hierarchy = await getHierarchy(contentId, collection)
|
|
526
530
|
|
|
@@ -529,29 +533,33 @@ async function setStartedOrCompletedStatus(contentId, collection, isCompleted) {
|
|
|
529
533
|
...await bubbleProgress(hierarchy, contentId, collection)
|
|
530
534
|
}
|
|
531
535
|
// BE bubbling/trickling currently does not work, so we utilize non-tentative pushing when learning path collection
|
|
532
|
-
await db.contentProgress.recordProgressMany(progresses, collection,
|
|
536
|
+
await db.contentProgress.recordProgressMany(progresses, collection, {tentative: !isLP, skipPush: true})
|
|
537
|
+
if (isLP) {
|
|
533
538
|
|
|
534
|
-
if (collection && collection.type === COLLECTION_TYPE.LEARNING_PATH) {
|
|
535
539
|
let exportProgresses = progresses
|
|
536
540
|
exportProgresses[contentId] = progress
|
|
537
|
-
await duplicateLearningPathProgressToExternalContents(exportProgresses, collection, hierarchy)
|
|
541
|
+
await duplicateLearningPathProgressToExternalContents(exportProgresses, collection, hierarchy, {skipPush: true})
|
|
538
542
|
}
|
|
539
543
|
|
|
544
|
+
if (progress === 100) await onContentCompletedLearningPathActions(contentId, collection)
|
|
545
|
+
|
|
540
546
|
for (const [id, progress] of Object.entries(progresses)) {
|
|
541
547
|
if (progress === 100) {
|
|
542
|
-
|
|
548
|
+
await onContentCompletedLearningPathActions(Number(id), collection)
|
|
543
549
|
}
|
|
544
550
|
}
|
|
545
551
|
|
|
552
|
+
if (!skipPush) db.contentProgress.requestPushUnsynced()
|
|
553
|
+
|
|
546
554
|
return response
|
|
547
555
|
}
|
|
548
556
|
|
|
549
557
|
// we cannot simply pass LP id with self collection, because we do not have a-la-carte LP's set up yet,
|
|
550
558
|
// and we need each lesson to bubble to its parent outside of LP
|
|
551
|
-
async function duplicateLearningPathProgressToExternalContents(ids, collection, hierarchy) {
|
|
559
|
+
async function duplicateLearningPathProgressToExternalContents(ids, collection, hierarchy, {skipPush = false} = {}) {
|
|
552
560
|
// filter out LPs. we dont want to duplicate to LP's while we dont have a-la-cart LP's set up.
|
|
553
561
|
let filteredIds = Object.fromEntries(
|
|
554
|
-
Object.entries(ids).filter((id) => {
|
|
562
|
+
Object.entries(ids).filter(([id]) => {
|
|
555
563
|
return hierarchy.parents[parseInt(id)] !== null
|
|
556
564
|
})
|
|
557
565
|
)
|
|
@@ -567,8 +575,13 @@ async function duplicateLearningPathProgressToExternalContents(ids, collection,
|
|
|
567
575
|
})
|
|
568
576
|
|
|
569
577
|
// each handles its own bubbling.
|
|
570
|
-
|
|
571
|
-
|
|
578
|
+
// skipPush on all but last to avoid multiple push requests
|
|
579
|
+
filteredIds.forEach(([id, pct], index) => {
|
|
580
|
+
if (index === filteredIds.length - 1) {
|
|
581
|
+
saveContentProgress(parseInt(id), null, pct, null, {skipPush})
|
|
582
|
+
} else {
|
|
583
|
+
saveContentProgress(parseInt(id), null, pct, null, {skipPush: true})
|
|
584
|
+
}
|
|
572
585
|
})
|
|
573
586
|
}
|
|
574
587
|
|
|
@@ -580,10 +593,18 @@ async function getHierarchy(contentId, collection) {
|
|
|
580
593
|
}
|
|
581
594
|
}
|
|
582
595
|
|
|
583
|
-
async function setStartedOrCompletedStatusMany(contentIds, collection, isCompleted) {
|
|
596
|
+
async function setStartedOrCompletedStatusMany(contentIds, collection, isCompleted, {skipPush = false} = {}) {
|
|
597
|
+
const isLP = collection?.type === COLLECTION_TYPE.LEARNING_PATH
|
|
584
598
|
const progress = isCompleted ? 100 : 0
|
|
599
|
+
|
|
600
|
+
if (progress === 100) {
|
|
601
|
+
for (const contentId of contentIds) {
|
|
602
|
+
await onContentCompletedLearningPathActions(contentId, collection)
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
585
606
|
const contents = Object.fromEntries(contentIds.map((id) => [id, progress]))
|
|
586
|
-
const response = await db.contentProgress.recordProgressMany(contents, collection, true)
|
|
607
|
+
const response = await db.contentProgress.recordProgressMany(contents, collection, {tentative: !isLP, skipPush: true})
|
|
587
608
|
|
|
588
609
|
// we assume this is used only for contents within the same hierarchy
|
|
589
610
|
const hierarchy = await getHierarchy(collection.id, collection)
|
|
@@ -597,22 +618,32 @@ async function setStartedOrCompletedStatusMany(contentIds, collection, isComplet
|
|
|
597
618
|
}
|
|
598
619
|
}
|
|
599
620
|
// BE bubbling/trickling currently does not work, so we utilize non-tentative pushing when learning path collection
|
|
600
|
-
await db.contentProgress.recordProgressMany(progresses, collection,
|
|
621
|
+
await db.contentProgress.recordProgressMany(progresses, collection, {tentative: !isLP, skipPush: true})
|
|
601
622
|
|
|
602
|
-
if (
|
|
623
|
+
if (isLP) {
|
|
603
624
|
let exportProgresses = progresses
|
|
604
625
|
for (const contentId of contentIds){
|
|
605
626
|
exportProgresses[contentId] = progress
|
|
606
627
|
}
|
|
607
|
-
await duplicateLearningPathProgressToExternalContents(exportProgresses, collection, hierarchy)
|
|
628
|
+
await duplicateLearningPathProgressToExternalContents(exportProgresses, collection, hierarchy, {skipPush: true})
|
|
608
629
|
}
|
|
609
630
|
|
|
631
|
+
for (const [id, progress] of Object.entries(progresses)) {
|
|
632
|
+
if (progress === 100) {
|
|
633
|
+
await onContentCompletedLearningPathActions(Number(id), collection)
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
if (!skipPush) db.contentProgress.requestPushUnsynced()
|
|
638
|
+
|
|
610
639
|
return response
|
|
611
640
|
}
|
|
612
641
|
|
|
613
|
-
async function resetStatus(contentId, collection = null) {
|
|
642
|
+
async function resetStatus(contentId, collection = null, {skipPush = false} = {}) {
|
|
643
|
+
const isLP = collection?.type === COLLECTION_TYPE.LEARNING_PATH
|
|
644
|
+
|
|
614
645
|
const progress = 0
|
|
615
|
-
const response = await db.contentProgress.eraseProgress(contentId, collection)
|
|
646
|
+
const response = await db.contentProgress.eraseProgress(contentId, collection, {skipPush: true})
|
|
616
647
|
const hierarchy = await getHierarchy(contentId, collection)
|
|
617
648
|
|
|
618
649
|
let progresses = {
|
|
@@ -620,13 +651,15 @@ async function resetStatus(contentId, collection = null) {
|
|
|
620
651
|
...await bubbleProgress(hierarchy, contentId, collection)
|
|
621
652
|
}
|
|
622
653
|
// BE bubbling/trickling currently does not work, so we utilize non-tentative pushing when learning path collection
|
|
623
|
-
await db.contentProgress.recordProgressMany(progresses, collection,
|
|
654
|
+
await db.contentProgress.recordProgressMany(progresses, collection, {tentative: !isLP, skipPush: true})
|
|
624
655
|
|
|
625
|
-
if (
|
|
656
|
+
if (isLP) {
|
|
626
657
|
progresses[contentId] = progress
|
|
627
|
-
await duplicateLearningPathProgressToExternalContents(progresses, collection, hierarchy)
|
|
658
|
+
await duplicateLearningPathProgressToExternalContents(progresses, collection, hierarchy, {skipPush: true})
|
|
628
659
|
}
|
|
629
660
|
|
|
661
|
+
if (!skipPush) db.contentProgress.requestPushUnsynced()
|
|
662
|
+
|
|
630
663
|
return response
|
|
631
664
|
}
|
|
632
665
|
|
|
@@ -56,55 +56,3 @@ export function emitProgressSaved(event) {
|
|
|
56
56
|
}
|
|
57
57
|
})
|
|
58
58
|
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* @typedef {Object} ContentCompletedEvent
|
|
62
|
-
* @property {number} contentId - Railcontent ID of the completed content item
|
|
63
|
-
* @property {Object|null} collection - Collection context information
|
|
64
|
-
* @property {string} collection.type - Collection type (learning-path, guided-course, etc.)
|
|
65
|
-
* @property {number} collection.id - Collection ID
|
|
66
|
-
*/
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* @callback ContentCompletedListener
|
|
70
|
-
* @param {ContentCompletedEvent} event - The content completion event data
|
|
71
|
-
* @returns {void}
|
|
72
|
-
*/
|
|
73
|
-
const completedListeners = new Set()
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* @param {ContentCompletedListener} listener - Function called when content is completed
|
|
77
|
-
* @returns {function(): void} Cleanup function to unregister the listener
|
|
78
|
-
*
|
|
79
|
-
* @example Listen for content completion
|
|
80
|
-
* const cleanup = onContentCompleted((event) => {
|
|
81
|
-
* console.log(`Content ${event.contentId} completed!`)
|
|
82
|
-
* if (event.collection) {
|
|
83
|
-
* console.log(`Within ${event.collection.type}: ${event.collection.id}`)
|
|
84
|
-
* checkCollectionProgress(event.collection.id)
|
|
85
|
-
* }
|
|
86
|
-
* })
|
|
87
|
-
*
|
|
88
|
-
* // Later, when no longer needed:
|
|
89
|
-
* cleanup()
|
|
90
|
-
*/
|
|
91
|
-
export function onContentCompleted(listener) {
|
|
92
|
-
completedListeners.add(listener)
|
|
93
|
-
return () => completedListeners.delete(listener)
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* @param {number} contentId - The ID of the completed content item
|
|
98
|
-
* @param {Object|null} collection - Collection context information
|
|
99
|
-
* @returns {void}
|
|
100
|
-
*/
|
|
101
|
-
export function emitContentCompleted(contentId, collection) {
|
|
102
|
-
const event = { contentId: contentId, collection: collection }
|
|
103
|
-
completedListeners.forEach((listener) => {
|
|
104
|
-
try {
|
|
105
|
-
listener(event)
|
|
106
|
-
} catch (error) {
|
|
107
|
-
console.error('Error in contentCompleted listener:', error)
|
|
108
|
-
}
|
|
109
|
-
})
|
|
110
|
-
}
|
|
@@ -14,9 +14,6 @@ import { inBoundary } from './errors/boundary'
|
|
|
14
14
|
import createStoresFromConfig from './store-configs'
|
|
15
15
|
import { contentProgressObserver } from '../awards/internal/content-progress-observer'
|
|
16
16
|
|
|
17
|
-
import { onProgressSaved, onContentCompleted } from '../progress-events'
|
|
18
|
-
import { onContentCompletedLearningPathListener } from '../content-org/learning-paths'
|
|
19
|
-
|
|
20
17
|
export default class SyncManager {
|
|
21
18
|
private static counter = 0
|
|
22
19
|
private static instance: SyncManager | null = null
|
|
@@ -137,7 +134,6 @@ export default class SyncManager {
|
|
|
137
134
|
contentProgressObserver.start(this.database).catch((error) => {
|
|
138
135
|
this.telemetry.error('[SyncManager] Failed to start contentProgressObserver', error)
|
|
139
136
|
})
|
|
140
|
-
onContentCompleted(onContentCompletedLearningPathListener)
|
|
141
137
|
|
|
142
138
|
const teardown = async () => {
|
|
143
139
|
this.telemetry.debug('[SyncManager] Tearing down')
|
|
@@ -96,10 +96,10 @@ export default class SyncRepository<TModel extends BaseModel> {
|
|
|
96
96
|
)
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
protected async upsertOne(id: RecordId, builder: (record: TModel) => void) {
|
|
99
|
+
protected async upsertOne(id: RecordId, builder: (record: TModel) => void, { skipPush = false } = {}) {
|
|
100
100
|
return this.store.telemetry.trace(
|
|
101
101
|
{ name: `upsertOne:${this.store.model.table}`, op: 'upsert' },
|
|
102
|
-
(span) => this._respondToWrite(() => this.store.upsertOne(id, builder, span), span)
|
|
102
|
+
(span) => this._respondToWrite(() => this.store.upsertOne(id, builder, span, {skipPush}), span)
|
|
103
103
|
)
|
|
104
104
|
}
|
|
105
105
|
|
|
@@ -110,24 +110,24 @@ export default class SyncRepository<TModel extends BaseModel> {
|
|
|
110
110
|
)
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
protected async upsertSome(builders: Record<RecordId, (record: TModel) => void
|
|
113
|
+
protected async upsertSome(builders: Record<RecordId, (record: TModel) => void>, { skipPush = false } = {}) {
|
|
114
114
|
return this.store.telemetry.trace(
|
|
115
115
|
{ name: `upsertSome:${this.store.model.table}`, op: 'upsert' },
|
|
116
|
-
(span) => this._respondToWrite(() => this.store.upsertSome(builders, span), span)
|
|
116
|
+
(span) => this._respondToWrite(() => this.store.upsertSome(builders, span, {skipPush}), span)
|
|
117
117
|
)
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
protected async upsertSomeTentative(builders: Record<RecordId, (record: TModel) => void
|
|
120
|
+
protected async upsertSomeTentative(builders: Record<RecordId, (record: TModel) => void>, { skipPush = false } = {}) {
|
|
121
121
|
return this.store.telemetry.trace(
|
|
122
122
|
{ name: `upsertSomeTentative:${this.store.model.table}`, op: 'upsert' },
|
|
123
|
-
(span) => this._respondToWrite(() => this.store.upsertSomeTentative(builders, span), span)
|
|
123
|
+
(span) => this._respondToWrite(() => this.store.upsertSomeTentative(builders, span, {skipPush}), span)
|
|
124
124
|
)
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
protected async deleteOne(id: RecordId) {
|
|
127
|
+
protected async deleteOne(id: RecordId, { skipPush = false } = {}) {
|
|
128
128
|
return this.store.telemetry.trace(
|
|
129
129
|
{ name: `delete:${this.store.model.table}`, op: 'delete' },
|
|
130
|
-
(span) => this._respondToWriteIds(() => this.store.deleteOne(id, span), span)
|
|
130
|
+
(span) => this._respondToWriteIds(() => this.store.deleteOne(id, span, {skipPush}), span)
|
|
131
131
|
)
|
|
132
132
|
}
|
|
133
133
|
|
|
@@ -244,4 +244,8 @@ export default class SyncRepository<TModel extends BaseModel> {
|
|
|
244
244
|
}
|
|
245
245
|
return result
|
|
246
246
|
}
|
|
247
|
+
|
|
248
|
+
protected async _requestPushUnsynced() {
|
|
249
|
+
await this.store.pushUnsyncedWithRetry()
|
|
250
|
+
}
|
|
247
251
|
}
|
|
@@ -133,7 +133,7 @@ export default class ProgressRepository extends SyncRepository<ContentProgress>
|
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
recordProgress(contentId: number, collection: CollectionParameter | null, progressPct: number, resumeTime?: number) {
|
|
136
|
+
recordProgress(contentId: number, collection: CollectionParameter | null, progressPct: number, resumeTime?: number, {skipPush = false} = {}) {
|
|
137
137
|
const id = ProgressRepository.generateId(contentId, collection)
|
|
138
138
|
|
|
139
139
|
const result = this.upsertOne(id, (r) => {
|
|
@@ -146,7 +146,7 @@ export default class ProgressRepository extends SyncRepository<ContentProgress>
|
|
|
146
146
|
if (typeof resumeTime != 'undefined') {
|
|
147
147
|
r.resume_time_seconds = Math.floor(resumeTime)
|
|
148
148
|
}
|
|
149
|
-
})
|
|
149
|
+
}, { skipPush })
|
|
150
150
|
|
|
151
151
|
// Emit event AFTER database write completes
|
|
152
152
|
result.then(() => {
|
|
@@ -176,7 +176,7 @@ export default class ProgressRepository extends SyncRepository<ContentProgress>
|
|
|
176
176
|
recordProgressMany(
|
|
177
177
|
contentProgresses: Record<string, number>, // Accept plain object
|
|
178
178
|
collection: CollectionParameter | null,
|
|
179
|
-
tentative: boolean
|
|
179
|
+
{ tentative = true, skipPush = false }: { tentative?: boolean; skipPush?: boolean } = {}
|
|
180
180
|
) {
|
|
181
181
|
|
|
182
182
|
const data = Object.fromEntries(
|
|
@@ -192,14 +192,18 @@ export default class ProgressRepository extends SyncRepository<ContentProgress>
|
|
|
192
192
|
])
|
|
193
193
|
)
|
|
194
194
|
return tentative
|
|
195
|
-
? this.upsertSomeTentative(data)
|
|
196
|
-
: this.upsertSome(data)
|
|
195
|
+
? this.upsertSomeTentative(data, { skipPush })
|
|
196
|
+
: this.upsertSome(data, { skipPush })
|
|
197
197
|
|
|
198
198
|
//todo add event emitting for bulk updates?
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
-
eraseProgress(contentId: number, collection: CollectionParameter | null) {
|
|
202
|
-
return this.deleteOne(ProgressRepository.generateId(contentId, collection))
|
|
201
|
+
eraseProgress(contentId: number, collection: CollectionParameter | null, {skipPush = false} = {}) {
|
|
202
|
+
return this.deleteOne(ProgressRepository.generateId(contentId, collection), { skipPush })
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async requestPushUnsynced() {
|
|
206
|
+
await this._requestPushUnsynced()
|
|
203
207
|
}
|
|
204
208
|
|
|
205
209
|
private static generateId(
|
|
@@ -235,7 +235,7 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
|
|
|
235
235
|
})
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
-
async upsertSome(builders: Record<RecordId, (record: TModel) => void>, span?: Span) {
|
|
238
|
+
async upsertSome(builders: Record<RecordId, (record: TModel) => void>, span?: Span, { skipPush = false } = {}) {
|
|
239
239
|
if (Object.keys(builders).length === 0) return []
|
|
240
240
|
|
|
241
241
|
return await this.runScope.abortable(async () => {
|
|
@@ -298,29 +298,31 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
|
|
|
298
298
|
|
|
299
299
|
this.emit('upserted', records)
|
|
300
300
|
|
|
301
|
-
|
|
301
|
+
if (!skipPush) {
|
|
302
|
+
this.pushUnsyncedWithRetry(span)
|
|
303
|
+
}
|
|
302
304
|
await this.ensurePersistence()
|
|
303
305
|
|
|
304
306
|
return records.map((record) => this.modelSerializer.toPlainObject(record))
|
|
305
307
|
})
|
|
306
308
|
}
|
|
307
309
|
|
|
308
|
-
async upsertSomeTentative(builders: Record<RecordId, (record: TModel) => void>, span?: Span) {
|
|
310
|
+
async upsertSomeTentative(builders: Record<RecordId, (record: TModel) => void>, span?: Span, { skipPush = false } = {}) {
|
|
309
311
|
return this.upsertSome(Object.fromEntries(Object.entries(builders).map(([id, builder]) => [id, record => {
|
|
310
312
|
builder(record)
|
|
311
313
|
record._raw._status = 'synced'
|
|
312
|
-
}])), span)
|
|
314
|
+
}])), span, {skipPush})
|
|
313
315
|
}
|
|
314
316
|
|
|
315
|
-
async upsertOne(id: RecordId, builder: (record: TModel) => void, span?: Span) {
|
|
316
|
-
return this.upsertSome({ [id]: builder }, span).then(r => r[0])
|
|
317
|
+
async upsertOne(id: RecordId, builder: (record: TModel) => void, span?: Span, { skipPush = false } = {}) {
|
|
318
|
+
return this.upsertSome({ [id]: builder }, span, {skipPush}).then(r => r[0])
|
|
317
319
|
}
|
|
318
320
|
|
|
319
321
|
async upsertOneTentative(id: string, builder: (record: TModel) => void, span?: Span) {
|
|
320
322
|
return this.upsertSomeTentative({ [id]: builder }, span).then(r => r[0])
|
|
321
323
|
}
|
|
322
324
|
|
|
323
|
-
async deleteOne(id: RecordId, span?: Span) {
|
|
325
|
+
async deleteOne(id: RecordId, span?: Span, { skipPush = false } = {}) {
|
|
324
326
|
return await this.runScope.abortable(async () => {
|
|
325
327
|
let record: TModel | null = null
|
|
326
328
|
|
|
@@ -345,7 +347,9 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
|
|
|
345
347
|
|
|
346
348
|
this.emit('deleted', [id])
|
|
347
349
|
|
|
348
|
-
|
|
350
|
+
if (!skipPush) {
|
|
351
|
+
this.pushUnsyncedWithRetry(span)
|
|
352
|
+
}
|
|
349
353
|
await this.ensurePersistence()
|
|
350
354
|
|
|
351
355
|
return id
|
|
@@ -477,7 +481,7 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
|
|
|
477
481
|
)()
|
|
478
482
|
}
|
|
479
483
|
|
|
480
|
-
|
|
484
|
+
public async pushUnsyncedWithRetry(span?: Span) {
|
|
481
485
|
const records = await this.queryMaybeDeletedRecords(Q.where('_status', Q.notEq('synced')))
|
|
482
486
|
|
|
483
487
|
if (records.length) {
|