musora-content-services 2.119.2 → 2.120.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.
@@ -0,0 +1,19 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(find:*)",
5
+ "Bash(docker exec:*)",
6
+ "Bash(npm test:*)",
7
+ "WebSearch",
8
+ "WebFetch(domain:watermelondb.dev)",
9
+ "WebFetch(domain:github.com)",
10
+ "Bash(git checkout:*)",
11
+ "Bash(npm run doc:*)",
12
+ "Bash(cat:*)",
13
+ "Bash(tr:*)",
14
+ "Bash(npm run build-index:*)"
15
+ ],
16
+ "deny": [],
17
+ "ask": []
18
+ }
19
+ }
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,22 @@
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.120.0](https://github.com/railroadmedia/musora-content-services/compare/v2.119.3...v2.120.0) (2026-01-19)
6
+
7
+
8
+ ### Features
9
+
10
+ * add fetchLiveStreamData method ([aa9b704](https://github.com/railroadmedia/musora-content-services/commit/aa9b7046c7fc69e8b30c0f035577da62f8c5b5fd))
11
+ * add vimeo_live_event_id to GROQ queries ([d4fff6f](https://github.com/railroadmedia/musora-content-services/commit/d4fff6f09ca62682f6cead7e43f6979a56b86e0a))
12
+ * **TP-1060:** method data caching in mcs ([#701](https://github.com/railroadmedia/musora-content-services/issues/701)) ([25efc43](https://github.com/railroadmedia/musora-content-services/commit/25efc43b6c07172db2f20d51d5b79ac9fc204eef))
13
+
14
+ ### [2.119.3](https://github.com/railroadmedia/musora-content-services/compare/v2.119.2...v2.119.3) (2026-01-14)
15
+
16
+
17
+ ### Bug Fixes
18
+
19
+ * remove unsupported collection types ([#713](https://github.com/railroadmedia/musora-content-services/issues/713)) ([0441941](https://github.com/railroadmedia/musora-content-services/commit/04419413ffea82303258b839c8e1e11b3a4c507a))
20
+
5
21
  ### [2.119.2](https://github.com/railroadmedia/musora-content-services/compare/v2.119.1...v2.119.2) (2026-01-14)
6
22
 
7
23
 
package/jest.config.js CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "musora-content-services",
3
- "version": "2.119.2",
3
+ "version": "2.120.0",
4
4
  "description": "A package for Musoras content services ",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -53,7 +53,7 @@ export class Tabs {
53
53
  static Collections = { name: 'Collections', short_name: 'Collections', value: 'type,collections', cardType: 'big' }
54
54
  static ExploreAll = { name: 'Explore All', short_name: 'Explore All', value: 'tab,explore all', icon: 'icon-filters', cardType: 'big'}
55
55
  static All = { name: 'All', short_name: 'All', value: '' }
56
- static Courses = { name: 'Courses', short_name: 'Courses', value: 'type,Courses', recSysSection: 'lesson', }
56
+ static Courses = { name: 'Courses', short_name: 'Courses', value: 'tab,courses', recSysSection: 'lesson', }
57
57
  static SkillLevel = { name: 'Skill Level', short_name: 'SKILL LEVEL', is_group_by: true, value: 'difficulty_string' }
58
58
  static Genres = { name: 'Genres', short_name: 'Genres', is_group_by: true, value: 'genre' }
59
59
  static Completed = {
@@ -229,7 +229,7 @@ export const lessonTypesMapping = {
229
229
  performances: performancesLessonTypes,
230
230
  'student archives': studentArchivesLessonTypes,
231
231
  documentaries: ['documentary-lesson'],
232
- courses: ['course', 'course-collection'],
232
+ courses: ['course'],
233
233
  'guided courses': ['guided-course'],
234
234
  'course collections': ['course-collection'],
235
235
  'skill packs': ['skill-pack'],
package/src/index.d.ts CHANGED
@@ -226,6 +226,7 @@ import {
226
226
  fetchComments,
227
227
  fetchContentPageUserData,
228
228
  fetchLikeCount,
229
+ fetchLiveStreamData,
229
230
  fetchRecentUserActivities,
230
231
  fetchTopComment,
231
232
  fetchUserPermissionsData,
@@ -415,6 +416,7 @@ import {
415
416
  getPracticeNotes,
416
417
  getPracticeSessions,
417
418
  getRecentActivity,
419
+ getStreaksAndMessage,
418
420
  getUserMonthlyStats,
419
421
  getUserWeeklyStats,
420
422
  recordUserActivity,
@@ -523,6 +525,7 @@ declare module 'musora-content-services' {
523
525
  fetchLikeCount,
524
526
  fetchLiveEvent,
525
527
  fetchLiveEventPollingState,
528
+ fetchLiveStreamData,
526
529
  fetchMemberships,
527
530
  fetchMetadata,
528
531
  fetchMethodV2IntroVideo,
@@ -623,6 +626,7 @@ declare module 'musora-content-services' {
623
626
  getSongTypesFor,
624
627
  getSortOrder,
625
628
  getStartedOrCompletedProgressOnly,
629
+ getStreaksAndMessage,
626
630
  getTabResults,
627
631
  getTimeRemainingUntilLocal,
628
632
  getToday,
package/src/index.js CHANGED
@@ -230,6 +230,7 @@ import {
230
230
  fetchComments,
231
231
  fetchContentPageUserData,
232
232
  fetchLikeCount,
233
+ fetchLiveStreamData,
233
234
  fetchRecentUserActivities,
234
235
  fetchTopComment,
235
236
  fetchUserPermissionsData,
@@ -419,6 +420,7 @@ import {
419
420
  getPracticeNotes,
420
421
  getPracticeSessions,
421
422
  getRecentActivity,
423
+ getStreaksAndMessage,
422
424
  getUserMonthlyStats,
423
425
  getUserWeeklyStats,
424
426
  recordUserActivity,
@@ -522,6 +524,7 @@ export {
522
524
  fetchLikeCount,
523
525
  fetchLiveEvent,
524
526
  fetchLiveEventPollingState,
527
+ fetchLiveStreamData,
525
528
  fetchMemberships,
526
529
  fetchMetadata,
527
530
  fetchMethodV2IntroVideo,
@@ -622,6 +625,7 @@ export {
622
625
  getSongTypesFor,
623
626
  getSortOrder,
624
627
  getStartedOrCompletedProgressOnly,
628
+ getStreaksAndMessage,
625
629
  getTabResults,
626
630
  getTimeRemainingUntilLocal,
627
631
  getToday,
@@ -7,6 +7,11 @@ import { DefaultHeaderProvider } from './providers/DefaultHeaderProvider'
7
7
  import { FetchRequestExecutor } from './executors/FetchRequestExecutor'
8
8
  import { globalConfig } from '../../services/config'
9
9
 
10
+ type _RequestOptions = {
11
+ dataVersion?: string | null
12
+ cache?: RequestCache
13
+ }
14
+
10
15
  export class HttpClient {
11
16
  private baseUrl: string
12
17
  private token: string | null
@@ -33,38 +38,38 @@ export class HttpClient {
33
38
  this.token = null;
34
39
  }
35
40
 
36
- public async get<T>(url: string, dataVersion: string | null = null): Promise<T> {
37
- return this.request<T>(url, 'GET', dataVersion)
41
+ public async get<T>(url: string, options: _RequestOptions = {}): Promise<T> {
42
+ return this.request<T>(url, 'GET', options)
38
43
  }
39
44
 
40
- public async post<T>(url: string, data: any, dataVersion: string | null = null): Promise<T> {
41
- return this.request<T>(url, 'POST', dataVersion, data)
45
+ public async post<T>(url: string, data: any, options: _RequestOptions = {}): Promise<T> {
46
+ return this.request<T>(url, 'POST', options, data)
42
47
  }
43
48
 
44
- public async put<T>(url: string, data: any, dataVersion: string | null = null): Promise<T> {
45
- return this.request<T>(url, 'PUT', dataVersion, data)
49
+ public async put<T>(url: string, data: any, options: _RequestOptions = {}): Promise<T> {
50
+ return this.request<T>(url, 'PUT', options, data)
46
51
  }
47
52
 
48
- public async patch<T>(url: string, data: any, dataVersion: string | null = null): Promise<T> {
49
- return this.request<T>(url, 'PATCH', dataVersion, data)
53
+ public async patch<T>(url: string, data: any, options: _RequestOptions = {}): Promise<T> {
54
+ return this.request<T>(url, 'PATCH', options, data)
50
55
  }
51
56
 
52
- public async delete<T>(url: string, data: any = null, dataVersion: string | null = null): Promise<T> {
53
- return this.request<T>(url, 'DELETE', dataVersion, data)
57
+ public async delete<T>(url: string, data: any = null, options: _RequestOptions = {}): Promise<T> {
58
+ return this.request<T>(url, 'DELETE', options, data)
54
59
  }
55
60
 
56
61
  private async request<T>(
57
62
  url: string,
58
63
  method: string,
59
- dataVersion: string | null = null,
64
+ options: _RequestOptions = {},
60
65
  body: any = null
61
66
  ): Promise<T> {
62
67
  try {
63
- const headers = this.buildHeaders(dataVersion)
64
- const options = this.buildRequestOptions(method, headers, body)
68
+ const headers = this.buildHeaders(options.dataVersion)
69
+ const requestOptions = this.buildRequestOptions(method, headers, body, options.cache)
65
70
  const fullUrl = this.resolveUrl(url)
66
71
 
67
- return await this.requestExecutor.execute<T>(fullUrl, options)
72
+ return await this.requestExecutor.execute<T>(fullUrl, requestOptions)
68
73
  } catch (error: any) {
69
74
  return this.handleError(error, url, method)
70
75
  }
@@ -90,7 +95,8 @@ export class HttpClient {
90
95
  private buildRequestOptions(
91
96
  method: string,
92
97
  headers: Record<string, string>,
93
- body: any
98
+ body: any,
99
+ cache?: RequestCache | null
94
100
  ): RequestOptions {
95
101
  const options: RequestOptions = {
96
102
  method,
@@ -98,6 +104,10 @@ export class HttpClient {
98
104
  credentials: 'include',
99
105
  }
100
106
 
107
+ if (cache) {
108
+ options.cache = cache
109
+ }
110
+
101
111
  if (body) {
102
112
  const isFormData = typeof FormData !== 'undefined' && body instanceof FormData
103
113
  if (isFormData) {
@@ -3,4 +3,5 @@ export interface RequestOptions {
3
3
  headers: Record<string, string>
4
4
  credentials?: "omit" | "same-origin" | "include"
5
5
  body?: string
6
+ cache?: RequestCache
6
7
  }
@@ -16,11 +16,13 @@ import { COLLECTION_TYPE, STATE } from '../sync/models/ContentProgress'
16
16
  import { SyncWriteDTO } from '../sync'
17
17
  import { ContentProgress } from '../sync/models'
18
18
  import { CollectionParameter } from '../sync/repositories/content-progress'
19
- import { getToday } from "../dateUtils.js";
19
+ import dayjs from 'dayjs'
20
20
 
21
21
  const BASE_PATH: string = `/api/content-org`
22
22
  const LEARNING_PATHS_PATH = `${BASE_PATH}/v1/user/learning-paths`
23
23
  const LEARNING_PATH_LESSON = 'learning-path-lesson-v2'
24
+ let dailySessionPromise: Promise<DailySessionResponse> | null = null
25
+ let activePathPromise: Promise<ActiveLearningPathResponse> | null = null
24
26
 
25
27
  interface ActiveLearningPathResponse {
26
28
  user_id: number
@@ -51,17 +53,20 @@ interface CollectionObject {
51
53
  * Gets today's daily session for the user.
52
54
  * If the daily session doesn't exist, it will be created.
53
55
  * @param brand
54
- * @param userDate
56
+ * @param userDate - local datetime. must have date and time - format 2025-10-31T13:45:00
57
+ * @param forceRefresh - force cache refresh
55
58
  */
56
- export async function getDailySession(brand: string, userDate: Date) {
57
- const stringDate = userDate.toISOString().split('T')[0]
58
- const url: string = `${LEARNING_PATHS_PATH}/daily-session/get?brand=${brand}&userDate=${stringDate}`
59
+ export async function getDailySession(brand: string, userDate: Date, forceRefresh: boolean = false) {
60
+ const dateWithTimezone = formatLocalDateTime(userDate)
61
+ const url: string = `${LEARNING_PATHS_PATH}/daily-session/get?brand=${brand}&userDate=${encodeURIComponent(dateWithTimezone)}`
59
62
  try {
60
- const response = await GET(url)
63
+ const response = await dataPromiseGET(url, forceRefresh) as DailySessionResponse
61
64
  if (!response) {
62
65
  return await updateDailySession(brand, userDate, false)
63
66
  }
67
+ dailySessionPromise = null // reset promise after successful fetch
64
68
  return response as DailySessionResponse
69
+
65
70
  } catch (error: any) {
66
71
  if (error.status === 204) {
67
72
  return await updateDailySession(brand, userDate, false)
@@ -81,20 +86,38 @@ export async function updateDailySession(
81
86
  userDate: Date,
82
87
  keepFirstLearningPath: boolean = false
83
88
  ) {
84
- const stringDate = userDate.toISOString().split('T')[0]
89
+ const dateWithTimezone = formatLocalDateTime(userDate)
85
90
  const url: string = `${LEARNING_PATHS_PATH}/daily-session/create`
86
- const body = { brand: brand, userDate: stringDate, keepFirstLearningPath: keepFirstLearningPath }
87
- return (await POST(url, body)) as DailySessionResponse
91
+ const body = { brand: brand, userDate: dateWithTimezone, keepFirstLearningPath: keepFirstLearningPath }
92
+
93
+ const response = await POST(url, body) as DailySessionResponse
94
+
95
+ if (response) {
96
+ const urlGet: string = `${LEARNING_PATHS_PATH}/daily-session/get?brand=${brand}&userDate=${encodeURIComponent(dateWithTimezone)}`
97
+ dataPromiseGET(urlGet, true) // refresh cache
98
+ }
99
+
100
+ return response
101
+ }
102
+
103
+ function formatLocalDateTime(date: Date): string {
104
+ return dayjs(date).format('YYYY-MM-DD Z')
88
105
  }
89
106
 
90
107
  /**
91
108
  * Gets user's active learning path.
92
109
  * @param brand
110
+ * @param forceRefresh - force cache refresh
93
111
  */
94
- export async function getActivePath(brand: string) {
112
+ export async function getActivePath(brand: string, forceRefresh: boolean = false) {
95
113
  const url: string = `${LEARNING_PATHS_PATH}/active-path/get?brand=${brand}`
96
- return (await GET(url)) as ActiveLearningPathResponse
114
+
115
+ const response = await dataPromiseGET(url, forceRefresh) as ActiveLearningPathResponse
116
+ activePathPromise = null // reset promise after successful fetch
117
+
118
+ return response
97
119
  }
120
+
98
121
  /**
99
122
  * Sets a new learning path as the user's active learning path.
100
123
  * @param brand
@@ -103,7 +126,31 @@ export async function getActivePath(brand: string) {
103
126
  export async function startLearningPath(brand: string, learningPathId: number) {
104
127
  const url: string = `${LEARNING_PATHS_PATH}/active-path/set`
105
128
  const body = { brand: brand, learning_path_id: learningPathId }
106
- return (await POST(url, body)) as ActiveLearningPathResponse
129
+
130
+ const response = await POST(url, body) as ActiveLearningPathResponse
131
+
132
+ // manual BE call to avoid recursive POST<->GET calls
133
+ if (response) {
134
+ const urlGet: string = `${LEARNING_PATHS_PATH}/active-path/get?brand=${brand}`
135
+ dataPromiseGET(urlGet, true) // refresh cache
136
+ }
137
+
138
+ return response
139
+ }
140
+
141
+ async function dataPromiseGET(url: string, forceRefresh: boolean): Promise<DailySessionResponse|ActiveLearningPathResponse> {
142
+ if (url.includes('daily-session')) {
143
+ if (!dailySessionPromise || forceRefresh) {
144
+ dailySessionPromise = GET(url, {cache: forceRefresh ? 'reload' : 'default'}) as Promise<DailySessionResponse>
145
+ }
146
+ return dailySessionPromise
147
+
148
+ } else if (url.includes('active-path')) {
149
+ if (!activePathPromise || forceRefresh) {
150
+ activePathPromise = GET(url, {cache: forceRefresh ? 'reload' : 'default'}) as Promise<ActiveLearningPathResponse>
151
+ }
152
+ return activePathPromise
153
+ }
107
154
  }
108
155
 
109
156
  /**
@@ -374,7 +421,7 @@ export async function completeMethodIntroVideo(
374
421
  const methodStructure = await fetchMethodV2Structure(brand)
375
422
 
376
423
  const firstLearningPathId = methodStructure.learning_paths[0].id
377
- response.active_path_response = await methodIntroVideoCompleteActions(brand, firstLearningPathId, getToday())
424
+ response.active_path_response = await methodIntroVideoCompleteActions(brand, firstLearningPathId, new Date())
378
425
 
379
426
  response.intro_video_response = await completeIfNotCompleted(introVideoId)
380
427
 
@@ -428,7 +475,7 @@ export async function completeLearningPathIntroVideo(
428
475
  if (activePath.active_learning_path_id === learningPathId) {
429
476
  response.update_dailies_response = await updateDailySession(
430
477
  brand,
431
- getToday(),
478
+ new Date(),
432
479
  true
433
480
  )
434
481
  }
@@ -4,7 +4,6 @@ import { COLLECTION_TYPE, STATE } from './sync/models/ContentProgress'
4
4
  import { trackUserPractice, findIncompleteLesson } from './userActivity'
5
5
  import { getNextLessonLessonParentTypes } from '../contentTypeConfig.js'
6
6
  import {getDailySession, onContentCompletedLearningPathActions} from "./content-org/learning-paths.ts";
7
- import {getToday} from "./dateUtils.js";
8
7
  import { fetchBrandsByContentIds } from './sanity.js'
9
8
 
10
9
  const STATE_STARTED = STATE.STARTED
@@ -37,7 +36,7 @@ export async function getNavigateToForMethod(data) {
37
36
  let navigateToData = {}
38
37
 
39
38
  const brand = data[0].content.brand || null
40
- const dailySessionResponse = await getDailySession(brand, getToday())
39
+ const dailySessionResponse = await getDailySession(brand, new Date())
41
40
  const dailySession = dailySessionResponse?.daily_session || null
42
41
  const activeLearningPathId = dailySessionResponse?.active_learning_path_id || null
43
42
 
@@ -3,7 +3,6 @@
3
3
  */
4
4
 
5
5
  import { getActivePath, fetchLearningPathLessons } from '../../content-org/learning-paths'
6
- import { getToday } from '../../dateUtils.js'
7
6
  import { fetchMethodV2IntroVideo } from '../../sanity'
8
7
  import { getProgressState } from '../../contentProgress'
9
8
  import {COLLECTION_TYPE, STATE} from '../../sync/models/ContentProgress'
@@ -45,7 +44,7 @@ export async function getMethodCard(brand) {
45
44
  const learningPath = await fetchLearningPathLessons(
46
45
  activeLearningPath.active_learning_path_id,
47
46
  brand,
48
- getToday()
47
+ new Date()
49
48
  )
50
49
 
51
50
  if (!learningPath) {
@@ -37,6 +37,15 @@ export async function fetchLikeCount(contendId) {
37
37
  return await GET(url)
38
38
  }
39
39
 
40
+ /**
41
+ * @param {number} contentId
42
+ * @returns {Promise<{hls_url: string|null, status: string, vimeo_event_id: string|null}>}
43
+ */
44
+ export async function fetchLiveStreamData(contentId) {
45
+ const url = `/api/content/v1/live-events/${contentId}/stream`
46
+ return await GET(url)
47
+ }
48
+
40
49
  export async function postPlaylistContentEngaged(playlistItemId) {
41
50
  const url = `/railtracker/v1/last-engaged/${playlistItemId}`
42
51
  return await POST(url, null)
@@ -964,6 +964,7 @@ export async function fetchLessonContent(railContentId, { addParent = false } =
964
964
  "live_event_start_time": live_event_start_time,
965
965
  "live_event_end_time": live_event_end_time,
966
966
  "live_event_stream_id": live_event_stream_id,
967
+ "vimeo_live_event_id": vimeo_live_event_id,
967
968
  "videoId": coalesce(live_event_stream_id, video.external_id),
968
969
  "live_event_is_global": live_global_event == true
969
970
  }
@@ -1194,6 +1195,7 @@ export async function fetchLiveEvent(brand, forcedContentId = null) {
1194
1195
  live_event_start_time,
1195
1196
  live_event_end_time,
1196
1197
  live_event_stream_id,
1198
+ vimeo_live_event_id,
1197
1199
  railcontent_id,
1198
1200
  published_on,
1199
1201
  'event_coach_url' : instructor[0]->web_url_path,
@@ -1211,6 +1213,7 @@ export async function fetchLiveEvent(brand, forcedContentId = null) {
1211
1213
  live_event_start_time,
1212
1214
  live_event_end_time,
1213
1215
  live_event_stream_id,
1216
+ vimeo_live_event_id,
1214
1217
  railcontent_id,
1215
1218
  published_on,
1216
1219
  'event_coach_url' : instructor[0]->web_url_path,
@@ -10,9 +10,7 @@ import {
10
10
 
11
11
  export enum COLLECTION_TYPE {
12
12
  SELF = 'self',
13
- GUIDED_COURSE = 'guided-course',
14
13
  LEARNING_PATH = 'learning-path-v2',
15
- PLAYLIST = 'playlist',
16
14
  }
17
15
  export const COLLECTION_ID_SELF = 0
18
16
 
File without changes
File without changes
package/test/log.js CHANGED
File without changes