musora-content-services 2.95.5 → 2.96.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.
@@ -1,8 +1,9 @@
1
1
  import { fetchHierarchy, fetchLearningPathHierarchy } from './sanity.js'
2
2
  import { db } from './sync'
3
- import {COLLECTION_TYPE, STATE} from './sync/models/ContentProgress'
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'
6
7
 
7
8
  const STATE_STARTED = STATE.STARTED
8
9
  const STATE_COMPLETED = STATE.COMPLETED
@@ -17,7 +18,12 @@ export async function getProgressStateByIds(contentIds, collection = null) {
17
18
  }
18
19
 
19
20
  export async function getResumeTimeSecondsByIds(contentIds, collection = null) {
20
- return getByIds(normalizeContentIds(contentIds), normalizeCollection(collection), 'resume_time_seconds', 0)
21
+ return getByIds(
22
+ normalizeContentIds(contentIds),
23
+ normalizeCollection(collection),
24
+ 'resume_time_seconds',
25
+ 0
26
+ )
21
27
  }
22
28
 
23
29
  export async function getNavigateTo(data, collection = null) {
@@ -54,23 +60,45 @@ export async function getNavigateTo(data, collection = null) {
54
60
  const firstChild = validChildren[0]
55
61
  let lastInteractedChildNavToData = await getNavigateTo([firstChild], collection)
56
62
  lastInteractedChildNavToData = lastInteractedChildNavToData[firstChild.id] ?? null
57
- navigateToData[content.id] = buildNavigateTo(firstChild, lastInteractedChildNavToData, collection) //no G-child for LP
63
+ navigateToData[content.id] = buildNavigateTo(
64
+ firstChild,
65
+ lastInteractedChildNavToData,
66
+ collection
67
+ ) //no G-child for LP
58
68
  } else {
59
69
  const childrenStates = await getProgressStateByIds(childrenIds, collection)
60
70
  const lastInteracted = await getLastInteractedOf(childrenIds, collection)
61
71
  const lastInteractedStatus = childrenStates[lastInteracted]
62
72
 
63
73
  if (['course', 'pack-bundle', 'skill-pack'].includes(content.type)) {
64
- if (lastInteractedStatus === STATE_STARTED) { // send to last interacted
65
- navigateToData[content.id] = buildNavigateTo(children.get(lastInteracted), null, collection)
66
- } else { // send to first incomplete after last interacted
74
+ if (lastInteractedStatus === STATE_STARTED) {
75
+ // send to last interacted
76
+ navigateToData[content.id] = buildNavigateTo(
77
+ children.get(lastInteracted),
78
+ null,
79
+ collection
80
+ )
81
+ } else {
82
+ // send to first incomplete after last interacted
67
83
  let incompleteChild = findIncompleteLesson(childrenStates, lastInteracted, content.type)
68
- navigateToData[content.id] = buildNavigateTo(children.get(incompleteChild), null, collection)
84
+ navigateToData[content.id] = buildNavigateTo(
85
+ children.get(incompleteChild),
86
+ null,
87
+ collection
88
+ )
69
89
  }
70
- } else if (['song-tutorial', 'guided-course', COLLECTION_TYPE.LEARNING_PATH].includes(content.type)) { // send to first incomplete
90
+ } else if (
91
+ ['song-tutorial', 'guided-course', COLLECTION_TYPE.LEARNING_PATH].includes(content.type)
92
+ ) {
93
+ // send to first incomplete
71
94
  let incompleteChild = findIncompleteLesson(childrenStates, lastInteracted, content.type)
72
- navigateToData[content.id] = buildNavigateTo(children.get(incompleteChild), null, collection)
73
- } else if (twoDepthContentTypes.includes(content.type)) { // send to navigateTo child of last interacted child
95
+ navigateToData[content.id] = buildNavigateTo(
96
+ children.get(incompleteChild),
97
+ null,
98
+ collection
99
+ )
100
+ } else if (twoDepthContentTypes.includes(content.type)) {
101
+ // send to navigateTo child of last interacted child
74
102
  const firstChildren = content.children ?? []
75
103
  const lastInteractedChildId = await getLastInteractedOf(
76
104
  firstChildren.map((child) => child.id),
@@ -117,21 +145,28 @@ function buildNavigateTo(content, child = null, collection = null) {
117
145
  * @returns {Promise<number>}
118
146
  */
119
147
  export async function getLastInteractedOf(contentIds, collection = null) {
120
- return db.contentProgress.mostRecentlyUpdatedId(normalizeContentIds(contentIds), normalizeCollection(collection)).then(r => r.data ? parseInt(r.data) : undefined)
148
+ return db.contentProgress
149
+ .mostRecentlyUpdatedId(normalizeContentIds(contentIds), normalizeCollection(collection))
150
+ .then((r) => (r.data ? parseInt(r.data) : undefined))
121
151
  }
122
152
 
123
153
  export async function getProgressDataByIds(contentIds, collection) {
124
154
  contentIds = normalizeContentIds(contentIds)
125
155
  collection = normalizeCollection(collection)
126
156
 
127
- const progress = Object.fromEntries(contentIds.map(id => [id, {
128
- last_update: 0,
129
- progress: 0,
130
- status: '',
131
- }]))
157
+ const progress = Object.fromEntries(
158
+ contentIds.map((id) => [
159
+ id,
160
+ {
161
+ last_update: 0,
162
+ progress: 0,
163
+ status: '',
164
+ },
165
+ ])
166
+ )
132
167
 
133
- await db.contentProgress.getSomeProgressByContentIds(contentIds, collection).then(r => {
134
- r.data.forEach(p => {
168
+ await db.contentProgress.getSomeProgressByContentIds(contentIds, collection).then((r) => {
169
+ r.data.forEach((p) => {
135
170
  progress[p.content_id] = {
136
171
  last_update: p.updated_at,
137
172
  progress: p.progress_percent,
@@ -145,15 +180,17 @@ export async function getProgressDataByIds(contentIds, collection) {
145
180
 
146
181
  async function getById(contentId, dataKey, defaultValue) {
147
182
  if (!contentId) return defaultValue
148
- return db.contentProgress.getOneProgressByContentId(contentId).then(r => r.data?.[dataKey] ?? defaultValue)
183
+ return db.contentProgress
184
+ .getOneProgressByContentId(contentId)
185
+ .then((r) => r.data?.[dataKey] ?? defaultValue)
149
186
  }
150
187
 
151
188
  async function getByIds(contentIds, collection, dataKey, defaultValue) {
152
189
  if (contentIds.length === 0) return {}
153
190
 
154
- const progress = Object.fromEntries(contentIds.map(id => [id, defaultValue]))
155
- await db.contentProgress.getSomeProgressByContentIds(contentIds, collection).then(r => {
156
- r.data.forEach(p => {
191
+ const progress = Object.fromEntries(contentIds.map((id) => [id, defaultValue]))
192
+ await db.contentProgress.getSomeProgressByContentIds(contentIds, collection).then((r) => {
193
+ r.data.forEach((p) => {
157
194
  progress[p.content_id] = p[dataKey] ?? defaultValue
158
195
  })
159
196
  })
@@ -161,11 +198,11 @@ async function getByIds(contentIds, collection, dataKey, defaultValue) {
161
198
  }
162
199
 
163
200
  export async function getAllStarted(limit = null) {
164
- return db.contentProgress.startedIds(limit).then(r => r.data.map(id => parseInt(id)))
201
+ return db.contentProgress.startedIds(limit).then((r) => r.data.map((id) => parseInt(id)))
165
202
  }
166
203
 
167
204
  export async function getAllCompleted(limit = null) {
168
- return db.contentProgress.completedIds(limit).then(r => r.data.map(id => parseInt(id)))
205
+ return db.contentProgress.completedIds(limit).then((r) => r.data.map((id) => parseInt(id)))
169
206
  }
170
207
 
171
208
  export async function getAllCompletedByIds(contentIds) {
@@ -175,7 +212,7 @@ export async function getAllCompletedByIds(contentIds) {
175
212
  export async function getAllStartedOrCompleted({
176
213
  onlyIds = true,
177
214
  brand = null,
178
- limit = null
215
+ limit = null,
179
216
  } = {}) {
180
217
  const agoInSeconds = Math.floor(Date.now() / 1000) - 60 * 24 * 60 * 60 // 60 days in seconds
181
218
  const filters = {
@@ -185,15 +222,22 @@ export async function getAllStartedOrCompleted({
185
222
  }
186
223
 
187
224
  if (onlyIds) {
188
- return db.contentProgress.startedOrCompletedIds(filters).then(r => r.data.map(id => parseInt(id)))
225
+ return db.contentProgress
226
+ .startedOrCompletedIds(filters)
227
+ .then((r) => r.data.map((id) => parseInt(id)))
189
228
  } else {
190
- return db.contentProgress.startedOrCompleted(filters).then(r => {
191
- return Object.fromEntries(r.data.map(p => [p.content_id, {
192
- last_update: p.updated_at,
193
- progress: p.progress_percent,
194
- status: p.state,
195
- brand: p.content_brand,
196
- }]))
229
+ return db.contentProgress.startedOrCompleted(filters).then((r) => {
230
+ return Object.fromEntries(
231
+ r.data.map((p) => [
232
+ p.content_id,
233
+ {
234
+ last_update: p.updated_at,
235
+ progress: p.progress_percent,
236
+ status: p.state,
237
+ brand: p.content_brand,
238
+ },
239
+ ])
240
+ )
197
241
  })
198
242
  }
199
243
  }
@@ -216,8 +260,8 @@ export async function getAllStartedOrCompleted({
216
260
  * console.log(progressMap[123]); // => 52
217
261
  */
218
262
  export async function getStartedOrCompletedProgressOnly({ brand = undefined } = {}) {
219
- return db.contentProgress.startedOrCompleted({ brand: brand }).then(r => {
220
- return Object.fromEntries(r.data.map(p => [p.content_id, p.progress_percent]))
263
+ return db.contentProgress.startedOrCompleted({ brand: brand }).then((r) => {
264
+ return Object.fromEntries(r.data.map((p) => [p.content_id, p.progress_percent]))
221
265
  })
222
266
  }
223
267
 
@@ -256,9 +300,7 @@ export async function recordWatchSession(
256
300
  async function trackPractice(contentId, secondsPlayed, prevSession, details = {}) {
257
301
  const session = prevSession || new Map()
258
302
 
259
- const secondsSinceLastUpdate = Math.ceil(
260
- secondsPlayed - (session.get(contentId) ?? 0)
261
- )
303
+ const secondsSinceLastUpdate = Math.ceil(secondsPlayed - (session.get(contentId) ?? 0))
262
304
  session.set(contentId, secondsPlayed)
263
305
 
264
306
  await trackUserPractice(contentId, secondsSinceLastUpdate, details)
@@ -274,43 +316,78 @@ async function trackProgress(contentId, collection, currentSeconds, mediaLengthS
274
316
  }
275
317
 
276
318
  export async function contentStatusCompleted(contentId, collection = null) {
277
- return setStartedOrCompletedStatus(normalizeContentId(contentId), normalizeCollection(collection), true)
319
+ return setStartedOrCompletedStatus(
320
+ normalizeContentId(contentId),
321
+ normalizeCollection(collection),
322
+ true
323
+ )
278
324
  }
279
325
 
280
326
  export async function contentsStatusCompleted(contentIds, collection = null) {
281
- return setStartedOrCompletedStatuses(normalizeContentIds(contentIds), normalizeCollection(collection), true)
327
+ return setStartedOrCompletedStatuses(
328
+ normalizeContentIds(contentIds),
329
+ normalizeCollection(collection),
330
+ true
331
+ )
282
332
  }
283
333
 
284
334
  export async function contentStatusStarted(contentId, collection = null) {
285
- return setStartedOrCompletedStatus(normalizeContentId(contentId), normalizeCollection(collection), false)
335
+ return setStartedOrCompletedStatus(
336
+ normalizeContentId(contentId),
337
+ normalizeCollection(collection),
338
+ false
339
+ )
286
340
  }
287
341
  export async function contentStatusReset(contentId, collection = null) {
288
342
  return resetStatus(contentId, collection)
289
343
  }
290
344
 
291
345
  async function saveContentProgress(contentId, collection, progress, currentSeconds) {
292
- const response = await db.contentProgress.recordProgress(contentId, collection, progress, currentSeconds)
346
+ const response = await db.contentProgress.recordProgress(
347
+ contentId,
348
+ collection,
349
+ progress,
350
+ currentSeconds
351
+ )
352
+ if (progress === 100) emitContentCompleted(contentId, collection)
293
353
 
294
354
  // note - previous implementation explicitly did not trickle progress to children here
295
355
  // (only to siblings/parents via le bubbles)
296
356
 
297
- const bubbledProgresses = await bubbleProgress(await getHierarchy(contentId, collection), contentId, collection)
357
+ const bubbledProgresses = await bubbleProgress(
358
+ await getHierarchy(contentId, collection),
359
+ contentId,
360
+ collection
361
+ )
298
362
  await db.contentProgress.recordProgressesTentative(bubbledProgresses, collection)
299
363
 
364
+ for (const [bubbledContentId, bubbledProgress] of Object.entries(bubbledProgresses)) {
365
+ if (bubbledProgress === 100) {
366
+ emitContentCompleted(Number(bubbledContentId), collection)
367
+ }
368
+ }
300
369
  return response
301
370
  }
302
371
 
303
372
  async function setStartedOrCompletedStatus(contentId, collection, isCompleted) {
304
373
  const progress = isCompleted ? 100 : 0
305
374
  const response = await db.contentProgress.recordProgress(contentId, collection, progress)
306
-
375
+ if (progress === 100) emitContentCompleted(contentId, collection)
307
376
  const hierarchy = await getHierarchy(contentId, collection)
308
-
309
377
  await Promise.all([
310
- db.contentProgress.recordProgressesTentative(trickleProgress(hierarchy, contentId, collection, progress), collection),
311
- bubbleProgress(hierarchy, contentId, collection).then(bubbledProgresses => db.contentProgress.recordProgressesTentative(bubbledProgresses, collection))
378
+ db.contentProgress.recordProgressesTentative(
379
+ trickleProgress(hierarchy, contentId, collection, progress),
380
+ collection
381
+ ),
382
+ bubbleProgress(hierarchy, contentId, collection).then(async (bubbledProgresses) => {
383
+ await db.contentProgress.recordProgressesTentative(bubbledProgresses, collection)
384
+ for (const [bubbledContentId, bubbledProgress] of Object.entries(bubbledProgresses)) {
385
+ if (bubbledProgress === 100) {
386
+ emitContentCompleted(Number(bubbledContentId), collection)
387
+ }
388
+ }
389
+ }),
312
390
  ])
313
-
314
391
  return response
315
392
  }
316
393
 
@@ -337,13 +414,11 @@ async function setStartedOrCompletedStatuses(contentIds, collection, isCompleted
337
414
  ids = {
338
415
  ...ids,
339
416
  ...trickleProgress(hierarchy, contentId, collection, progress),
340
- ...await bubbleProgress(hierarchy, contentId, collection)
417
+ ...(await bubbleProgress(hierarchy, contentId, collection)),
341
418
  }
342
419
  }
343
420
 
344
- await Promise.all([
345
- db.contentProgress.recordProgressesTentative(ids, collection),
346
- ]);
421
+ await Promise.all([db.contentProgress.recordProgressesTentative(ids, collection)])
347
422
 
348
423
  return response
349
424
  }
@@ -353,8 +428,13 @@ async function resetStatus(contentId, collection = null) {
353
428
  const hierarchy = await getHierarchy(contentId, collection)
354
429
 
355
430
  await Promise.all([
356
- db.contentProgress.recordProgressesTentative(trickleProgress(hierarchy, contentId, collection, 0), collection),
357
- bubbleProgress(hierarchy, contentId, collection).then(bubbledProgresses => db.contentProgress.recordProgressesTentative(bubbledProgresses, collection))
431
+ db.contentProgress.recordProgressesTentative(
432
+ trickleProgress(hierarchy, contentId, collection, 0),
433
+ collection
434
+ ),
435
+ bubbleProgress(hierarchy, contentId, collection).then((bubbledProgresses) =>
436
+ db.contentProgress.recordProgressesTentative(bubbledProgresses, collection)
437
+ ),
358
438
  ])
359
439
 
360
440
  return response
@@ -364,10 +444,10 @@ async function resetStatus(contentId, collection = null) {
364
444
  // as long as callers remember to pass collection where needed
365
445
  function trickleProgress(hierarchy, contentId, _collection, progress) {
366
446
  const descendantIds = getChildrenToDepth(contentId, hierarchy, MAX_DEPTH)
367
- return Object.fromEntries(descendantIds.map(id => [id, progress]))
447
+ return Object.fromEntries(descendantIds.map((id) => [id, progress]))
368
448
  }
369
449
 
370
- async function bubbleProgress(hierarchy, contentId, collection = null) {
450
+ async function bubbleProgress(hierarchy, contentId, collection = null) {
371
451
  const ids = getAncestorAndSiblingIds(hierarchy, contentId)
372
452
  const progresses = await getByIds(ids, collection, 'progress_percent', 0)
373
453
  return averageProgressesFor(hierarchy, contentId, progresses)
@@ -386,7 +466,7 @@ function getAncestorAndSiblingIds(hierarchy, contentId, depth = 1) {
386
466
 
387
467
  return [
388
468
  ...(hierarchy?.children?.[parentId] ?? []),
389
- ...getAncestorAndSiblingIds(hierarchy, parentId, depth + 1)
469
+ ...getAncestorAndSiblingIds(hierarchy, parentId, depth + 1),
390
470
  ]
391
471
  }
392
472
 
@@ -398,10 +478,13 @@ function averageProgressesFor(hierarchy, contentId, progressData, depth = 1) {
398
478
  const parentId = hierarchy?.parents?.[contentId]
399
479
  if (!parentId) return {}
400
480
 
401
- const parentChildProgress = hierarchy?.children?.[parentId]?.map(childId => {
481
+ const parentChildProgress = hierarchy?.children?.[parentId]?.map((childId) => {
402
482
  return progressData[childId] ?? 0
403
483
  })
404
- const avgParentProgress = parentChildProgress.length > 0 ? Math.round(parentChildProgress.reduce((a, b) => a + b, 0) / parentChildProgress.length) : 0
484
+ const avgParentProgress =
485
+ parentChildProgress.length > 0
486
+ ? Math.round(parentChildProgress.reduce((a, b) => a + b, 0) / parentChildProgress.length)
487
+ : 0
405
488
 
406
489
  return {
407
490
  ...averageProgressesFor(hierarchy, parentId, progressData, depth + 1),
@@ -426,7 +509,7 @@ function normalizeContentId(contentId) {
426
509
  }
427
510
 
428
511
  function normalizeContentIds(contentIds) {
429
- return contentIds.map(id => normalizeContentId(id))
512
+ return contentIds.map((id) => normalizeContentId(id))
430
513
  }
431
514
 
432
515
  function normalizeCollection(collection) {
@@ -435,7 +518,7 @@ function normalizeCollection(collection) {
435
518
  if (!Object.values(COLLECTION_TYPE).includes(collection.type)) {
436
519
  throw new Error(`Invalid collection type: ${collection.type}`)
437
520
  }
438
-
521
+
439
522
  if (typeof collection.id === 'string' && isNaN(+collection.id)) {
440
523
  throw new Error(`Invalid collection id: ${collection.id}`)
441
524
  }
@@ -48,7 +48,7 @@ export function onProgressSaved(listener) {
48
48
  * @returns {void}
49
49
  */
50
50
  export function emitProgressSaved(event) {
51
- listeners.forEach(listener => {
51
+ listeners.forEach((listener) => {
52
52
  try {
53
53
  listener(event)
54
54
  } catch (error) {
@@ -56,3 +56,55 @@ 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
+ }
@@ -308,12 +308,7 @@ function serializeIds(ids: { id: RecordId }): { client_record_id: RecordId } {
308
308
 
309
309
  function deserializeRecord(record: SyncSyncable<BaseModel, 'client_record_id'> | null): SyncSyncable<BaseModel, 'id'> | null {
310
310
  if (record) {
311
- const { client_record_id: id, ...rest } = record as any
312
-
313
- if ('collection_type' in rest && rest.collection_type === 'self') {
314
- rest.collection_type = null
315
- rest.collection_id = null
316
- }
311
+ const { client_record_id: id, ...rest } = record
317
312
 
318
313
  return {
319
314
  ...rest,
@@ -14,6 +14,9 @@ import { inBoundary } from './errors/boundary'
14
14
  import createStoresFromConfig from './store-configs'
15
15
  import { contentProgressObserver } from '../awards/internal/content-progress-observer'
16
16
 
17
+ import { onProgressSaved, onContentCompleted } from '../progress-events'
18
+ import { onContentCompletedLearningPathListener } from '../content-org/learning-paths'
19
+
17
20
  export default class SyncManager {
18
21
  private static counter = 0
19
22
  private static instance: SyncManager | null = null
@@ -30,6 +33,10 @@ export default class SyncManager {
30
33
  }
31
34
  }
32
35
 
36
+ public static getInstanceOrNull() {
37
+ return SyncManager.instance
38
+ }
39
+
33
40
  public static getInstance(): SyncManager {
34
41
  if (!SyncManager.instance) {
35
42
  throw new SyncError('SyncManager not initialized')
@@ -72,17 +79,26 @@ export default class SyncManager {
72
79
  }
73
80
 
74
81
  createStore<TModel extends BaseModel>(config: SyncStoreConfig<TModel>) {
75
- return new SyncStore<TModel>(config, this.context, this.database, this.retry, this.runScope, this.telemetry)
82
+ return new SyncStore<TModel>(
83
+ config,
84
+ this.context,
85
+ this.database,
86
+ this.retry,
87
+ this.runScope,
88
+ this.telemetry
89
+ )
76
90
  }
77
91
 
78
92
  registerStores<TModel extends BaseModel>(stores: SyncStore<TModel>[]) {
79
- return Object.fromEntries(stores.map(store => {
80
- return [store.model.table, store]
81
- })) as Record<string, SyncStore<TModel>>
93
+ return Object.fromEntries(
94
+ stores.map((store) => {
95
+ return [store.model.table, store]
96
+ })
97
+ ) as Record<string, SyncStore<TModel>>
82
98
  }
83
99
 
84
100
  storesForModels(models: ModelClass[]) {
85
- return models.map(model => this.storesRegistry[model.table])
101
+ return models.map((model) => this.storesRegistry[model.table])
86
102
  }
87
103
 
88
104
  createStrategy<T extends SyncStrategy, U extends any[]>(
@@ -96,11 +112,8 @@ export default class SyncManager {
96
112
  this.strategyMap.push({ stores, strategies })
97
113
  }
98
114
 
99
- protectStores(
100
- stores: SyncStore<any>[],
101
- mechanisms: SyncConcurrencySafetyMechanism[]
102
- ) {
103
- const teardowns = mechanisms.map(mechanism => mechanism(this.context, stores))
115
+ protectStores(stores: SyncStore<any>[], mechanisms: SyncConcurrencySafetyMechanism[]) {
116
+ const teardowns = mechanisms.map((mechanism) => mechanism(this.context, stores))
104
117
  this.safetyMap.push({ stores, mechanisms: teardowns })
105
118
  }
106
119
 
@@ -111,9 +124,9 @@ export default class SyncManager {
111
124
  this.retry.start()
112
125
 
113
126
  this.strategyMap.forEach(({ stores, strategies }) => {
114
- strategies.forEach(strategy => {
115
- stores.forEach(store => {
116
- strategy.onTrigger(store, reason => {
127
+ strategies.forEach((strategy) => {
128
+ stores.forEach((store) => {
129
+ strategy.onTrigger(store, (reason) => {
117
130
  store.requestSync(reason)
118
131
  })
119
132
  })
@@ -121,15 +134,18 @@ export default class SyncManager {
121
134
  })
122
135
  })
123
136
 
124
- contentProgressObserver.start(this.database).catch(error => {
137
+ contentProgressObserver.start(this.database).catch((error) => {
125
138
  this.telemetry.error('[SyncManager] Failed to start contentProgressObserver', error)
126
139
  })
140
+ onContentCompleted(onContentCompletedLearningPathListener)
127
141
 
128
142
  const teardown = async () => {
129
143
  this.telemetry.debug('[SyncManager] Tearing down')
130
144
  this.runScope.abort()
131
- this.strategyMap.forEach(({ strategies }) => strategies.forEach(strategy => strategy.stop()))
132
- this.safetyMap.forEach(({ mechanisms }) => mechanisms.forEach(mechanism => mechanism()))
145
+ this.strategyMap.forEach(({ strategies }) =>
146
+ strategies.forEach((strategy) => strategy.stop())
147
+ )
148
+ this.safetyMap.forEach(({ mechanisms }) => mechanisms.forEach((mechanism) => mechanism()))
133
149
  contentProgressObserver.stop()
134
150
  this.retry.stop()
135
151
  this.context.stop()
@@ -2,9 +2,12 @@ import BaseModel from './Base'
2
2
  import { SYNC_TABLES } from '../schema'
3
3
 
4
4
  export enum COLLECTION_TYPE {
5
+ SELF = 'self',
6
+ GUIDED_COURSE = 'guided-course',
5
7
  LEARNING_PATH = 'learning-path-v2',
6
8
  PLAYLIST = 'playlist',
7
9
  }
10
+ export const COLLECTION_ID_SELF = 0
8
11
 
9
12
  export enum STATE {
10
13
  STARTED = 'started',
@@ -13,8 +16,8 @@ export enum STATE {
13
16
 
14
17
  export default class ContentProgress extends BaseModel<{
15
18
  content_id: number
16
- collection_type: COLLECTION_TYPE | null
17
- collection_id: number | null
19
+ collection_type: COLLECTION_TYPE
20
+ collection_id: number
18
21
  state: STATE
19
22
  progress_percent: number
20
23
  resume_time_seconds: number | null
@@ -34,10 +37,10 @@ export default class ContentProgress extends BaseModel<{
34
37
  return this._getRaw('progress_percent') as number
35
38
  }
36
39
  get collection_type() {
37
- return (this._getRaw('collection_type') as COLLECTION_TYPE) || null
40
+ return this._getRaw('collection_type') as COLLECTION_TYPE
38
41
  }
39
42
  get collection_id() {
40
- return (this._getRaw('collection_id') as number) || null
43
+ return this._getRaw('collection_id') as number
41
44
  }
42
45
  get resume_time_seconds() {
43
46
  return (this._getRaw('resume_time_seconds') as number) || null
@@ -55,10 +58,10 @@ export default class ContentProgress extends BaseModel<{
55
58
  set progress_percent(value: number) {
56
59
  this._setRaw('progress_percent', Math.min(100, Math.max(0, value)))
57
60
  }
58
- set collection_type(value: COLLECTION_TYPE | null) {
61
+ set collection_type(value: COLLECTION_TYPE) {
59
62
  this._setRaw('collection_type', value)
60
63
  }
61
- set collection_id(value: number | null) {
64
+ set collection_id(value: number) {
62
65
  this._setRaw('collection_id', value)
63
66
  }
64
67
  set resume_time_seconds(value: number | null) {