musora-content-services 2.92.7 → 2.93.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -39,14 +39,6 @@ jobs:
39
39
  with:
40
40
  node-version: '20'
41
41
 
42
- - name: Install dependencies for v1
43
- working-directory: main-content
44
- run: npm ci
45
-
46
- - name: Generate v1 documentation
47
- working-directory: main-content
48
- run: npm run doc
49
-
50
42
  - name: Install dependencies for v2
51
43
  working-directory: v2-content
52
44
  run: npm ci
@@ -58,8 +50,11 @@ jobs:
58
50
  - name: Combine v1 and v2 docs into deployment structure
59
51
  run: |
60
52
  mkdir -p _site
61
- # Copy v1 docs to root
62
- cp -r main-content/docs/* _site/
53
+ # Copy existing v1 docs from main branch (already committed)
54
+ if [ -d "main-content/docs" ]; then
55
+ cp -r main-content/docs/* _site/
56
+ echo "✅ Copied existing v1 docs from main branch"
57
+ fi
63
58
  # Copy v2 docs to /v2/ subdirectory
64
59
  mkdir -p _site/v2
65
60
  cp -r v2-content/docs/* _site/v2/
package/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
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.93.1](https://github.com/railroadmedia/musora-content-services/compare/v2.93.0...v2.93.1) (2025-12-02)
6
+
7
+ ## [2.93.0](https://github.com/railroadmedia/musora-content-services/compare/v2.92.7...v2.93.0) (2025-12-02)
8
+
9
+
10
+ ### Features
11
+
12
+ * **BEH-1192:** revise fetchLearningPathProgressCheckLessons ([#604](https://github.com/railroadmedia/musora-content-services/issues/604)) ([db8cb70](https://github.com/railroadmedia/musora-content-services/commit/db8cb70ed70f4341fdd4651cdd3fea2b8047dc21))
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+ * method progress card subtitle ([#608](https://github.com/railroadmedia/musora-content-services/issues/608)) ([3bbfb56](https://github.com/railroadmedia/musora-content-services/commit/3bbfb56fce08e0753e7708bdf2d1356000b1c398))
18
+ * Pull in permission_v2 ([#602](https://github.com/railroadmedia/musora-content-services/issues/602)) ([289e32d](https://github.com/railroadmedia/musora-content-services/commit/289e32d08bd3a6448191b9f6034801e4cf8d40e5))
19
+
5
20
  ### [2.92.7](https://github.com/railroadmedia/musora-content-services/compare/v2.92.6...v2.92.7) (2025-12-02)
6
21
 
7
22
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "musora-content-services",
3
- "version": "2.92.7",
3
+ "version": "2.93.1",
4
4
  "description": "A package for Musoras content services ",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -61,7 +61,7 @@ export const DEFAULT_FIELDS = [
61
61
  `'genre': ${genreField}`,
62
62
  'status',
63
63
  "'slug' : slug.current",
64
- "'permission_id': permission[]->railcontent_id",
64
+ "'permission_id': permission_v2",
65
65
  'child_count',
66
66
  '"parent_id": parent_content_data[0].id',
67
67
  ]
@@ -84,7 +84,7 @@ export const DEFAULT_CHILD_FIELDS = [
84
84
  `'genre': ${genreField}`,
85
85
  'status',
86
86
  "'slug' : slug.current",
87
- "'permission_id': permission[]->railcontent_id",
87
+ "'permission_id': permission_v2",
88
88
  'child_count',
89
89
  '"parent_id": parent_content_data[0].id',
90
90
  ]
@@ -11,6 +11,7 @@ import {
11
11
  getAllCompletedByIds,
12
12
  getProgressState,
13
13
  } from '../contentProgress.js'
14
+ import { STATE } from '../sync/models/ContentProgress'
14
15
 
15
16
  const BASE_PATH: string = `/api/content-org`
16
17
  const LEARNING_PATHS_PATH = `${BASE_PATH}/v1/user/learning-paths`
@@ -225,29 +226,18 @@ export async function fetchLearningPathLessons(
225
226
  * For an array of contentIds, fetch any content progress with state=completed,
226
227
  * including other learning paths and a la carte progress.
227
228
  *
228
- * @param contentIds The array of content IDs within the learning path
229
- * @param learningPathId The learning path ID
230
- * @returns {Promise<Object>} Response object
231
- * @returns {Array} result.lessons - Array of all learning path lesson contentIds.
232
- * @returns {Array} result.completed_lessons - Array of learning path lesson contentIds that are completed.
233
- * @returns {Array} result.lessons_count - Count of learning path lessons.
234
- * @returns {Array} result.completed_lessons_count - Count of learning path completed lessons.
229
+ * @param {number[]} contentIds The array of content IDs within the learning path
230
+ * @returns {Promise<Object>} Object with content IDs as keys and the progress state as values
235
231
  */
236
- export async function fetchLearningPathProgressCheckLessons(
237
- contentIds: number[],
238
- learningPathId: number
239
- ): Promise<object> {
240
- let query = await getAllCompletedByIds(contentIds, {
241
- id: learningPathId,
242
- type: 'learning-path-v2',
243
- })
244
- let completedContentIds = query.data
245
- return {
246
- lessons: contentIds,
247
- completed_lessons: completedContentIds,
248
- lessons_count: contentIds.length,
249
- completed_lessons_count: completedContentIds.length,
250
- }
232
+ export async function fetchLearningPathProgressCheckLessons(contentIds: number[]): Promise<Object> {
233
+ let query = await getAllCompletedByIds(contentIds)
234
+ let completedLessons = query.data.map(lesson => lesson.content_id)
235
+
236
+ return contentIds.reduce((obj, contentId) => {
237
+ let lessonIsCompleted = completedLessons.includes(contentId)
238
+ obj[contentId] = lessonIsCompleted ? STATE.COMPLETED : ""
239
+ return obj
240
+ }, {})
251
241
  }
252
242
 
253
243
  interface completeMethodIntroVideo {
@@ -162,18 +162,8 @@ export async function getAllCompleted(limit = null) {
162
162
  return db.contentProgress.completedIds(limit).then(r => r.data.map(id => parseInt(id)))
163
163
  }
164
164
 
165
- /**
166
- *
167
- * @param {array} contentIds List of content children within learning path
168
- * @param {object} collection Learning path object
169
- * @returns {Promise<array>} Filtered list of contentIds that are completed
170
- */
171
- export async function getAllCompletedByIds(contentIds, collection) {
172
- // TODO - implement collection filtering
173
- return db.contentProgress.queryAllIds(
174
- Q.whereIn('content_id', contentIds),
175
- Q.where('state', STATE_COMPLETED)
176
- )
165
+ export async function getAllCompletedByIds(contentIds) {
166
+ return db.contentProgress.completedByContentIds(contentIds)
177
167
  }
178
168
 
179
169
  export async function getAllStartedOrCompleted({
@@ -51,9 +51,7 @@ export class PermissionsV2Adapter extends PermissionsAdapter {
51
51
  }
52
52
 
53
53
  // Get content's required permissions
54
- const oldPermissions = content?.permission_id ?? []
55
- const newPermissions = content?.permission_v2 ?? []
56
- const contentPermissions = new Set([...oldPermissions, ...newPermissions])
54
+ const contentPermissions = new Set(content?.permission_id ?? [])
57
55
 
58
56
  // Content with no permissions is accessible to all
59
57
  if (contentPermissions.size === 0) {
@@ -2,16 +2,9 @@
2
2
  * @module ProgressRow
3
3
  */
4
4
 
5
- import {
6
- getDailySession,
7
- getActivePath,
8
- resetAllLearningPaths,
9
- startLearningPath,
10
- fetchLearningPathLessons,
11
- } from '../content-org/learning-paths'
5
+ import { getActivePath, fetchLearningPathLessons } from '../content-org/learning-paths'
12
6
  import { getToday } from '../dateUtils.js'
13
- import { fetchByRailContentId, fetchByRailContentIds, fetchMethodV2IntroVideo } from '../sanity'
14
- import { addContextToContent } from '../contentAggregator.js'
7
+ import { fetchMethodV2IntroVideo } from '../sanity'
15
8
  import { getProgressState } from '../contentProgress'
16
9
 
17
10
  export async function getMethodCard(brand) {
@@ -21,6 +14,10 @@ export async function getMethodCard(brand) {
21
14
  if (introVideoProgressState !== 'completed') {
22
15
  //startLearningPath('drumeo', 422533)
23
16
  const timestamp = Math.floor(Date.now() / 1000)
17
+ const instructorText =
18
+ introVideo.instructor?.length > 1
19
+ ? 'Multiple Instructors'
20
+ : introVideo.instructor?.[0]?.name || ''
24
21
  return {
25
22
  id: 1, // method card has no id
26
23
  type: 'method',
@@ -29,7 +26,7 @@ export async function getMethodCard(brand) {
29
26
  body: {
30
27
  thumbnail: introVideo.thumbnail,
31
28
  title: introVideo.title,
32
- subtitle: `${introVideo.difficulty_string} • ${introVideo.instructor?.[0]?.name}`,
29
+ subtitle: `${introVideo.difficulty_string} • ${instructorText}`,
33
30
  },
34
31
  cta: {
35
32
  text: 'Get Started',
@@ -59,9 +56,7 @@ export async function getMethodCard(brand) {
59
56
  const nextIncompleteLesson = learningPath?.todays_lessons.find(
60
57
  (lesson) => lesson.progressStatus !== 'completed'
61
58
  )
62
- let ctaText,
63
- action,
64
- nextLesson = null
59
+ let ctaText, action
65
60
  if (noneCompleted) {
66
61
  ctaText = 'Start Session'
67
62
  action = getMethodActionCTA(nextIncompleteLesson)
@@ -219,7 +219,7 @@ export async function fetchRelatedSongs(brand, songId) {
219
219
  "published_on": published_on,
220
220
  status,
221
221
  "image": thumbnail.asset->url,
222
- "permission_id": permission[]->railcontent_id,
222
+ "permission_id": permission_v2,
223
223
  "fields": [
224
224
  {
225
225
  "key": "title",
@@ -245,7 +245,7 @@ export async function fetchRelatedSongs(brand, songId) {
245
245
  "id": railcontent_id,
246
246
  "url": web_url_path,
247
247
  "published_on": published_on,
248
- "permission_id": permission[]->railcontent_id,
248
+ "permission_id": permission_v2,
249
249
  status,
250
250
  "fields": [
251
251
  {
@@ -306,7 +306,7 @@ export async function fetchNewReleases(
306
306
  published_on,
307
307
  "type": _type,
308
308
  web_url_path,
309
- "permission_id": permission[]->railcontent_id,
309
+ "permission_id": permission_v2,
310
310
  `
311
311
  const query = buildRawQuery(filter, fields, { sortOrder: sortOrder, start, end: end })
312
312
  return fetchSanity(query, true)
@@ -343,7 +343,7 @@ export async function fetchUpcomingEvents(brand, { page = 1, limit = 10 } = {})
343
343
  published_on,
344
344
  "type": _type,
345
345
  web_url_path,
346
- "permission_id": permission[]->railcontent_id,
346
+ "permission_id": permission_v2,
347
347
  live_event_start_time,
348
348
  live_event_end_time,
349
349
  "isLive": live_event_start_time <= '${now}' && live_event_end_time >= '${now}'`
@@ -395,7 +395,7 @@ export async function fetchScheduledReleases(brand, { page = 1, limit = 10 }) {
395
395
  published_on,
396
396
  "type": _type,
397
397
  web_url_path,
398
- "permission_id": permission[]->railcontent_id,
398
+ "permission_id": permission_v2,
399
399
  } | order(published_on asc)[${start}...${end}]`
400
400
  return fetchSanity(query, true)
401
401
  }
@@ -911,7 +911,7 @@ export async function fetchMethod(brand, slug) {
911
911
  "url": *[railcontent_id == ^.id][0].web_url_path
912
912
  } | order(length(url)),
913
913
  "type": _type,
914
- "permission_id": permission[]->railcontent_id,
914
+ "permission_id": permission_v2,
915
915
  "levels": child[${childrenFilter}]->
916
916
  {
917
917
  "id": railcontent_id,
@@ -1128,7 +1128,7 @@ export async function fetchLessonContent(railContentId, { addParent = false } =
1128
1128
  mp3_no_drums_yes_click_url,
1129
1129
  mp3_yes_drums_no_click_url,
1130
1130
  mp3_yes_drums_yes_click_url,
1131
- "permission_id": permission[]->railcontent_id,
1131
+ "permission_id": permission_v2,
1132
1132
  ${parentQuery}
1133
1133
  ...select(
1134
1134
  defined(live_event_start_time) => {
@@ -1266,7 +1266,7 @@ export async function fetchSiblingContent(railContentId, brand = null) {
1266
1266
  }).buildFilter()
1267
1267
 
1268
1268
  const brandString = brand ? ` && brand == "${brand}"` : ''
1269
- const queryFields = `_id, "id":railcontent_id, published_on, "instructor": instructor[0]->name, title, "thumbnail":thumbnail.asset->url, length_in_seconds, status, "type": _type, difficulty, difficulty_string, artist->, "permission_id": permission[]->railcontent_id, "genre": genre[]->name, "parent_id": parent_content_data[0].id`
1269
+ const queryFields = `_id, "id":railcontent_id, published_on, "instructor": instructor[0]->name, title, "thumbnail":thumbnail.asset->url, length_in_seconds, status, "type": _type, difficulty, difficulty_string, artist->, "permission_id": permission_v2, "genre": genre[]->name, "parent_id": parent_content_data[0].id`
1270
1270
 
1271
1271
  const query = `*[railcontent_id == ${railContentId}${brandString}]{
1272
1272
  _type, parent_type, 'parent_id': parent_content_data[0].id, railcontent_id,
@@ -1313,7 +1313,7 @@ export async function fetchRelatedLessons(railContentId) {
1313
1313
  { showMembershipRestrictedContent: true }
1314
1314
  ).buildFilter()
1315
1315
 
1316
- const queryFields = `_id, "id":railcontent_id, published_on, "instructor": instructor[0]->name, title, "thumbnail":thumbnail.asset->url, length_in_seconds, status, "type": _type, difficulty, difficulty_string, railcontent_id, artist->,"permission_id": permission[]->railcontent_id,_type, "genre": genre[]->name`
1316
+ const queryFields = `_id, "id":railcontent_id, published_on, "instructor": instructor[0]->name, title, "thumbnail":thumbnail.asset->url, length_in_seconds, status, "type": _type, difficulty, difficulty_string, railcontent_id, artist->,"permission_id": permission_v2,_type, "genre": genre[]->name`
1317
1317
 
1318
1318
  const query = `*[railcontent_id == ${railContentId} && (!defined(permission) || references(*[_type=='permission']._id))]{
1319
1319
  _type, parent_type, railcontent_id,
@@ -2134,7 +2134,7 @@ export async function fetchScheduledAndNewReleases(
2134
2134
  published_on,
2135
2135
  "type": _type,
2136
2136
  show_in_new_feed,
2137
- "permission_id": permission[]->railcontent_id,
2137
+ "permission_id": permission_v2,
2138
2138
  "isLive": live_event_start_time <= '${now}' && live_event_end_time >= '${now}',
2139
2139
  }`
2140
2140
 
@@ -29,7 +29,7 @@ export function inBoundary<T, TContext extends Record<string, any>>(fn: (context
29
29
  if (result instanceof Promise) {
30
30
  return result.catch((err: unknown) => {
31
31
  const wrapped = err instanceof SyncError ? err : new SyncUnexpectedError((err as Error).message, context);
32
- SyncTelemetry.getInstance().capture(wrapped)
32
+ SyncTelemetry.getInstance()?.capture(wrapped)
33
33
 
34
34
  throw wrapped;
35
35
  });
@@ -38,7 +38,7 @@ export function inBoundary<T, TContext extends Record<string, any>>(fn: (context
38
38
  return result;
39
39
  } catch (err: unknown) {
40
40
  const wrapped = err instanceof SyncError ? err : new SyncUnexpectedError((err as Error).message, context);
41
- SyncTelemetry.getInstance().capture(wrapped);
41
+ SyncTelemetry.getInstance()?.capture(wrapped);
42
42
 
43
43
  throw wrapped;
44
44
  }
@@ -23,19 +23,19 @@ interface RawPushResponse {
23
23
  }
24
24
 
25
25
  export type SyncResponse = SyncPushResponse | SyncPullResponse
26
+ export type SyncPushResponse = SyncPushSuccessResponse | SyncPushFetchFailureResponse | SyncPushFailureResponse
26
27
 
27
- export type SyncPushResponse = SyncPushSuccessResponse | SyncPushFailureResponse
28
-
29
- type SyncPushSuccessResponse = SyncPushResponseBase & {
28
+ type SyncPushSuccessResponse = SyncResponseBase & {
30
29
  ok: true
31
30
  results: SyncStorePushResult[]
32
31
  }
33
- type SyncPushFailureResponse = SyncPushResponseBase & {
32
+ type SyncPushFetchFailureResponse = SyncResponseBase & {
34
33
  ok: false,
35
- originalError: Error
34
+ isRetryable: boolean
36
35
  }
37
- interface SyncPushResponseBase extends SyncResponseBase {
38
-
36
+ type SyncPushFailureResponse = SyncResponseBase & {
37
+ ok: false,
38
+ originalError: Error
39
39
  }
40
40
 
41
41
  type SyncStorePushResult<TRecordKey extends string = 'id'> = SyncStorePushResultSuccess<TRecordKey> | SyncStorePushResultFailure<TRecordKey>
@@ -61,20 +61,21 @@ interface SyncStorePushResultBase {
61
61
  type: 'success' | 'failure'
62
62
  }
63
63
 
64
- export type SyncPullResponse = SyncPullSuccessResponse | SyncPullFailureResponse
64
+ export type SyncPullResponse = SyncPullSuccessResponse | SyncPullFailureResponse | SyncPullFetchFailureResponse
65
65
 
66
- type SyncPullSuccessResponse = SyncPullResponseBase & {
66
+ type SyncPullSuccessResponse = SyncResponseBase & {
67
67
  ok: true
68
68
  entries: SyncEntry[]
69
69
  token: SyncToken
70
70
  previousToken: SyncToken | null
71
71
  }
72
- type SyncPullFailureResponse = SyncPullResponseBase & {
72
+ type SyncPullFailureResponse = SyncResponseBase & {
73
73
  ok: false,
74
- originalError: Error
74
+ isRetryable: boolean
75
75
  }
76
- interface SyncPullResponseBase extends SyncResponseBase {
77
-
76
+ type SyncPullFetchFailureResponse = SyncResponseBase & {
77
+ ok: false,
78
+ originalError: Error
78
79
  }
79
80
  export interface SyncResponseBase {
80
81
  ok: boolean
@@ -141,11 +142,18 @@ export function handlePull(callback: (session: BaseSessionProvider) => Request)
141
142
 
142
143
  let response: Response | null = null
143
144
  try {
144
- response = await performFetch(request)
145
+ response = await fetch(request)
145
146
  } catch (e) {
146
147
  return {
147
148
  ok: false,
148
- originalError: e
149
+ originalError: e as Error
150
+ }
151
+ }
152
+
153
+ if (response.ok === false) {
154
+ return {
155
+ ok: false,
156
+ isRetryable: (response.status >= 500 && response.status < 504) || response.status === 429 || response.status === 408
149
157
  }
150
158
  }
151
159
 
@@ -180,11 +188,18 @@ export function handlePush(callback: (session: BaseSessionProvider) => Request)
180
188
 
181
189
  let response: Response | null = null
182
190
  try {
183
- response = await performFetch(request)
191
+ response = await fetch(request)
184
192
  } catch (e) {
185
193
  return {
186
194
  ok: false,
187
- originalError: e
195
+ originalError: e as Error
196
+ }
197
+ }
198
+
199
+ if (response.ok === false) {
200
+ return {
201
+ ok: false,
202
+ isRetryable: (response.status >= 500 && response.status < 504) || response.status === 429 || response.status === 408
188
203
  }
189
204
  }
190
205
 
@@ -198,17 +213,6 @@ export function handlePush(callback: (session: BaseSessionProvider) => Request)
198
213
  }
199
214
  }
200
215
 
201
- async function performFetch(request: Request) {
202
- const response = await fetch(request)
203
- const isRetryable = (response.status >= 500 && response.status < 504) || response.status === 429 || response.status === 408
204
-
205
- if (isRetryable) {
206
- throw new Error(`Server returned ${response.status}`)
207
- }
208
-
209
- return response
210
- }
211
-
212
216
  function serializePullUrlQuery(url: string, fetchToken: SyncToken | null) {
213
217
  const queryString = url.replace(/^[^?]*\??/, '');
214
218
  const searchParams = new URLSearchParams(queryString);
@@ -14,6 +14,7 @@ import { inBoundary } from './errors/boundary'
14
14
  import createStoresFromConfig from './store-configs'
15
15
 
16
16
  export default class SyncManager {
17
+ private static counter = 0
17
18
  private static instance: SyncManager | null = null
18
19
 
19
20
  public static assignAndSetupInstance(instance: SyncManager) {
@@ -23,8 +24,8 @@ export default class SyncManager {
23
24
  SyncManager.instance = instance
24
25
  const teardown = instance.setup()
25
26
  return async () => {
26
- await teardown()
27
27
  SyncManager.instance = null
28
+ await teardown()
28
29
  }
29
30
  }
30
31
 
@@ -35,6 +36,7 @@ export default class SyncManager {
35
36
  return SyncManager.instance
36
37
  }
37
38
 
39
+ private id: string
38
40
  public telemetry: SyncTelemetry
39
41
  private database: Database
40
42
  private context: SyncContext
@@ -45,10 +47,12 @@ export default class SyncManager {
45
47
  private safetyMap: { stores: SyncStore<any>[]; mechanisms: (() => void)[] }[]
46
48
 
47
49
  constructor(context: SyncContext, initDatabase: () => Database) {
50
+ this.id = (SyncManager.counter++).toString()
51
+
48
52
  this.telemetry = SyncTelemetry.getInstance()!
49
53
  this.context = context
50
54
 
51
- this.database = this.telemetry.trace({ name: 'db:init' }, () => inBoundary(initDatabase))
55
+ this.database = this.telemetry.trace({ name: 'db:init' }, () => inBoundary(initDatabase)) // todo - can cause undefined??
52
56
 
53
57
  this.runScope = new SyncRunScope()
54
58
  this.retry = new SyncRetry(this.context, this.telemetry)
@@ -59,6 +63,13 @@ export default class SyncManager {
59
63
  this.safetyMap = []
60
64
  }
61
65
 
66
+ /**
67
+ * Useful as a cache key (if user logs in and out multiple times, creating multiple managers)
68
+ */
69
+ getId() {
70
+ return this.id
71
+ }
72
+
62
73
  createStore<TModel extends BaseModel>(config: SyncStoreConfig<TModel>) {
63
74
  return new SyncStore<TModel>(config, this.context, this.database, this.retry, this.runScope, this.telemetry)
64
75
  }
@@ -23,6 +23,13 @@ export default class ProgressRepository extends SyncRepository<ContentProgress>
23
23
  )
24
24
  }
25
25
 
26
+ async completedByContentIds(contentIds: number[]) {
27
+ return this.queryAll(
28
+ Q.where('content_id', Q.oneOf(contentIds)),
29
+ Q.where('state', STATE.COMPLETED)
30
+ )
31
+ }
32
+
26
33
  // null collection only
27
34
  async startedOrCompleted(opts: Parameters<typeof this.startedOrCompletedClauses>[0] = {}) {
28
35
  return this.queryAll(...this.startedOrCompletedClauses(opts))
@@ -14,35 +14,50 @@ import {
14
14
  PracticeDayNote
15
15
  } from "./models"
16
16
 
17
+
17
18
  interface SyncRepositories {
18
- likes: ContentLikesRepository
19
- contentProgress: ContentProgressRepository
20
- practices: PracticesRepository
21
- practiceDayNotes: PracticeDayNotesRepository
19
+ likes: ContentLikesRepository;
20
+ contentProgress: ContentProgressRepository;
21
+ practices: PracticesRepository;
22
+ practiceDayNotes: PracticeDayNotesRepository;
22
23
  }
23
24
 
24
- export default new Proxy({} as SyncRepositories, {
25
- get(target: SyncRepositories, prop: keyof SyncRepositories) {
26
- if (!target[prop]) {
27
- const manager = SyncManager.getInstance()
28
25
 
26
+ // internal cache for repositories, keyed by managerId and property name
27
+ const repoCache: Record<string, Partial<SyncRepositories>> = {};
28
+
29
+ const proxy = new Proxy({} as SyncRepositories, {
30
+ get(_target, prop: keyof SyncRepositories) {
31
+ const manager = SyncManager.getInstance();
32
+ const managerId = manager.getId();
33
+
34
+ if (!repoCache[managerId]) {
35
+ repoCache[managerId] = {};
36
+ }
37
+ const cache = repoCache[managerId];
38
+
39
+ if (!cache[prop]) {
29
40
  switch (prop) {
30
41
  case 'likes':
31
- target[prop] = new ContentLikesRepository(manager.getStore(ContentLike))
32
- break
42
+ cache.likes = new ContentLikesRepository(manager.getStore(ContentLike));
43
+ break;
33
44
  case 'contentProgress':
34
- target[prop] = new ContentProgressRepository(manager.getStore(ContentProgress))
35
- break
45
+ cache.contentProgress = new ContentProgressRepository(manager.getStore(ContentProgress));
46
+ break;
36
47
  case 'practices':
37
- target[prop] = new PracticesRepository(manager.getStore(Practice))
38
- break
48
+ cache.practices = new PracticesRepository(manager.getStore(Practice));
49
+ break;
39
50
  case 'practiceDayNotes':
40
- target[prop] = new PracticeDayNotesRepository(manager.getStore(PracticeDayNote))
41
- break
51
+ cache.practiceDayNotes = new PracticeDayNotesRepository(manager.getStore(PracticeDayNote));
52
+ break;
42
53
  default:
43
- throw new SyncError(`Repository '${prop}' not found`)
54
+ throw new SyncError(`Repository '${String(prop)}' not found`);
44
55
  }
45
56
  }
46
- return target[prop]
57
+ return cache[prop];
47
58
  }
48
- })
59
+ });
60
+
61
+ export default proxy;
62
+
63
+
@@ -56,8 +56,14 @@ export default class SyncRetry {
56
56
  this.resetBackoff()
57
57
  return result
58
58
  } else {
59
- this.scheduleBackoff()
60
- if (attempt >= this.MAX_ATTEMPTS) return result
59
+ const isRetryable = 'isRetryable' in result ? result.isRetryable : false
60
+
61
+ if (isRetryable) {
62
+ this.scheduleBackoff()
63
+ if (attempt >= this.MAX_ATTEMPTS) return result
64
+ } else {
65
+ return result
66
+ }
61
67
  }
62
68
  }
63
69
  }
@@ -424,19 +424,21 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
424
424
  }
425
425
 
426
426
  private async setLastFetchToken(token: SyncToken | null) {
427
- await this.db.write(async () => {
428
- if (token) {
429
- const storedValue = await this.getLastFetchToken()
430
-
431
- // avoids thrashing if we get and compare first before setting
432
- if (storedValue !== token) {
433
- this.telemetry.debug(`[store:${this.model.table}] Setting last fetch token: ${token}`)
434
- return this.db.localStorage.set(this.lastFetchTokenKey, token)
427
+ await this.runScope.abortable(async () => {
428
+ await this.db.write(async () => {
429
+ if (token) {
430
+ const storedValue = await this.getLastFetchToken()
431
+
432
+ // avoids thrashing if we get and compare first before setting
433
+ if (storedValue !== token) {
434
+ this.telemetry.debug(`[store:${this.model.table}] Setting last fetch token: ${token}`)
435
+ return this.db.localStorage.set(this.lastFetchTokenKey, token)
436
+ }
437
+ } else {
438
+ this.telemetry.debug(`[store:${this.model.table}] Removing last fetch token`)
439
+ return this.db.localStorage.remove(this.lastFetchTokenKey)
435
440
  }
436
- } else {
437
- this.telemetry.debug(`[store:${this.model.table}] Removing last fetch token`)
438
- return this.db.localStorage.remove(this.lastFetchTokenKey)
439
- }
441
+ })
440
442
  })
441
443
  }
442
444
 
@@ -75,7 +75,6 @@ export class SyncTelemetry {
75
75
  }
76
76
  } : undefined)
77
77
 
78
-
79
78
  this._ignoreConsole = true
80
79
  this.error(err.message)
81
80
  this._ignoreConsole = false
@@ -4,7 +4,7 @@ import { SyncError } from '../errors'
4
4
  type ReturnsUndefined<T extends (...args: any[]) => any> = (...args: Parameters<T>) => ReturnType<T> | undefined
5
5
 
6
6
  export const syncSentryBeforeSend: ReturnsUndefined<NonNullable<SentryBrowserOptions['beforeSend']>> = (event, hint) => {
7
- if (event.logger === 'console' && SyncTelemetry.getInstance().shouldIgnoreConsole()) {
7
+ if (event.logger === 'console' && SyncTelemetry.getInstance()?.shouldIgnoreConsole()) {
8
8
  return null
9
9
  }
10
10