musora-content-services 2.107.4 → 2.107.6

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.
Files changed (218) hide show
  1. package/.claude/settings.local.json +3 -10
  2. package/.coderabbit.yaml +0 -0
  3. package/.editorconfig +0 -0
  4. package/.github/pull_request_template.md +0 -0
  5. package/.github/workflows/conventional-commits.yaml +0 -0
  6. package/.github/workflows/docs.js.yml +0 -0
  7. package/.github/workflows/node.js.yml +0 -0
  8. package/.prettierignore +0 -0
  9. package/.prettierrc +0 -0
  10. package/CHANGELOG.md +14 -0
  11. package/CLAUDE.md +0 -0
  12. package/README.md +0 -0
  13. package/babel.config.cjs +0 -0
  14. package/jest.config.js +0 -0
  15. package/jsdoc.json +0 -0
  16. package/package.json +1 -1
  17. package/src/constants/award-assets.js +0 -0
  18. package/src/contentMetaData.js +0 -0
  19. package/src/filterBuilder.js +0 -0
  20. package/src/index.d.ts +2 -6
  21. package/src/index.js +2 -6
  22. package/src/infrastructure/http/HttpClient.ts +0 -0
  23. package/src/infrastructure/http/executors/FetchRequestExecutor.ts +0 -0
  24. package/src/infrastructure/http/index.ts +0 -0
  25. package/src/infrastructure/http/interfaces/HeaderProvider.ts +0 -0
  26. package/src/infrastructure/http/interfaces/HttpError.ts +0 -0
  27. package/src/infrastructure/http/interfaces/NetworkError.ts +0 -0
  28. package/src/infrastructure/http/interfaces/RequestExecutor.ts +0 -0
  29. package/src/infrastructure/http/interfaces/RequestOptions.ts +0 -0
  30. package/src/infrastructure/http/providers/DefaultHeaderProvider.ts +0 -0
  31. package/src/lib/ads/monoid.ts +0 -0
  32. package/src/lib/ads/semigroup.ts +0 -0
  33. package/src/lib/brands.ts +0 -0
  34. package/src/lib/lastUpdated.js +0 -0
  35. package/src/lib/sanity/filter.ts +0 -0
  36. package/src/lib/sanity/query.ts +0 -0
  37. package/src/services/api/types.js +0 -0
  38. package/src/services/api/types.ts +0 -0
  39. package/src/services/awards/award-callbacks.js +0 -0
  40. package/src/services/awards/award-query.js +0 -0
  41. package/src/services/awards/internal/.indexignore +0 -0
  42. package/src/services/awards/internal/award-definitions.js +0 -0
  43. package/src/services/awards/internal/award-events.js +0 -0
  44. package/src/services/awards/internal/award-manager.js +0 -0
  45. package/src/services/awards/internal/certificate-builder.js +0 -0
  46. package/src/services/awards/internal/completion-data-generator.js +0 -0
  47. package/src/services/awards/internal/content-progress-observer.js +0 -0
  48. package/src/services/awards/internal/image-utils.js +0 -0
  49. package/src/services/awards/internal/message-generator.js +0 -0
  50. package/src/services/awards/internal/types.js +0 -0
  51. package/src/services/awards/types.d.ts +0 -0
  52. package/src/services/awards/types.js +0 -0
  53. package/src/services/config.js +0 -0
  54. package/src/services/content/artist.ts +0 -0
  55. package/src/services/content/content.ts +0 -0
  56. package/src/services/content/genre.ts +0 -0
  57. package/src/services/content/instructor.ts +0 -0
  58. package/src/services/content-org/content-org.js +0 -0
  59. package/src/services/content-org/guided-courses.ts +0 -0
  60. package/src/services/content-org/learning-paths.ts +5 -5
  61. package/src/services/content-org/playlists-types.js +0 -0
  62. package/src/services/content-org/playlists.js +0 -0
  63. package/src/services/content.js +0 -0
  64. package/src/services/contentAggregator.js +0 -0
  65. package/src/services/contentLikes.js +0 -0
  66. package/src/services/contentProgress.js +83 -33
  67. package/src/services/dataContext.js +0 -0
  68. package/src/services/dateUtils.js +0 -0
  69. package/src/services/eventsAPI.js +0 -0
  70. package/src/services/forums/categories.ts +0 -0
  71. package/src/services/forums/forums.ts +0 -0
  72. package/src/services/forums/types.ts +0 -0
  73. package/src/services/gamification/awards.ts +0 -0
  74. package/src/services/gamification/gamification.js +0 -0
  75. package/src/services/imageSRCBuilder.js +0 -0
  76. package/src/services/imageSRCVerify.js +0 -0
  77. package/src/services/liveTesting.ts +0 -0
  78. package/src/services/permissions/PermissionsAdapter.ts +0 -0
  79. package/src/services/permissions/PermissionsAdapterFactory.ts +0 -0
  80. package/src/services/permissions/PermissionsV1Adapter.ts +0 -0
  81. package/src/services/permissions/PermissionsV2Adapter.ts +0 -0
  82. package/src/services/permissions/README.md +0 -0
  83. package/src/services/permissions/index.ts +0 -0
  84. package/src/services/progress-events.js +0 -52
  85. package/src/services/progress-row/method-card.js +0 -0
  86. package/src/services/recommendations.js +0 -0
  87. package/src/services/reporting/README.md +0 -0
  88. package/src/services/reporting/types.ts +0 -0
  89. package/src/services/sanity.js +1 -1
  90. package/src/services/sentry/.indexignore +0 -0
  91. package/src/services/sentry/index.ts +0 -0
  92. package/src/services/sync/.indexignore +0 -0
  93. package/src/services/sync/adapters/factory.ts +0 -0
  94. package/src/services/sync/adapters/lokijs.ts +0 -0
  95. package/src/services/sync/adapters/sqlite.ts +0 -0
  96. package/src/services/sync/concurrency-safety.ts +0 -0
  97. package/src/services/sync/context/index.ts +0 -0
  98. package/src/services/sync/context/providers/base.ts +0 -0
  99. package/src/services/sync/context/providers/connectivity.ts +0 -0
  100. package/src/services/sync/context/providers/durability.ts +0 -0
  101. package/src/services/sync/context/providers/index.ts +0 -0
  102. package/src/services/sync/context/providers/session.ts +0 -0
  103. package/src/services/sync/context/providers/tabs.ts +0 -0
  104. package/src/services/sync/context/providers/visibility.ts +0 -0
  105. package/src/services/sync/database/factory.ts +0 -0
  106. package/src/services/sync/errors/boundary.ts +0 -0
  107. package/src/services/sync/errors/index.ts +0 -0
  108. package/src/services/sync/errors/validators.ts +0 -0
  109. package/src/services/sync/fetch.ts +0 -0
  110. package/src/services/sync/index.ts +0 -0
  111. package/src/services/sync/manager.ts +0 -4
  112. package/src/services/sync/models/Base.ts +0 -0
  113. package/src/services/sync/models/ContentLike.ts +0 -0
  114. package/src/services/sync/models/ContentProgress.ts +0 -0
  115. package/src/services/sync/models/Practice.ts +0 -0
  116. package/src/services/sync/models/PracticeDayNote.ts +0 -0
  117. package/src/services/sync/models/UserAwardProgress.ts +0 -0
  118. package/src/services/sync/models/index.ts +0 -0
  119. package/src/services/sync/repositories/base.ts +12 -8
  120. package/src/services/sync/repositories/content-likes.ts +0 -0
  121. package/src/services/sync/repositories/content-progress.ts +11 -7
  122. package/src/services/sync/repositories/index.ts +0 -0
  123. package/src/services/sync/repositories/practice-day-notes.ts +0 -0
  124. package/src/services/sync/repositories/practices.ts +0 -0
  125. package/src/services/sync/repositories/user-award-progress.ts +0 -0
  126. package/src/services/sync/repository-proxy.ts +0 -0
  127. package/src/services/sync/resolver.ts +0 -0
  128. package/src/services/sync/retry.ts +0 -0
  129. package/src/services/sync/run-scope.ts +0 -0
  130. package/src/services/sync/schema/index.ts +0 -0
  131. package/src/services/sync/serializers/index.ts +0 -0
  132. package/src/services/sync/serializers/model.ts +0 -0
  133. package/src/services/sync/serializers/raw.ts +0 -0
  134. package/src/services/sync/store/index.ts +13 -9
  135. package/src/services/sync/store/push-coalescer.ts +0 -0
  136. package/src/services/sync/store-configs.ts +0 -0
  137. package/src/services/sync/strategies/base.ts +0 -0
  138. package/src/services/sync/strategies/index.ts +0 -0
  139. package/src/services/sync/strategies/initial.ts +0 -0
  140. package/src/services/sync/strategies/polling.ts +0 -0
  141. package/src/services/sync/telemetry/index.ts +0 -0
  142. package/src/services/sync/telemetry/sampling.ts +0 -0
  143. package/src/services/sync/utils/event-emitter.ts +0 -0
  144. package/src/services/sync/utils/index.ts +0 -0
  145. package/src/services/sync/utils/throttle.ts +0 -0
  146. package/src/services/sync/utils/timers.ts +0 -0
  147. package/src/services/types.js +0 -0
  148. package/src/services/user/account.ts +0 -0
  149. package/src/services/user/chat.js +0 -0
  150. package/src/services/user/interests.js +0 -0
  151. package/src/services/user/management.js +0 -0
  152. package/src/services/user/memberships.ts +0 -0
  153. package/src/services/user/notifications.js +0 -0
  154. package/src/services/user/onboarding.ts +0 -0
  155. package/src/services/user/payments.ts +0 -0
  156. package/src/services/user/permissions.js +0 -0
  157. package/src/services/user/profile.js +0 -0
  158. package/src/services/user/sessions.js +0 -0
  159. package/src/services/user/types.d.ts +0 -0
  160. package/src/services/user/types.js +0 -0
  161. package/src/services/user/user-management-system.js +0 -0
  162. package/src/services/userActivity.js +0 -0
  163. package/test/HttpClient.test.js +0 -0
  164. package/test/awards/award-alacarte-observer.test.js +0 -0
  165. package/test/awards/award-auto-refresh.test.js +0 -0
  166. package/test/awards/award-calculations.test.js +0 -0
  167. package/test/awards/award-certificate-display.test.js +0 -0
  168. package/test/awards/award-collection-edge-cases.test.js +0 -0
  169. package/test/awards/award-collection-filtering.test.js +0 -0
  170. package/test/awards/award-completion-flow.test.js +0 -0
  171. package/test/awards/award-exclusion-handling.test.js +0 -0
  172. package/test/awards/award-multi-lesson.test.js +0 -0
  173. package/test/awards/award-observer-integration.test.js +0 -0
  174. package/test/awards/award-query-messages.test.js +0 -0
  175. package/test/awards/award-user-collection.test.js +0 -0
  176. package/test/awards/duplicate-prevention.test.js +0 -0
  177. package/test/awards/helpers/completion-mock.js +0 -0
  178. package/test/awards/helpers/index.js +0 -0
  179. package/test/awards/helpers/mock-setup.js +0 -0
  180. package/test/awards/helpers/progress-emitter.js +0 -0
  181. package/test/awards/message-generator.test.js +0 -0
  182. package/test/content.test.js +0 -0
  183. package/test/contentLikes.test.js +0 -0
  184. package/test/contentProgress.test.js +0 -0
  185. package/test/dataContext.test.js +0 -0
  186. package/test/forum.test.js +0 -0
  187. package/test/imageSRCBuilder.test.js +0 -0
  188. package/test/imageSRCVerify.test.js +0 -0
  189. package/test/initializeTests.js +0 -0
  190. package/test/learningPaths.test.js +0 -0
  191. package/test/lib/__snapshots__/filter.test.ts.snap +0 -0
  192. package/test/lib/filter.test.ts +0 -0
  193. package/test/lib/lastUpdated.test.js +0 -0
  194. package/test/lib/query.test.ts +0 -0
  195. package/test/live/contentProgressLive.test.js +0 -0
  196. package/test/live/railcontentLive.test.js +0 -0
  197. package/test/localStorageMock.js +0 -0
  198. package/test/log.js +0 -0
  199. package/test/mockData/award-definitions.js +0 -0
  200. package/test/mockData/mockData_fetchByRailContentIds_one_content.json +0 -0
  201. package/test/mockData/mockData_progress_content.json +0 -0
  202. package/test/mockData/mockData_sanity_progress_content.json +0 -0
  203. package/test/mockData/mockData_user_practices.json +0 -0
  204. package/test/notifications.test.js +0 -0
  205. package/test/progressRows.test.js +0 -0
  206. package/test/sanityQueryService.test.js +0 -0
  207. package/test/streakMessage.test.js +0 -0
  208. package/test/sync/adapter.ts +0 -0
  209. package/test/sync/initialize-sync-manager.js +0 -0
  210. package/test/sync/models/award-database-integration.test.js +0 -0
  211. package/test/user/permissions.test.js +0 -0
  212. package/test/userActivity.test.js +0 -0
  213. package/tools/generate-index.cjs +0 -0
  214. package/.yarnrc.yml +0 -1
  215. package/check_content.js +0 -30
  216. package/check_content.mjs +0 -32
  217. package/test/reporting.test.js +0 -132
  218. package/test_owned_navigate.js +0 -74
@@ -1,16 +1,9 @@
1
1
  {
2
2
  "permissions": {
3
3
  "allow": [
4
- "Read(//app/musora-platform-backend/**)",
5
- "Read(//app/musora-platform-frontend/**)",
6
- "Bash(find:*)",
7
- "Bash(sed:*)",
8
- "Read(//app/**)",
9
- "Bash(cat:*)",
10
- "Bash(docker exec:*)",
11
- "Bash(npm config:*)"
4
+ "Bash(rg:*)",
5
+ "Bash(npm run lint:*)"
12
6
  ],
13
- "deny": [],
14
- "ask": []
7
+ "deny": []
15
8
  }
16
9
  }
package/.coderabbit.yaml CHANGED
File without changes
package/.editorconfig CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
package/.prettierignore CHANGED
File without changes
package/.prettierrc CHANGED
File without changes
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.6](https://github.com/railroadmedia/musora-content-services/compare/v2.107.5...v2.107.6) (2025-12-29)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * **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))
11
+
12
+ ### [2.107.5](https://github.com/railroadmedia/musora-content-services/compare/v2.107.4...v2.107.5) (2025-12-29)
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+ * establishes a positive progress validation ([#672](https://github.com/railroadmedia/musora-content-services/issues/672)) ([e9bc211](https://github.com/railroadmedia/musora-content-services/commit/e9bc211659262b282e1073c2746c3c8824371c35))
18
+
5
19
  ### [2.107.4](https://github.com/railroadmedia/musora-content-services/compare/v2.107.1...v2.107.4) (2025-12-22)
6
20
 
7
21
 
package/CLAUDE.md CHANGED
File without changes
package/README.md CHANGED
File without changes
package/babel.config.cjs CHANGED
File without changes
package/jest.config.js CHANGED
File without changes
package/jsdoc.json CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "musora-content-services",
3
- "version": "2.107.4",
3
+ "version": "2.107.6",
4
4
  "description": "A package for Musoras content services ",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
File without changes
File without changes
File without changes
package/src/index.d.ts CHANGED
@@ -54,7 +54,7 @@ import {
54
54
  getEnrichedLearningPaths,
55
55
  getLearningPathLessonsByIds,
56
56
  mapContentToParent,
57
- onContentCompletedLearningPathListener,
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
- onContentCompleted,
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
- onContentCompletedLearningPathListener,
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
- onContentCompleted,
678
- onContentCompletedLearningPathListener,
674
+ onContentCompletedLearningPathActions,
679
675
  onProgressSaved,
680
676
  openComment,
681
677
  otherStats,
File without changes
File without changes
File without changes
File without changes
File without changes
package/src/lib/brands.ts CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -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 onContentCompletedLearningPathListener(event) {
449
- if (event?.collection?.type !== COLLECTION_TYPE.LEARNING_PATH) return
450
- if (event.contentId !== event?.collection?.id) return
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 = event.contentId
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
  }
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -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 { emitContentCompleted } from './progress-events'
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,47 +463,69 @@ 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
 
470
+ async function saveContentProgress(contentId, collection, progress, currentSeconds, {skipPush = false} = {}) {
471
+ const isLP = collection?.type === COLLECTION_TYPE.LEARNING_PATH
471
472
  async function saveContentProgress(contentId, collection, progress, currentSeconds) {
473
+
474
+ // filter out contentIds that are setting progress lower than existing
475
+ const contentIdProgress = await getProgressDataByIds([contentId], collection)
476
+ if (progress <= contentIdProgress[contentId].progress) {
477
+ return
478
+ }
479
+
472
480
  const response = await db.contentProgress.recordProgress(
473
481
  contentId,
474
482
  collection,
475
483
  progress,
476
- currentSeconds
484
+ currentSeconds,
485
+ {skipPush: true}
477
486
  )
478
- if (progress === 100) emitContentCompleted(contentId, collection)
479
-
480
487
  // note - previous implementation explicitly did not trickle progress to children here
481
488
  // (only to siblings/parents via le bubbles)
482
489
 
483
490
  const hierarchy = await getHierarchy(contentId, collection)
484
491
 
485
492
  const bubbledProgresses = await bubbleProgress(hierarchy, contentId, collection)
493
+
494
+ // filter out contentIds that are setting progress lower than existing
495
+ const existingProgresses = await getProgressDataByIds(Object.keys(bubbledProgresses), collection)
496
+ for (const [bubbledContentId, bubbledProgress] of Object.entries(bubbledProgresses)) {
497
+ if (bubbledProgress <= existingProgresses[bubbledContentId].progress) {
498
+ delete bubbledProgresses[bubbledContentId]
499
+ }
500
+ }
501
+
486
502
  // BE bubbling/trickling currently does not work, so we utilize non-tentative pushing when learning path collection
487
- await db.contentProgress.recordProgressMany(bubbledProgresses, collection, collection?.type !== COLLECTION_TYPE.LEARNING_PATH)
503
+ await db.contentProgress.recordProgressMany(bubbledProgresses, collection, {tentative: !isLP, skipPush: true})
488
504
 
489
- if (collection && collection.type === COLLECTION_TYPE.LEARNING_PATH) {
505
+ if (isLP) {
490
506
  let exportIds = bubbledProgresses
491
507
  exportIds[contentId] = progress
492
- await duplicateLearningPathProgressToExternalContents(exportIds, collection, hierarchy)
508
+ await duplicateLearningPathProgressToExternalContents(exportIds, collection, hierarchy, {skipPush: true})
493
509
  }
494
510
 
511
+ if (progress === 100) await onContentCompletedLearningPathActions(contentId, collection)
512
+
495
513
  for (const [bubbledContentId, bubbledProgress] of Object.entries(bubbledProgresses)) {
496
514
  if (bubbledProgress === 100) {
497
- emitContentCompleted(Number(bubbledContentId), collection)
515
+ await onContentCompletedLearningPathActions(Number(bubbledContentId), collection)
498
516
  }
499
517
  }
518
+
519
+ if (!skipPush) db.contentProgress.requestPushUnsynced()
520
+
500
521
  return response
501
522
  }
502
523
 
503
- async function setStartedOrCompletedStatus(contentId, collection, isCompleted) {
504
- const progress = isCompleted ? 100 : 0
505
- const response = await db.contentProgress.recordProgress(contentId, collection, progress)
524
+ async function setStartedOrCompletedStatus(contentId, collection, isCompleted, {skipPush = false} = {}) {
525
+ const isLP = collection?.type === COLLECTION_TYPE.LEARNING_PATH
506
526
 
507
- if (progress === 100) emitContentCompleted(contentId, collection)
527
+ const progress = isCompleted ? 100 : 0
528
+ const response = await db.contentProgress.recordProgress(contentId, collection, progress, null, {skipPush: true})
508
529
 
509
530
  const hierarchy = await getHierarchy(contentId, collection)
510
531
 
@@ -513,29 +534,33 @@ async function setStartedOrCompletedStatus(contentId, collection, isCompleted) {
513
534
  ...await bubbleProgress(hierarchy, contentId, collection)
514
535
  }
515
536
  // BE bubbling/trickling currently does not work, so we utilize non-tentative pushing when learning path collection
516
- await db.contentProgress.recordProgressMany(progresses, collection, collection?.type !== COLLECTION_TYPE.LEARNING_PATH)
537
+ await db.contentProgress.recordProgressMany(progresses, collection, {tentative: !isLP, skipPush: true})
538
+ if (isLP) {
517
539
 
518
- if (collection && collection.type === COLLECTION_TYPE.LEARNING_PATH) {
519
540
  let exportProgresses = progresses
520
541
  exportProgresses[contentId] = progress
521
- await duplicateLearningPathProgressToExternalContents(exportProgresses, collection, hierarchy)
542
+ await duplicateLearningPathProgressToExternalContents(exportProgresses, collection, hierarchy, {skipPush: true})
522
543
  }
523
544
 
545
+ if (progress === 100) await onContentCompletedLearningPathActions(contentId, collection)
546
+
524
547
  for (const [id, progress] of Object.entries(progresses)) {
525
548
  if (progress === 100) {
526
- emitContentCompleted(Number(id), collection)
549
+ await onContentCompletedLearningPathActions(Number(id), collection)
527
550
  }
528
551
  }
529
552
 
553
+ if (!skipPush) db.contentProgress.requestPushUnsynced()
554
+
530
555
  return response
531
556
  }
532
557
 
533
558
  // we cannot simply pass LP id with self collection, because we do not have a-la-carte LP's set up yet,
534
559
  // and we need each lesson to bubble to its parent outside of LP
535
- async function duplicateLearningPathProgressToExternalContents(ids, collection, hierarchy) {
560
+ async function duplicateLearningPathProgressToExternalContents(ids, collection, hierarchy, {skipPush = false} = {}) {
536
561
  // filter out LPs. we dont want to duplicate to LP's while we dont have a-la-cart LP's set up.
537
562
  let filteredIds = Object.fromEntries(
538
- Object.entries(ids).filter((id) => {
563
+ Object.entries(ids).filter(([id]) => {
539
564
  return hierarchy.parents[parseInt(id)] !== null
540
565
  })
541
566
  )
@@ -551,8 +576,13 @@ async function duplicateLearningPathProgressToExternalContents(ids, collection,
551
576
  })
552
577
 
553
578
  // each handles its own bubbling.
554
- filteredIds.forEach(([id, pct]) => {
555
- saveContentProgress(parseInt(id), null, pct)
579
+ // skipPush on all but last to avoid multiple push requests
580
+ filteredIds.forEach(([id, pct], index) => {
581
+ if (index === filteredIds.length - 1) {
582
+ saveContentProgress(parseInt(id), null, pct, null, {skipPush})
583
+ } else {
584
+ saveContentProgress(parseInt(id), null, pct, null, {skipPush: true})
585
+ }
556
586
  })
557
587
  }
558
588
 
@@ -564,10 +594,18 @@ async function getHierarchy(contentId, collection) {
564
594
  }
565
595
  }
566
596
 
567
- async function setStartedOrCompletedStatusMany(contentIds, collection, isCompleted) {
597
+ async function setStartedOrCompletedStatusMany(contentIds, collection, isCompleted, {skipPush = false} = {}) {
598
+ const isLP = collection?.type === COLLECTION_TYPE.LEARNING_PATH
568
599
  const progress = isCompleted ? 100 : 0
600
+
601
+ if (progress === 100) {
602
+ for (const contentId of contentIds) {
603
+ await onContentCompletedLearningPathActions(contentId, collection)
604
+ }
605
+ }
606
+
569
607
  const contents = Object.fromEntries(contentIds.map((id) => [id, progress]))
570
- const response = await db.contentProgress.recordProgressMany(contents, collection, true)
608
+ const response = await db.contentProgress.recordProgressMany(contents, collection, {tentative: !isLP, skipPush: true})
571
609
 
572
610
  // we assume this is used only for contents within the same hierarchy
573
611
  const hierarchy = await getHierarchy(collection.id, collection)
@@ -581,22 +619,32 @@ async function setStartedOrCompletedStatusMany(contentIds, collection, isComplet
581
619
  }
582
620
  }
583
621
  // BE bubbling/trickling currently does not work, so we utilize non-tentative pushing when learning path collection
584
- await db.contentProgress.recordProgressMany(progresses, collection, collection?.type !== COLLECTION_TYPE.LEARNING_PATH)
622
+ await db.contentProgress.recordProgressMany(progresses, collection, {tentative: !isLP, skipPush: true})
585
623
 
586
- if (collection && collection.type === COLLECTION_TYPE.LEARNING_PATH) {
624
+ if (isLP) {
587
625
  let exportProgresses = progresses
588
626
  for (const contentId of contentIds){
589
627
  exportProgresses[contentId] = progress
590
628
  }
591
- await duplicateLearningPathProgressToExternalContents(exportProgresses, collection, hierarchy)
629
+ await duplicateLearningPathProgressToExternalContents(exportProgresses, collection, hierarchy, {skipPush: true})
592
630
  }
593
631
 
632
+ for (const [id, progress] of Object.entries(progresses)) {
633
+ if (progress === 100) {
634
+ await onContentCompletedLearningPathActions(Number(id), collection)
635
+ }
636
+ }
637
+
638
+ if (!skipPush) db.contentProgress.requestPushUnsynced()
639
+
594
640
  return response
595
641
  }
596
642
 
597
- async function resetStatus(contentId, collection = null) {
643
+ async function resetStatus(contentId, collection = null, {skipPush = false} = {}) {
644
+ const isLP = collection?.type === COLLECTION_TYPE.LEARNING_PATH
645
+
598
646
  const progress = 0
599
- const response = await db.contentProgress.eraseProgress(contentId, collection)
647
+ const response = await db.contentProgress.eraseProgress(contentId, collection, {skipPush: true})
600
648
  const hierarchy = await getHierarchy(contentId, collection)
601
649
 
602
650
  let progresses = {
@@ -604,13 +652,15 @@ async function resetStatus(contentId, collection = null) {
604
652
  ...await bubbleProgress(hierarchy, contentId, collection)
605
653
  }
606
654
  // BE bubbling/trickling currently does not work, so we utilize non-tentative pushing when learning path collection
607
- await db.contentProgress.recordProgressMany(progresses, collection, collection?.type !== COLLECTION_TYPE.LEARNING_PATH)
655
+ await db.contentProgress.recordProgressMany(progresses, collection, {tentative: !isLP, skipPush: true})
608
656
 
609
- if (collection && collection.type === COLLECTION_TYPE.LEARNING_PATH) {
657
+ if (isLP) {
610
658
  progresses[contentId] = progress
611
- await duplicateLearningPathProgressToExternalContents(progresses, collection, hierarchy)
659
+ await duplicateLearningPathProgressToExternalContents(progresses, collection, hierarchy, {skipPush: true})
612
660
  }
613
661
 
662
+ if (!skipPush) db.contentProgress.requestPushUnsynced()
663
+
614
664
  return response
615
665
  }
616
666
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -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
- }
File without changes
File without changes
File without changes
File without changes