musora-content-services 2.152.1 → 2.154.0

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 (90) hide show
  1. package/.github/workflows/automated-testing.yml +24 -0
  2. package/CHANGELOG.md +29 -0
  3. package/codecov.yml +5 -0
  4. package/jest.config.js +39 -8
  5. package/package.json +5 -1
  6. package/src/contentTypeConfig.js +13 -14
  7. package/src/index.d.ts +8 -2
  8. package/src/index.js +8 -2
  9. package/src/infrastructure/http/interfaces/RequestOptions.ts +1 -1
  10. package/src/services/awards/internal/award-definitions.js +3 -3
  11. package/src/services/content-org/guided-courses.ts +2 -2
  12. package/src/services/contentProgress.js +35 -54
  13. package/src/services/dateUtils.js +9 -1
  14. package/src/services/forums/posts.ts +2 -2
  15. package/src/services/multi-user-accounts/multi-user-accounts.ts +43 -7
  16. package/src/services/reporting/reporting.ts +3 -4
  17. package/src/services/sanity.js +26 -58
  18. package/src/services/sync/adapters/lokijs.ts +7 -4
  19. package/src/services/sync/fetch.ts +2 -14
  20. package/src/services/sync/repositories/base.ts +4 -0
  21. package/src/services/sync/repositories/content-progress.ts +3 -3
  22. package/src/services/sync/store/index.ts +6 -1
  23. package/src/services/sync/strategies/base.ts +1 -1
  24. package/src/services/sync/telemetry/index.ts +1 -1
  25. package/src/services/urlBuilder.ts +1 -0
  26. package/src/services/user/streakCalculator.ts +1 -1
  27. package/test/SKIPPED_TESTS.md +151 -0
  28. package/test/initializeTests.js +2 -3
  29. package/test/{content.test.js → integration/content.test.js} +7 -23
  30. package/test/integration/contentProgress.test.js +73 -0
  31. package/test/{forum.test.js → integration/forum.test.js} +2 -4
  32. package/test/{sanityQueryService.test.js → integration/sanityQueryService.test.js} +143 -291
  33. package/test/{user → integration/user}/permissions.test.js +5 -4
  34. package/test/live/README.md +29 -0
  35. package/test/setupConsole.js +6 -0
  36. package/test/setupNetworkGuard.js +3 -0
  37. package/test/{HttpClient.test.js → unit/HttpClient.test.js} +5 -5
  38. package/test/{awards → unit/awards}/award-alacarte-observer.test.js +13 -12
  39. package/test/{awards → unit/awards}/award-auto-refresh.test.js +4 -3
  40. package/test/{awards → unit/awards}/award-calculations.test.js +3 -2
  41. package/test/{awards → unit/awards}/award-certificate-display.test.js +12 -11
  42. package/test/{awards → unit/awards}/award-collection-edge-cases.test.js +12 -11
  43. package/test/{awards → unit/awards}/award-collection-filtering.test.js +12 -11
  44. package/test/{awards → unit/awards}/award-completion-flow.test.js +15 -14
  45. package/test/{awards → unit/awards}/award-exclusion-handling.test.js +20 -19
  46. package/test/{awards → unit/awards}/award-multi-lesson.test.js +14 -13
  47. package/test/{awards → unit/awards}/award-observer-integration.test.js +14 -13
  48. package/test/{awards → unit/awards}/award-query-messages.test.js +30 -21
  49. package/test/{awards → unit/awards}/award-user-collection.test.js +11 -8
  50. package/test/{awards → unit/awards}/duplicate-prevention.test.js +12 -11
  51. package/test/unit/awards/helpers/index.js +3 -0
  52. package/test/{awards → unit/awards}/helpers/mock-setup.js +1 -1
  53. package/test/{awards → unit/awards}/helpers/progress-emitter.js +2 -2
  54. package/test/{awards → unit/awards}/message-generator.test.js +1 -1
  55. package/test/unit/contentLikes.test.js +62 -0
  56. package/test/unit/contentProgress.test.js +75 -0
  57. package/test/{dataContext.test.js → unit/dataContext.test.js} +2 -2
  58. package/test/unit/dateUtils.test.js +188 -0
  59. package/test/{imageSRCBuilder.test.js → unit/imageSRCBuilder.test.js} +2 -2
  60. package/test/{imageSRCVerify.test.js → unit/imageSRCVerify.test.js} +1 -1
  61. package/test/{lib → unit/lib}/filter.test.ts +10 -4
  62. package/test/{lib → unit/lib}/lastUpdated.test.js +6 -6
  63. package/test/{lib → unit/lib}/query.test.ts +1 -1
  64. package/test/{notifications.test.js → unit/notifications.test.js} +51 -39
  65. package/test/{progressRows.test.js → unit/progressRows.test.js} +57 -35
  66. package/test/unit/sanityQueryService.test.js +180 -0
  67. package/test/{streakMessage.test.js → unit/streakMessage.test.js} +18 -27
  68. package/test/unit/sync/adapters/idb-errors.test.ts +144 -0
  69. package/test/unit/sync/adapters/sqlite-errors.test.ts +173 -0
  70. package/test/unit/sync/helpers/TestModel.ts +44 -0
  71. package/test/unit/sync/helpers/index.ts +172 -0
  72. package/test/unit/sync/repositories/content-likes.test.ts +99 -0
  73. package/test/unit/sync/repositories/practices.test.ts +179 -0
  74. package/test/unit/sync/repositories/progress.test.ts +245 -0
  75. package/test/unit/sync/store/store-idb.test.ts +180 -0
  76. package/test/unit/sync/store/store.test.ts +274 -0
  77. package/test/unit/userActivity.test.js +99 -0
  78. package/tsconfig.json +15 -0
  79. package/test/awards/helpers/index.js +0 -3
  80. package/test/contentLikes.test.js +0 -95
  81. package/test/contentProgress.test.js +0 -279
  82. package/test/learningPaths.test.js +0 -70
  83. package/test/live/contentProgressLive.test.js +0 -110
  84. package/test/live/railcontentLive.test.js +0 -7
  85. package/test/sync/adapter.ts +0 -9
  86. package/test/sync/initialize-sync-manager.js +0 -88
  87. package/test/sync/models/award-database-integration.test.js +0 -519
  88. package/test/userActivity.test.js +0 -118
  89. /package/test/{awards → unit/awards}/helpers/completion-mock.js +0 -0
  90. /package/test/{lib → unit/lib}/__snapshots__/filter.test.ts.snap +0 -0
@@ -0,0 +1,24 @@
1
+ name: Automated Testing
2
+ on:
3
+ pull_request:
4
+ types: [opened, synchronize, reopened]
5
+ push:
6
+ branches: [main]
7
+ jobs:
8
+ unit-tests:
9
+ runs-on: ubuntu-latest
10
+ timeout-minutes: 5
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: actions/setup-node@v4
14
+ with:
15
+ node-version: 20
16
+ cache: npm
17
+ - name: Install dependencies
18
+ run: npm ci
19
+ - name: Run unit tests
20
+ run: npm test -- --coverage
21
+ - name: Upload coverage to Codecov
22
+ uses: codecov/codecov-action@v4
23
+ with:
24
+ token: ${{ secrets.CODECOV_TOKEN }}
package/CHANGELOG.md CHANGED
@@ -2,6 +2,35 @@
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.154.0](https://github.com/railroadmedia/musora-content-services/compare/v2.153.0...v2.154.0) (2026-04-21)
6
+
7
+
8
+ ### Features
9
+
10
+ * **BEHLTP-23:** add multi-user account update endpoint ([#914](https://github.com/railroadmedia/musora-content-services/issues/914)) ([98f6c4b](https://github.com/railroadmedia/musora-content-services/commit/98f6c4bb830c7a8a25ba556ca07fc97023667011))
11
+ * muppet - add invite validity/error data ([#923](https://github.com/railroadmedia/musora-content-services/issues/923)) ([3938fa5](https://github.com/railroadmedia/musora-content-services/commit/3938fa56ec9295023b6ba624c5f238d1645a911b))
12
+
13
+
14
+ ### Bug Fixes
15
+
16
+ * add typeof before LokiJSAdapter in parameter list ([6a135b5](https://github.com/railroadmedia/musora-content-services/commit/6a135b57c516c277a50d707417544ab2f9eeb194))
17
+ * **BR-632:** dont trickle progress to children when enrolling in GC ([#916](https://github.com/railroadmedia/musora-content-services/issues/916)) ([67a96d6](https://github.com/railroadmedia/musora-content-services/commit/67a96d69bd8b61f12d0efd45475a7b0b08c58813))
18
+
19
+ ## [2.153.0](https://github.com/railroadmedia/musora-content-services/compare/v2.152.1...v2.153.0) (2026-04-15)
20
+
21
+
22
+ ### Features
23
+
24
+ * 15 minutes ([#911](https://github.com/railroadmedia/musora-content-services/issues/911)) ([87594f8](https://github.com/railroadmedia/musora-content-services/commit/87594f8d33a90452df66d55e7c00d9b0eb15b30b))
25
+ * initial not bad melon test slop ([#918](https://github.com/railroadmedia/musora-content-services/issues/918)) ([008052b](https://github.com/railroadmedia/musora-content-services/commit/008052bbce724ae49fc12734e28957846be76ab3))
26
+
27
+
28
+ ### Bug Fixes
29
+
30
+ * loki adapter cjs/esm export issues in mcs-cli ([#920](https://github.com/railroadmedia/musora-content-services/issues/920)) ([480755e](https://github.com/railroadmedia/musora-content-services/commit/480755e816e28540cd123efd24ae7e299baecabe))
31
+ * remove flush session interval ([#867](https://github.com/railroadmedia/musora-content-services/issues/867)) ([67af03b](https://github.com/railroadmedia/musora-content-services/commit/67af03b5a0d8159d7754e86d94fa736ac47beab0))
32
+ * unref timer only in node ([92385d6](https://github.com/railroadmedia/musora-content-services/commit/92385d66659b76f1f41ebbfba3557aaea14f1abd))
33
+
5
34
  ### [2.152.1](https://github.com/railroadmedia/musora-content-services/compare/v2.152.0...v2.152.1) (2026-04-09)
6
35
 
7
36
  ## [2.152.0](https://github.com/railroadmedia/musora-content-services/compare/v2.151.1...v2.152.0) (2026-04-09)
package/codecov.yml ADDED
@@ -0,0 +1,5 @@
1
+ # codecov.yml
2
+ comment:
3
+ layout: "reach,diff,flags,tree"
4
+ behavior: default
5
+ require_changes: false
package/jest.config.js CHANGED
@@ -17,19 +17,43 @@ export default {
17
17
  // Automatically clear mock calls, instances, contexts and results before every test
18
18
  clearMocks: true,
19
19
 
20
+ testTimeout: 30000,
21
+
20
22
  // Indicates whether the coverage information should be collected while executing the test
21
23
  collectCoverage: true,
22
24
 
23
- // An array of glob patterns indicating a set of files for which coverage information should be collected
24
- // collectCoverageFrom: undefined,
25
+ // Exclude pure transport/adapter layers with no business logic.
26
+ // Files with business logic stay in even at low coverage — gaps should be visible.
27
+ collectCoverageFrom: [
28
+ 'src/**/*.{js,ts}',
29
+ '!src/services/sanity.js',
30
+ '!src/services/railcontent.js',
31
+ '!src/services/recommendations.js',
32
+ '!src/index.js',
33
+ '!src/index.d.ts',
34
+ '!src/services/user/account.ts',
35
+ '!src/services/user/sessions.js',
36
+ '!src/services/user/profile.js',
37
+ '!src/services/user/management.js',
38
+ '!src/services/user/interests.js',
39
+ '!src/services/user/payments.ts',
40
+ '!src/services/user/chat.js',
41
+ ],
25
42
 
26
43
  // The directory where Jest should output its coverage files
27
44
  coverageDirectory: 'coverage',
28
45
 
29
- // An array of regexp pattern strings used to skip coverage collection
30
- // coveragePathIgnorePatterns: [
31
- // "/node_modules/"
32
- // ],
46
+ // Global threshold set just below current baseline.
47
+ // Intent is to ratchet up over time as coverage improves.
48
+ // Do not lower these numbers — raise them as tests are added.
49
+ coverageThreshold: {
50
+ global: {
51
+ statements: 40,
52
+ branches: 25,
53
+ functions: 40,
54
+ lines: 40,
55
+ },
56
+ },
33
57
 
34
58
  // Indicates which provider should be used to instrument code for coverage
35
59
  // coverageProvider: "babel",
@@ -92,7 +116,10 @@ export default {
92
116
  // moduleNameMapper: {},
93
117
 
94
118
  // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
95
- modulePathIgnorePatterns: ['<rootDir>/test/live'],
119
+ modulePathIgnorePatterns: [
120
+ '<rootDir>/test/live',
121
+ '<rootDir>/test/integration'
122
+ ],
96
123
 
97
124
  // Activates notifications for test results
98
125
  // notify: false,
@@ -136,7 +163,11 @@ export default {
136
163
  // setupFiles: [],
137
164
 
138
165
  // A list of paths to modules that run some code to configure or set up the testing framework before each test
139
- setupFilesAfterEnv: ['dotenv/config'],
166
+ setupFilesAfterEnv: [
167
+ 'dotenv/config',
168
+ '<rootDir>/test/setupConsole.js',
169
+ '<rootDir>/test/setupNetworkGuard.js'
170
+ ],
140
171
 
141
172
  // The number of seconds after which a test is considered as slow and reported as such in the results.
142
173
  // slowTestThreshold: 5,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "musora-content-services",
3
- "version": "2.152.1",
3
+ "version": "2.154.0",
4
4
  "description": "A package for Musoras content services ",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -26,10 +26,14 @@
26
26
  "@babel/preset-env": "^7.25.3",
27
27
  "@babel/preset-typescript": "^7.27.1",
28
28
  "@sentry/browser": "^10.21.0",
29
+ "@types/better-sqlite3": "^7.6.13",
29
30
  "@types/jest": "^30.0.0",
30
31
  "babel-jest": "^29.7.0",
32
+ "better-sqlite3": "^12.9.0",
31
33
  "dotenv": "^16.4.5",
34
+ "fake-indexeddb": "^6.2.5",
32
35
  "jest": "^29.7.0",
36
+ "jest-environment-jsdom": "^30.3.0",
33
37
  "jsdoc": "^4.0.3",
34
38
  "jsdoc-babel": "^0.5.0",
35
39
  "prettier": "3.4.2",
@@ -8,6 +8,9 @@ export const CloudFrontURl = 'https://d3fzm1tzeyr5n3.cloudfront.net'
8
8
 
9
9
  // This is used to pull related content by license, so we only show "consumable" content
10
10
  export const SONG_TYPES = ['song', 'play-along', 'jam-track', 'song-tutorial-lesson']
11
+
12
+ export const parentReferenceField = 'parent_content_reference[0]'
13
+ export const grandParentReferenceField = 'parent_content_reference[1]'
11
14
  // Oct 2025: It turns out content-meta categories are not really clear
12
15
  // THis is used for the page_type field as a post processor so we include parents and children
13
16
  // Duplicated in SanityGateway.php if you update this, update that
@@ -23,10 +26,6 @@ export const SINGLE_PARENT_TYPES = ['course-lesson', 'pack-bundle-lesson', 'song
23
26
 
24
27
  export const LEARNING_PATH_LESSON = 'learning-path-lesson-v2'
25
28
 
26
- export const parentField = 'parent_content_data[0]'
27
-
28
- export const grandParentField = 'parent_content_data[1]'
29
-
30
29
  export const genreField = `genre[]->{
31
30
  name,
32
31
  'slug': slug.current,
@@ -45,7 +44,7 @@ export const instructorField = `instructor[]->{
45
44
 
46
45
  export const artistField = `select(
47
46
  defined(artist) => artist->{ 'name': name, 'slug': slug.current, 'thumbnail': thumbnail_url.asset->url},
48
- defined(parent_content_data) => *[_type == ^.parent_content_data[0].type && railcontent_id == ^.parent_content_data[0].id][0].artist->{ 'name': name, 'slug': slug.current, 'thumbnail': thumbnail_url.asset->url}
47
+ defined(parent_content_reference) => ${parentReferenceField}->artist->{ 'name': name, 'slug': slug.current, 'thumbnail': thumbnail_url.asset->url}
49
48
  )`
50
49
 
51
50
  export const DEFAULT_FIELDS = [
@@ -68,8 +67,8 @@ export const DEFAULT_FIELDS = [
68
67
  "'slug' : slug.current",
69
68
  "'permission_id': permission_v2",
70
69
  'child_count',
71
- '"parent_id": parent_content_data[0].id',
72
- '"grandparent_id": parent_content_data[1].id',
70
+ `"parent_id": ${parentReferenceField}->railcontent_id`,
71
+ `"grandparent_id": ${grandParentReferenceField}->railcontent_id`,
73
72
  'live_event_start_time',
74
73
  'live_event_end_time',
75
74
  'enrollment_start_time',
@@ -96,8 +95,8 @@ export const DEFAULT_CHILD_FIELDS = [
96
95
  "'slug' : slug.current",
97
96
  "'permission_id': permission_v2",
98
97
  'child_count',
99
- '"parent_id": parent_content_data[0].id',
100
- '"grandparent_id": parent_content_data[1].id',
98
+ `"parent_id": ${parentReferenceField}->railcontent_id`,
99
+ `"grandparent_id": ${grandParentReferenceField}->railcontent_id`,
101
100
  ]
102
101
 
103
102
  export const playAlongMp3sField = `{
@@ -118,7 +117,7 @@ export const descriptionField = 'description[0].children[0].text'
118
117
  // this pulls both any defined resources for the document as well as any resources in the parent document
119
118
  export const resourcesField = `[
120
119
  ... resource[]{resource_name, _key, "resource_url": coalesce('${CloudFrontURl}'+string::split(resource_aws.asset->fileURL, '${AWSUrl}')[1], resource_url )},
121
- ... *[defined(resource) && railcontent_id in [...(^.parent_content_data[].id)]].resource[]{resource_name, _key, "resource_url": coalesce('${CloudFrontURl}'+string::split(resource_aws.asset->fileURL, '${AWSUrl}')[1], resource_url )},
120
+ ... coalesce(parent_content_reference[]->resource[]{resource_name, _key, "resource_url": coalesce('${CloudFrontURl}'+string::split(resource_aws.asset->fileURL, '${AWSUrl}')[1], resource_url )}, []),
122
121
  ]`
123
122
 
124
123
  export const contentAwardField = "*[references(^._id) && _type == 'content-award'][0]"
@@ -405,7 +404,7 @@ export let contentTypeConfig = {
405
404
  },
406
405
  'progress-tracker': {
407
406
  fields: [
408
- '"parent_content_data": parent_content_data[].id',
407
+ '"parent_content_data": parent_content_reference[]->railcontent_id',
409
408
  `"badge" : ${contentAwardField}.badge.asset->url`,
410
409
  `"badge_rear" : ${contentAwardField}.badge_rear.asset->url`,
411
410
  `"badge_logo" : ${contentAwardField}.logo.asset->url`,
@@ -528,9 +527,9 @@ export let contentTypeConfig = {
528
527
  ],
529
528
  includeChildFields: true,
530
529
  childFields: [
531
- `"parent_data": parent_content_data[0] {
532
- "id": id,
533
- "title": *[railcontent_id == ^.id][0].title,
530
+ `"parent_data": ${parentReferenceField}->{
531
+ "id": railcontent_id,
532
+ title,
534
533
  }`,
535
534
  ],
536
535
  },
package/src/index.d.ts CHANGED
@@ -149,7 +149,8 @@ import {
149
149
  getWeekNumber,
150
150
  isNextDay,
151
151
  isSameDate,
152
- toDayjs
152
+ toDayjs,
153
+ toLocalDay
153
154
  } from './services/dateUtils.js';
154
155
 
155
156
  import {
@@ -217,9 +218,11 @@ import {
217
218
  acceptInvite,
218
219
  createAccount,
219
220
  createInvites,
221
+ fetchInvite,
220
222
  fetchUsersMultiAccountDetails,
221
223
  removeUserFromActiveMultiUserAccount,
222
- rescindInvite
224
+ rescindInvite,
225
+ updateMultiUserAccount
223
226
  } from './services/multi-user-accounts/multi-user-accounts.ts';
224
227
 
225
228
  import {
@@ -550,6 +553,7 @@ declare module 'musora-content-services' {
550
553
  fetchInstructorLessons,
551
554
  fetchInstructors,
552
555
  fetchInterests,
556
+ fetchInvite,
553
557
  fetchLastSubscriptionPlatform,
554
558
  fetchLatestThreads,
555
559
  fetchLearningPathLessons,
@@ -759,6 +763,7 @@ declare module 'musora-content-services' {
759
763
  startOnboarding,
760
764
  status,
761
765
  toDayjs,
766
+ toLocalDay,
762
767
  togglePlaylistPrivate,
763
768
  toggleSignaturePrivate,
764
769
  toggleStudentView,
@@ -779,6 +784,7 @@ declare module 'musora-content-services' {
779
784
  updateDailySession,
780
785
  updateDisplayName,
781
786
  updateForumCategory,
787
+ updateMultiUserAccount,
782
788
  updateNotificationSetting,
783
789
  updateOnboarding,
784
790
  updatePlaylist,
package/src/index.js CHANGED
@@ -153,7 +153,8 @@ import {
153
153
  getWeekNumber,
154
154
  isNextDay,
155
155
  isSameDate,
156
- toDayjs
156
+ toDayjs,
157
+ toLocalDay
157
158
  } from './services/dateUtils.js';
158
159
 
159
160
  import {
@@ -221,9 +222,11 @@ import {
221
222
  acceptInvite,
222
223
  createAccount,
223
224
  createInvites,
225
+ fetchInvite,
224
226
  fetchUsersMultiAccountDetails,
225
227
  removeUserFromActiveMultiUserAccount,
226
- rescindInvite
228
+ rescindInvite,
229
+ updateMultiUserAccount
227
230
  } from './services/multi-user-accounts/multi-user-accounts.ts';
228
231
 
229
232
  import {
@@ -549,6 +552,7 @@ export {
549
552
  fetchInstructorLessons,
550
553
  fetchInstructors,
551
554
  fetchInterests,
555
+ fetchInvite,
552
556
  fetchLastSubscriptionPlatform,
553
557
  fetchLatestThreads,
554
558
  fetchLearningPathLessons,
@@ -758,6 +762,7 @@ export {
758
762
  startOnboarding,
759
763
  status,
760
764
  toDayjs,
765
+ toLocalDay,
761
766
  togglePlaylistPrivate,
762
767
  toggleSignaturePrivate,
763
768
  toggleStudentView,
@@ -778,6 +783,7 @@ export {
778
783
  updateDailySession,
779
784
  updateDisplayName,
780
785
  updateForumCategory,
786
+ updateMultiUserAccount,
781
787
  updateNotificationSetting,
782
788
  updateOnboarding,
783
789
  updatePlaylist,
@@ -2,6 +2,6 @@ export interface RequestOptions {
2
2
  method: string
3
3
  headers: Record<string, string>
4
4
  credentials?: "omit" | "same-origin" | "include"
5
- body?: string
5
+ body?: string | FormData
6
6
  cache?: RequestCache
7
7
  }
@@ -6,6 +6,8 @@
6
6
  /** @typedef {Map} AwardDefinitionsMap */
7
7
  /** @typedef {Map} ContentToAwardsMap */
8
8
 
9
+ import { globalConfig } from '../../config'
10
+
9
11
  const STORAGE_KEY = 'musora_award_definitions_last_fetch'
10
12
 
11
13
  class AwardDefinitionsService {
@@ -173,7 +175,6 @@ class AwardDefinitionsService {
173
175
 
174
176
  async loadLastFetchFromStorage() {
175
177
  try {
176
- const { globalConfig } = await import('../../config')
177
178
  if (!globalConfig.localStorage) {
178
179
  return
179
180
  }
@@ -194,8 +195,7 @@ class AwardDefinitionsService {
194
195
 
195
196
  async saveLastFetchToStorage() {
196
197
  try {
197
- const { globalConfig } = await import('../../config')
198
- if (!globalConfig.localStorage) {
198
+ if (!globalConfig?.localStorage) {
199
199
  return
200
200
  }
201
201
 
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @module GuidedCourses
3
3
  */
4
- import { GET, POST } from '../../infrastructure/http/HttpClient.ts'
4
+ import { GET, POST } from '../../infrastructure/http/HttpClient'
5
5
  import { contentStatusStarted, getProgressState } from '../contentProgress.js'
6
6
  import './playlists-types.js'
7
7
 
@@ -14,7 +14,7 @@ export async function enrollUserInGuidedCourse(guidedCourse, { notifications_ena
14
14
  const response = await POST(url, { notifications_enabled })
15
15
  const state = await getProgressState(guidedCourse)
16
16
  if (!state) {
17
- await contentStatusStarted(guidedCourse)
17
+ await contentStatusStarted(guidedCourse, null, { skipBubbleTrickle: true })
18
18
  }
19
19
  return response
20
20
  }
@@ -416,7 +416,6 @@ export async function recordWatchSession(
416
416
  mediaLengthSeconds,
417
417
  currentSeconds,
418
418
  secondsPlayed,
419
- prevSession = null,
420
419
  instrumentId = null,
421
420
  categoryId = null,
422
421
  isLivestream = false,
@@ -424,32 +423,14 @@ export async function recordWatchSession(
424
423
  contentId = normalizeContentId(contentId)
425
424
  collection = normalizeCollection(collection)
426
425
 
427
- if (!prevSession) {
428
- prevSession = {
429
- pushInterval: null
430
- }
431
- }
432
-
433
426
  // Track practice and progress locally (no immediate push)
434
427
  await Promise.all([
435
428
  trackPractice(contentId, secondsPlayed, { instrumentId, categoryId }),
436
429
  trackProgress(contentId, collection, currentSeconds, mediaLengthSeconds, isLivestream),
437
430
  ])
438
-
439
- if (!prevSession.pushInterval) {
440
- prevSession.pushInterval = setInterval(() => {
441
- flushWatchSession()
442
- }, PUSH_INTERVAL)
443
- }
444
- return prevSession
445
431
  }
446
432
 
447
- export async function flushWatchSession(sessionToFlush = null, shouldClearInterval = true) {
448
- if (shouldClearInterval && sessionToFlush?.pushInterval) {
449
- clearInterval(sessionToFlush.pushInterval)
450
- sessionToFlush.pushInterval = null
451
- }
452
-
433
+ export async function flushWatchSession() {
453
434
  db.contentProgress.requestPushUnsynced('flush-watch-session')
454
435
  db.practices.requestPushUnsynced('flush-watch-session')
455
436
  }
@@ -490,12 +471,14 @@ export async function contentStatusCompletedMany(contentIds, collection = null)
490
471
  )
491
472
  }
492
473
 
493
- export async function contentStatusStarted(contentId, collection = null) {
474
+ // skipBubbleTrickle is only for starting enrolled GC's as a hack to get them into the progress row.
475
+ export async function contentStatusStarted(contentId, collection = null, {skipPush = false, skipBubbleTrickle = false} = {}) {
494
476
  collection = collection ?? {id: COLLECTION_ID_SELF, type: COLLECTION_TYPE.SELF}
495
477
  return setStartedOrCompletedStatus(
496
478
  normalizeContentId(contentId),
497
479
  normalizeCollection(collection),
498
- false
480
+ false,
481
+ {skipPush, skipBubbleTrickle}
499
482
  )
500
483
  }
501
484
  export async function contentStatusReset(contentId, collection = null, {skipPush = false} = {}) {
@@ -510,9 +493,9 @@ async function saveContentProgress(contentId, collection, progress, currentSecon
510
493
 
511
494
  // filter out contentIds that are setting progress lower than existing
512
495
  const contentIdProgress = await getProgressDataByIds([contentId], collection)
513
- const currentProgress = contentIdProgress[contentId].progress;
496
+ const currentProgress = contentIdProgress[contentId].progress
514
497
  if (progress <= currentProgress) {
515
- progress = currentProgress;
498
+ progress = currentProgress
516
499
  }
517
500
 
518
501
  const hierarchy = await getHierarchy(contentId, collection)
@@ -578,7 +561,7 @@ async function saveContentProgress(contentId, collection, progress, currentSecon
578
561
  return response
579
562
  }
580
563
 
581
- async function setStartedOrCompletedStatus(contentId, collection, isCompleted, {skipPush = false} = {}) {
564
+ async function setStartedOrCompletedStatus(contentId, collection, isCompleted, {skipPush = false, skipBubbleTrickle = false} = {}) {
582
565
  const isLP = collection?.type === COLLECTION_TYPE.LEARNING_PATH
583
566
 
584
567
  const hierarchy = await getHierarchy(contentId, collection)
@@ -594,24 +577,25 @@ async function setStartedOrCompletedStatus(contentId, collection, isCompleted, {
594
577
  {skipPush: true}
595
578
  )
596
579
 
597
- let progresses = {
598
- ...trickleProgress(hierarchy, contentId, collection, progress),
599
- ...await bubbleProgress(hierarchy, contentId, collection)
600
- }
580
+ let allProgresses = {}
581
+ allProgresses[contentId] = progress
601
582
 
602
- // have to do this so we dont unnecessarily create a 0% record for each child on set to started/completed
603
- await bubbleAndTrickleProgressesSafely(progresses, collection, metadata, false)
583
+ if (!skipBubbleTrickle) {
584
+ let progresses = {
585
+ ...trickleProgress(hierarchy, contentId, collection, progress),
586
+ ...await bubbleProgress(hierarchy, contentId, collection)
587
+ }
588
+ Object.assign(allProgresses, progresses)
604
589
 
605
- if (isLP) {
606
- let exportProgresses = progresses
607
- exportProgresses[contentId] = progress
608
- await duplicateProgressToALaCarte(exportProgresses, collection, {skipPush: true})
590
+ await bubbleAndTrickleProgressesSafely(progresses, collection, metadata, false)
609
591
  }
610
592
 
611
- if (progress === 100) await onContentCompletedLearningPathActions(contentId, collection)
593
+ if (isLP) {
594
+ await duplicateProgressToALaCarte(allProgresses, collection, {skipPush: true})
595
+ }
612
596
 
613
- for (const [id, progress] of Object.entries(progresses)) {
614
- if (progress === 100) {
597
+ for (const [id, prog] of Object.entries(allProgresses)) {
598
+ if (prog === 100) {
615
599
  await onContentCompletedLearningPathActions(Number(id), collection)
616
600
  }
617
601
  }
@@ -637,6 +621,8 @@ async function setStartedOrCompletedStatusMany(contentIds, collection, isComplet
637
621
  {skipPush: true}
638
622
  )
639
623
 
624
+ let allProgresses = Object.fromEntries(contentIds.map(id => [id, progress]))
625
+
640
626
  let progresses = {}
641
627
  for (const contentId of contentIds) {
642
628
  progresses = {
@@ -645,24 +631,16 @@ async function setStartedOrCompletedStatusMany(contentIds, collection, isComplet
645
631
  ...(await bubbleProgress(hierarchy, contentId, collection)),
646
632
  }
647
633
  }
648
- // have to do this so we dont unnecessarily create a 0% record for each child on set to started/completed
634
+ Object.assign(allProgresses, progresses)
635
+
649
636
  await bubbleAndTrickleProgressesSafely(progresses, collection, metadata, false)
650
637
 
651
638
  if (isLP) {
652
- let exportProgresses = progresses
653
- for (const contentId of contentIds){
654
- exportProgresses[contentId] = progress
655
- }
656
- await duplicateProgressToALaCarte(exportProgresses, collection, {skipPush: true})
639
+ await duplicateProgressToALaCarte(allProgresses, collection, {skipPush: true})
657
640
  }
658
641
 
659
- if (progress === 100) {
660
- for (const contentId of contentIds) {
661
- await onContentCompletedLearningPathActions(contentId, collection)
662
- }
663
- }
664
- for (const [id, progress] of Object.entries(progresses)) {
665
- if (progress === 100) {
642
+ for (const [id, prog] of Object.entries(allProgresses)) {
643
+ if (prog === 100) {
666
644
  await onContentCompletedLearningPathActions(Number(id), collection)
667
645
  }
668
646
  }
@@ -681,17 +659,20 @@ async function resetStatus(contentId, collection = null, {skipPush = false} = {}
681
659
  const hierarchy = await getHierarchy(contentId, collection)
682
660
  const metadata = hierarchy.metadata || {}
683
661
 
662
+ let allProgresses = {}
663
+ allProgresses[contentId] = progress
664
+
684
665
  let progresses = {
685
666
  ...trickleProgress(hierarchy, contentId, collection, progress),
686
667
  ...await bubbleProgress(hierarchy, contentId, collection)
687
668
  }
669
+ Object.assign(allProgresses, progresses)
688
670
 
689
- // have to use different endpoints for erase vs record
690
671
  await bubbleAndTrickleProgressesSafely(progresses, collection, metadata, true)
691
672
 
673
+
692
674
  if (isLP) {
693
- progresses[contentId] = progress
694
- await duplicateProgressToALaCarte(progresses, collection, {skipPush: true})
675
+ await duplicateProgressToALaCarte(allProgresses, collection, {skipPush: true})
695
676
  }
696
677
 
697
678
  if (!skipPush) db.contentProgress.requestPushUnsynced('reset-status')
@@ -31,9 +31,17 @@ export function getMonday(date, timeZone = Intl.DateTimeFormat().resolvedOptions
31
31
  export function getWeekNumber(date) {
32
32
  return dayjs(date).isoWeek()
33
33
  }
34
+
35
+ // Decides if we need to convert or interpret the date based on whether it has timezone info.
36
+ export function toLocalDay(date, timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone) {
37
+ const hasTimezoneInfo = /Z$|[+-]\d{2}:?\d{2}$/.test(String(date))
38
+ // Has timezone info ? convert : interpret
39
+ return hasTimezoneInfo ? dayjs(date).tz(timeZone) : dayjs.tz(date, timeZone)
40
+ }
41
+
34
42
  //Check if two dates are the same
35
43
  export function isSameDate(date1, date2) {
36
- return dayjs(date1).isSame(dayjs(date2), 'day')
44
+ return toLocalDay(date1).isSame(toLocalDay(date2), 'day')
37
45
  }
38
46
 
39
47
  // Check if two dates are consecutive days
@@ -23,7 +23,7 @@ export interface CreatePostParams {
23
23
  * @throws {HttpError} - If the request fails.
24
24
  */
25
25
  export async function createPost(threadId: number, params: CreatePostParams): Promise<ForumPost> {
26
- const { generateForumPostUrl } = await import('../urlBuilder.ts')
26
+ const { generateForumPostUrl } = await import('../urlBuilder')
27
27
 
28
28
  // Generate forum post URL
29
29
  const contentUrl = generateForumPostUrl({
@@ -114,7 +114,7 @@ export async function fetchPosts(
114
114
  * @throws {HttpError} - If the request fails.
115
115
  */
116
116
  export async function likePost(postId: number, brand: string): Promise<void> {
117
- const { generateForumPostUrl } = await import('../urlBuilder.ts')
117
+ const { generateForumPostUrl } = await import('../urlBuilder')
118
118
 
119
119
  // Generate forum post URL
120
120
  const contentUrl = generateForumPostUrl({