musora-content-services 2.89.0 → 2.92.3

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 (139) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/docs/ContentOrganization.html +2 -2
  3. package/docs/Forums.html +2 -2
  4. package/docs/Gamification.html +2 -2
  5. package/docs/TestUser.html +2 -2
  6. package/docs/UserManagementSystem.html +2 -2
  7. package/docs/api_types.js.html +2 -2
  8. package/docs/config.js.html +2 -2
  9. package/docs/content-org_content-org.js.html +2 -2
  10. package/docs/content-org_guided-courses.ts.html +2 -2
  11. package/docs/content-org_learning-paths.ts.html +52 -40
  12. package/docs/content-org_playlists-types.js.html +2 -2
  13. package/docs/content-org_playlists.js.html +2 -2
  14. package/docs/content.js.html +2 -2
  15. package/docs/content_artist.ts.html +2 -2
  16. package/docs/content_genre.ts.html +2 -2
  17. package/docs/content_instructor.ts.html +2 -2
  18. package/docs/forums_categories.ts.html +2 -2
  19. package/docs/forums_forums.ts.html +2 -2
  20. package/docs/forums_posts.ts.html +2 -2
  21. package/docs/forums_threads.ts.html +2 -2
  22. package/docs/gamification_awards.ts.html +2 -2
  23. package/docs/gamification_gamification.js.html +2 -2
  24. package/docs/global.html +2 -2
  25. package/docs/index.html +2 -2
  26. package/docs/liveTesting.ts.html +2 -2
  27. package/docs/module-Accounts.html +2 -2
  28. package/docs/module-Artist.html +2 -2
  29. package/docs/module-Awards.html +2 -2
  30. package/docs/module-Config.html +2 -2
  31. package/docs/module-Content-Services-V2.html +2 -2
  32. package/docs/module-Forums.html +2 -2
  33. package/docs/module-Genre.html +2 -2
  34. package/docs/module-GuidedCourses.html +2 -2
  35. package/docs/module-Instructor.html +2 -2
  36. package/docs/module-Interests.html +2 -2
  37. package/docs/module-LearningPaths.html +269 -143
  38. package/docs/module-Onboarding.html +3 -3
  39. package/docs/module-Payments.html +2 -2
  40. package/docs/module-Permissions.html +2 -2
  41. package/docs/module-Playlists.html +2 -2
  42. package/docs/module-ProgressRow.html +2 -2
  43. package/docs/module-Railcontent-Services.html +34 -893
  44. package/docs/module-Sanity-Services.html +2 -2
  45. package/docs/module-Sessions.html +2 -2
  46. package/docs/module-UserActivity.html +70 -116
  47. package/docs/module-UserChat.html +2 -2
  48. package/docs/module-UserManagement.html +2 -2
  49. package/docs/module-UserMemberships.html +2 -2
  50. package/docs/module-UserNotifications.html +2 -2
  51. package/docs/module-UserProfile.html +2 -2
  52. package/docs/progress-row_method-card.js.html +3 -2
  53. package/docs/railcontent.js.html +14 -137
  54. package/docs/sanity.js.html +2 -2
  55. package/docs/userActivity.js.html +85 -150
  56. package/docs/user_account.ts.html +2 -2
  57. package/docs/user_chat.js.html +2 -2
  58. package/docs/user_interests.js.html +2 -2
  59. package/docs/user_management.js.html +2 -2
  60. package/docs/user_memberships.ts.html +2 -2
  61. package/docs/user_notifications.js.html +2 -2
  62. package/docs/user_onboarding.ts.html +10 -6
  63. package/docs/user_payments.ts.html +2 -2
  64. package/docs/user_permissions.js.html +2 -2
  65. package/docs/user_profile.js.html +2 -2
  66. package/docs/user_sessions.js.html +2 -2
  67. package/docs/user_types.js.html +2 -2
  68. package/docs/user_user-management-system.js.html +2 -2
  69. package/package.json +11 -3
  70. package/src/contentTypeConfig.js +6 -0
  71. package/src/index.d.ts +7 -31
  72. package/src/index.js +10 -34
  73. package/src/services/content-org/learning-paths.ts +31 -0
  74. package/src/services/contentAggregator.js +2 -2
  75. package/src/services/contentLikes.js +6 -39
  76. package/src/services/contentProgress.js +181 -479
  77. package/src/services/dataContext.js +0 -2
  78. package/src/services/progress-row/method-card.js +1 -0
  79. package/src/services/railcontent.js +12 -135
  80. package/src/services/sentry/.indexignore +0 -0
  81. package/src/services/sentry/index.ts +23 -0
  82. package/src/services/sync/.indexignore +0 -0
  83. package/src/services/sync/adapters/factory.ts +26 -0
  84. package/src/services/sync/adapters/lokijs.ts +1 -0
  85. package/src/services/sync/adapters/sqlite.ts +1 -0
  86. package/src/services/sync/concurrency-safety.ts +4 -0
  87. package/src/services/sync/context/index.ts +43 -0
  88. package/src/services/sync/context/providers/base.ts +4 -0
  89. package/src/services/sync/context/providers/connectivity.ts +14 -0
  90. package/src/services/sync/context/providers/durability.ts +5 -0
  91. package/src/services/sync/context/providers/index.ts +5 -0
  92. package/src/services/sync/context/providers/session.ts +8 -0
  93. package/src/services/sync/context/providers/tabs.ts +18 -0
  94. package/src/services/sync/context/providers/visibility.ts +14 -0
  95. package/src/services/sync/database/factory.ts +10 -0
  96. package/src/services/sync/errors/boundary.ts +45 -0
  97. package/src/services/sync/errors/index.ts +49 -0
  98. package/src/services/sync/fetch.ts +310 -0
  99. package/src/services/sync/index.ts +80 -0
  100. package/src/services/sync/manager.ts +139 -0
  101. package/src/services/sync/models/Base.ts +47 -0
  102. package/src/services/sync/models/ContentLike.ts +16 -0
  103. package/src/services/sync/models/ContentProgress.ts +69 -0
  104. package/src/services/sync/models/Practice.ts +72 -0
  105. package/src/services/sync/models/PracticeDayNote.ts +23 -0
  106. package/src/services/sync/models/index.ts +4 -0
  107. package/src/services/sync/repositories/base.ts +247 -0
  108. package/src/services/sync/repositories/content-likes.ts +26 -0
  109. package/src/services/sync/repositories/content-progress.ts +160 -0
  110. package/src/services/sync/repositories/index.ts +4 -0
  111. package/src/services/sync/repositories/practice-day-notes.ts +4 -0
  112. package/src/services/sync/repositories/practices.ts +52 -0
  113. package/src/services/sync/repository-proxy.ts +48 -0
  114. package/src/services/sync/resolver.ts +84 -0
  115. package/src/services/sync/retry.ts +88 -0
  116. package/src/services/sync/run-scope.ts +30 -0
  117. package/src/services/sync/schema/index.ts +66 -0
  118. package/src/services/sync/serializers/index.ts +2 -0
  119. package/src/services/sync/serializers/model.ts +32 -0
  120. package/src/services/sync/serializers/raw.ts +21 -0
  121. package/src/services/sync/store/index.ts +779 -0
  122. package/src/services/sync/store/push-coalescer.ts +57 -0
  123. package/src/services/sync/store-configs.ts +41 -0
  124. package/src/services/sync/strategies/base.ts +21 -0
  125. package/src/services/sync/strategies/index.ts +12 -0
  126. package/src/services/sync/strategies/initial.ts +11 -0
  127. package/src/services/sync/strategies/polling.ts +54 -0
  128. package/src/services/sync/telemetry/index.ts +140 -0
  129. package/src/services/sync/telemetry/sampling.ts +91 -0
  130. package/src/services/sync/utils/event-emitter.ts +24 -0
  131. package/src/services/sync/utils/index.ts +1 -0
  132. package/src/services/sync/utils/throttle.ts +93 -0
  133. package/src/services/sync/utils/timers.ts +9 -0
  134. package/src/services/userActivity.js +83 -148
  135. package/test/contentProgress.test.js +6 -39
  136. package/test/live/contentProgressLive.test.js +2 -31
  137. package/tools/generate-index.cjs +10 -4
  138. package/.claude/settings.local.json +0 -8
  139. package/babel.config.cjs +0 -3
@@ -8,8 +8,6 @@ import { globalConfig } from './config.js'
8
8
  const excludeFromGeneratedIndex = []
9
9
 
10
10
  //These constants need to match MWP UserDataVersionKeyEnum enum
11
- export const ContentLikesVersionKey = 0
12
- export const ContentProgressVersionKey = 1
13
11
  export const UserActivityVersionKey = 2
14
12
  export const PollingStateVersionKey = 3
15
13
 
@@ -107,5 +107,6 @@ function getMethodActionCTA(item) {
107
107
  brand: item.brand,
108
108
  id: item.id,
109
109
  slug: item.slug,
110
+ parent_id: item.parent_id,
110
111
  }
111
112
  }
@@ -10,13 +10,6 @@ import { fetchJSONHandler } from '../lib/httpHelper.js'
10
10
  * @type {string[]}
11
11
  */
12
12
  const excludeFromGeneratedIndex = [
13
- 'fetchUserLikes',
14
- 'postContentLiked',
15
- 'postContentUnliked',
16
- 'postRecordWatchSession',
17
- 'postContentStarted',
18
- 'postContentComplete',
19
- 'postContentReset',
20
13
  'fetchUserPermissionsData',
21
14
  ]
22
15
 
@@ -275,10 +268,6 @@ export async function fetchUserPermissionsData() {
275
268
  return (await fetchHandler(url, 'get')) ?? []
276
269
  }
277
270
 
278
- async function fetchDataHandler(url, dataVersion, method = 'get') {
279
- return fetchHandler(url, method, dataVersion)
280
- }
281
-
282
271
  async function postDataHandler(url, data) {
283
272
  return fetchHandler(url, 'post', null, data)
284
273
  }
@@ -297,27 +286,7 @@ async function deleteDataHandler(url, data) {
297
286
 
298
287
  export async function fetchLikeCount(contendId) {
299
288
  const url = `/api/content/v1/content/like_count/${contendId}`
300
- return await fetchDataHandler(url)
301
- }
302
-
303
- export async function fetchUserLikes(currentVersion) {
304
- let url = `/api/content/v1/user/likes`
305
- return fetchDataHandler(url, currentVersion)
306
- }
307
-
308
- export async function postContentLiked(contentId) {
309
- let url = `/api/content/v1/user/likes/${contentId}`
310
- return await postDataHandler(url)
311
- }
312
-
313
- export async function postContentUnliked(contentId) {
314
- let url = `/api/content/v1/user/likes/${contentId}`
315
- return await deleteDataHandler(url)
316
- }
317
-
318
- export async function fetchContentProgress(currentVersion) {
319
- let url = `/content/user/progress/all`
320
- return fetchDataHandler(url, currentVersion)
289
+ return await fetchHandler(url)
321
290
  }
322
291
 
323
292
  export async function postPlaylistContentEngaged(playlistItemId) {
@@ -325,25 +294,6 @@ export async function postPlaylistContentEngaged(playlistItemId) {
325
294
  return postDataHandler(url)
326
295
  }
327
296
 
328
- export async function postRecordWatchSession(
329
- contentId,
330
- mediaTypeId,
331
- mediaLengthSeconds,
332
- currentSeconds,
333
- secondsPlayed,
334
- sessionId
335
- ) {
336
- let url = `/railtracker/v2/media-playback-session`
337
- return postDataHandler(url, {
338
- content_id: contentId,
339
- media_type_id: mediaTypeId,
340
- media_length_seconds: mediaLengthSeconds,
341
- current_second: currentSeconds,
342
- seconds_played: secondsPlayed,
343
- session_id: sessionId,
344
- })
345
- }
346
-
347
297
  /**
348
298
  * Fetch the user's best award for this challenge
349
299
  *
@@ -378,58 +328,6 @@ export async function fetchUserBadges(brand = null) {
378
328
  return await fetchHandler(url, 'get')
379
329
  }
380
330
 
381
- /**
382
- * complete a content's progress for a given user
383
- * @param contentId
384
- * @param collection {object|null} - the collection context of the progress. null is normal content progress
385
- * @param collection.type - the type of collection. options: ["learning-path"]
386
- * @param collection.id - the content_id of collection.
387
- * @returns {Promise<any|string|null>}
388
- */
389
- export async function postContentComplete(contentId, collection = null) {
390
- let url = `/api/content/v1/user/progress/complete/${contentId}`
391
- const body = {collection: collection}
392
- return postDataHandler(url, body)
393
- }
394
-
395
- /**
396
- * start the user's progress on a content
397
- * @param contentId
398
- * @param collection {object|null} - the collection context of the progress. null is normal content progress
399
- * @param collection.type - the type of collection. options: ["learning-path"]
400
- * @param collection.id - the content_id of collection.
401
- * @returns {Promise<any|string|null>}
402
- */
403
- export async function postContentStart(contentId, collection = null) {
404
- let url = `/api/content/v1/user/progress/start/${contentId}`
405
- const body = {collection: collection}
406
- return postDataHandler(url, body)
407
- }
408
-
409
- /**
410
- * resets the user's progress on a content
411
- * @param contentId
412
- * @param collection {object|null} - the collection context of the progress. null is normal content progress
413
- * @param collection.type - the type of collection. options: ["learning-path"]
414
- * @param collection.id - the content_id of collection.
415
- * @returns {Promise<any|string|null>}
416
- */
417
- export async function postContentReset(contentId, collection = null) {
418
- let url = `/api/content/v1/user/progress/reset/${contentId}`
419
- const body = {collection: collection}
420
- return postDataHandler(url, body)
421
- }
422
-
423
- /**
424
- * restores the user's progress on a content
425
- * @param contentId
426
- * @returns {Promise<any|string|null>}
427
- */
428
- export async function postContentRestore(contentId) {
429
- let url = `/api/content/v1/user/progress/restore/${contentId}`
430
- return postDataHandler(url)
431
- }
432
-
433
331
  /**
434
332
  * Set a user's StudentView Flag
435
333
  *
@@ -624,24 +522,21 @@ export async function fetchComment(commentId) {
624
522
  return comment.parent ? comment.parent : comment
625
523
  }
626
524
 
627
- export async function fetchUserPractices(currentVersion = 0, { userId } = {}) {
628
- const params = new URLSearchParams()
629
- if (userId) params.append('user_id', userId)
630
- const query = params.toString() ? `?${params.toString()}` : ''
631
- const url = `/api/user/practices/v1/practices${query}`
632
- const response = await fetchDataHandler(url, currentVersion)
633
- const { data, version } = response
525
+ export async function fetchUserPractices(userId) {
526
+ const url = `/api/user/practices/v1/practices?user_id=${userId}`
527
+ const response = await fetchHandler(url)
528
+ const { data } = response
634
529
  const userPractices = data
635
530
  if (!userPractices) {
636
- return { data: { practices: {} }, version }
531
+ return {}
637
532
  }
638
533
 
639
534
  const formattedPractices = userPractices.reduce((acc, practice) => {
640
- if (!acc[practice.day]) {
641
- acc[practice.day] = []
535
+ if (!acc[practice.date]) {
536
+ acc[practice.date] = []
642
537
  }
643
538
 
644
- acc[practice.day].push({
539
+ acc[practice.date].push({
645
540
  id: practice.id,
646
541
  duration_seconds: practice.duration_seconds,
647
542
  })
@@ -649,29 +544,11 @@ export async function fetchUserPractices(currentVersion = 0, { userId } = {}) {
649
544
  return acc
650
545
  }, {})
651
546
 
652
- return {
653
- data: {
654
- practices: formattedPractices,
655
- },
656
- version,
657
- }
547
+ return formattedPractices
658
548
  }
659
549
 
660
- export async function logUserPractice(practiceDetails) {
661
- const url = `/api/user/practices/v1/practices`
662
- return await fetchHandler(url, 'POST', null, practiceDetails)
663
- }
664
- export async function fetchUserPracticeMeta(practiceIds, userId = null) {
665
- if (practiceIds.length == 0) {
666
- return []
667
- }
668
- const params = new URLSearchParams()
669
- practiceIds.forEach((id) => params.append('practice_ids[]', id))
670
-
671
- if (userId !== null) {
672
- params.append('user_id', userId)
673
- }
674
- const url = `/api/user/practices/v1/practices?${params.toString()}`
550
+ export async function fetchUserPracticeMeta(day, userId) {
551
+ const url = `/api/user/practices/v1/practices?user_id=${userId}&date=${date}`
675
552
  return await fetchHandler(url, 'GET', null)
676
553
  }
677
554
 
File without changes
@@ -0,0 +1,23 @@
1
+ import * as Sentry from "@sentry/browser";
2
+
3
+ type SentryBrowserInitOptions = NonNullable<Parameters<typeof Sentry.init>[0]>;
4
+
5
+ type TracesSampler = SentryBrowserInitOptions['tracesSampler'];
6
+ type BeforeSend = SentryBrowserInitOptions['beforeSend'];
7
+ type BeforeSendTransaction = SentryBrowserInitOptions['beforeSendTransaction'];
8
+
9
+ // Compose multiple handlers of the same type into one.
10
+ // Stops at first handler that returns a non-undefined value.
11
+
12
+ export function composeHandlers<H extends TracesSampler>(...handlers: H[]): H;
13
+ export function composeHandlers<H extends BeforeSend>(...handlers: H[]): H;
14
+ export function composeHandlers<H extends BeforeSendTransaction>(...handlers: H[]): H;
15
+ export function composeHandlers<H extends (...args: any[]) => any>(...handlers: H[]): H {
16
+ return ((...args: Parameters<H>) => {
17
+ for (const handler of handlers) {
18
+ const res = handler(...args);
19
+ if (res !== undefined) return res;
20
+ }
21
+ return args[0]
22
+ }) as H;
23
+ }
File without changes
@@ -0,0 +1,26 @@
1
+ import schema from '../schema'
2
+
3
+ import type SQLiteAdapter from '@nozbe/watermelondb/adapters/sqlite'
4
+ import type LokiJSAdapter from '@nozbe/watermelondb/adapters/lokijs'
5
+
6
+ export type DatabaseAdapter = SQLiteAdapter | LokiJSAdapter
7
+
8
+ type SQLiteAdapterOptions = ConstructorParameters<typeof SQLiteAdapter>[0]
9
+ type LokiJSAdapterOptions = ConstructorParameters<typeof LokiJSAdapter>[0]
10
+
11
+ type DatabaseAdapterOptions = SQLiteAdapterOptions & LokiJSAdapterOptions
12
+
13
+ export default function syncAdapterFactory<T extends DatabaseAdapter>(
14
+ AdapterClass: new (options: DatabaseAdapterOptions) => T,
15
+ namespace: string,
16
+ opts: Omit<DatabaseAdapterOptions, 'schema' | 'migrations'>
17
+ ): () => T {
18
+ const options = {
19
+ ...opts,
20
+ dbName: `sync:${namespace}`,
21
+ schema,
22
+ migrations: undefined
23
+ }
24
+
25
+ return () => new AdapterClass(options)
26
+ }
@@ -0,0 +1 @@
1
+ export { default } from '@nozbe/watermelondb/adapters/lokijs'
@@ -0,0 +1 @@
1
+ export { default } from '@nozbe/watermelondb/adapters/sqlite'
@@ -0,0 +1,4 @@
1
+ import type SyncContext from "./context"
2
+ import type SyncStore from "./store"
3
+
4
+ export type SyncConcurrencySafetyMechanism = (context: SyncContext, stores: SyncStore[]) => () => void
@@ -0,0 +1,43 @@
1
+ import type {
2
+ BaseSessionProvider,
3
+ BaseConnectivityProvider,
4
+ BaseVisibilityProvider,
5
+ BaseTabsProvider,
6
+ BaseDurabilityProvider,
7
+ } from './providers'
8
+
9
+ type Providers = {
10
+ session: BaseSessionProvider
11
+ connectivity: BaseConnectivityProvider
12
+ visibility: BaseVisibilityProvider
13
+ tabs: BaseTabsProvider
14
+ durability: BaseDurabilityProvider
15
+ }
16
+
17
+ export default class SyncContext {
18
+ constructor(private providers: Providers) {}
19
+
20
+ start() {
21
+ Object.values(this.providers).forEach((p) => p.start())
22
+ }
23
+
24
+ stop() {
25
+ Object.values(this.providers).forEach((p) => p.stop())
26
+ }
27
+
28
+ get session() {
29
+ return this.providers.session
30
+ }
31
+ get connectivity() {
32
+ return this.providers.connectivity
33
+ }
34
+ get visibility() {
35
+ return this.providers.visibility
36
+ }
37
+ get tabs() {
38
+ return this.providers.tabs
39
+ }
40
+ get durability() {
41
+ return this.providers.durability
42
+ }
43
+ }
@@ -0,0 +1,4 @@
1
+ export default class BaseContextProvider {
2
+ start() {}
3
+ stop() {}
4
+ }
@@ -0,0 +1,14 @@
1
+ import EventEmitter from "../../utils/event-emitter";
2
+ import BaseContextProvider from "./base";
3
+
4
+ export default abstract class BaseConnectivityProvider extends BaseContextProvider {
5
+ private emitter = new EventEmitter<{ change: [boolean] }>()
6
+
7
+ abstract getValue(): boolean
8
+
9
+ subscribe(listener: (value: boolean) => void) {
10
+ return this.emitter.on('change', listener)
11
+ }
12
+
13
+ protected notifyListeners = () => this.emitter.emit('change', this.getValue())
14
+ }
@@ -0,0 +1,5 @@
1
+ import BaseContextProvider from "./base";
2
+
3
+ export default abstract class BaseDurabilityProvider extends BaseContextProvider {
4
+ abstract getValue(): boolean
5
+ }
@@ -0,0 +1,5 @@
1
+ export { default as BaseSessionProvider } from './session'
2
+ export { default as BaseConnectivityProvider } from './connectivity'
3
+ export { default as BaseVisibilityProvider } from './visibility'
4
+ export { default as BaseTabsProvider, NullTabsProvider } from './tabs'
5
+ export { default as BaseDurabilityProvider } from './durability'
@@ -0,0 +1,8 @@
1
+ import BaseContextProvider from "./base";
2
+
3
+ export default abstract class BaseSessionProvider extends BaseContextProvider {
4
+ abstract getClientId(): string
5
+ getSessionId(): string | null {
6
+ return null
7
+ }
8
+ }
@@ -0,0 +1,18 @@
1
+ import BaseContextProvider from "./base";
2
+
3
+ export default abstract class BaseTabsProvider extends BaseContextProvider {
4
+ abstract hasOtherTabs(): boolean
5
+ abstract broadcast<T>(name: string, payload: T): void
6
+ abstract subscribe<T>(name: string, callback: (payload: T) => void): () => void
7
+ }
8
+
9
+ export class NullTabsProvider extends BaseTabsProvider {
10
+ hasOtherTabs() {
11
+ return false
12
+ }
13
+
14
+ broadcast() {}
15
+ subscribe() {
16
+ return () => {}
17
+ }
18
+ }
@@ -0,0 +1,14 @@
1
+ import EventEmitter from "../../utils/event-emitter";
2
+ import BaseContextProvider from "./base";
3
+
4
+ export default abstract class BaseVisibilityProvider extends BaseContextProvider {
5
+ private emitter = new EventEmitter<{ change: [boolean] }>()
6
+
7
+ abstract getValue(): boolean
8
+
9
+ subscribe(listener: (value: boolean) => void) {
10
+ return this.emitter.on('change', listener)
11
+ }
12
+
13
+ protected notifyListeners = () => this.emitter.emit('change', this.getValue())
14
+ }
@@ -0,0 +1,10 @@
1
+ import type { DatabaseAdapter } from '../adapters/factory'
2
+ import { Database, } from '@nozbe/watermelondb'
3
+ import * as modelClasses from '../models'
4
+
5
+ export default function syncDatabaseFactory(adapter: () => DatabaseAdapter) {
6
+ return () => new Database({
7
+ adapter: adapter(),
8
+ modelClasses: Object.values(modelClasses)
9
+ })
10
+ }
@@ -0,0 +1,45 @@
1
+ import { SyncTelemetry } from "../telemetry/index";
2
+ import { SyncError, SyncUnexpectedError } from "./index";
3
+
4
+ /**
5
+ * Safely executes a function within a "sync boundary" — ensuring that
6
+ * any thrown or rejected errors (even from asynchronous code) are caught,
7
+ * wrapped, and reported via SyncManager telemetry.
8
+ *
9
+ * This is especially useful for code that runs "out of band" — meaning
10
+ * it's not directly part of the main sync pipeline, and errors might
11
+ * otherwise not be decorated/reported how we like (like in an generic
12
+ * app-wide global error handler).
13
+ *
14
+ * - Automatically catches both synchronous and asynchronous errors.
15
+ * - Wraps unknown errors in `SyncUnexpectedError`, including optional `context`.
16
+ * - Reports all handled errors through `SyncManager.telemetry.capture`.
17
+ *
18
+ * @param fn The function to run inside the error boundary.
19
+ * @param context Optional contextual details to include in captured errors.
20
+ * @returns The result of `fn`, or a promise that resolves to it.
21
+ */
22
+
23
+ export function inBoundary<T, TContext extends Record<string, any>>(fn: (context: TContext) => T, context?: TContext): T;
24
+ export function inBoundary<T, TContext extends Record<string, any>>(fn: (context: TContext) => Promise<T>, context?: TContext): Promise<T>;
25
+ export function inBoundary<T, TContext extends Record<string, any>>(fn: (context: TContext) => T | Promise<T>, context?: TContext): T | Promise<T> {
26
+ try {
27
+ const result = fn(context || ({} as TContext));
28
+
29
+ if (result instanceof Promise) {
30
+ return result.catch((err: unknown) => {
31
+ const wrapped = err instanceof SyncError ? err : new SyncUnexpectedError((err as Error).message, context);
32
+ SyncTelemetry.getInstance().capture(wrapped)
33
+
34
+ throw wrapped;
35
+ });
36
+ }
37
+
38
+ return result;
39
+ } catch (err: unknown) {
40
+ const wrapped = err instanceof SyncError ? err : new SyncUnexpectedError((err as Error).message, context);
41
+ SyncTelemetry.getInstance().capture(wrapped);
42
+
43
+ throw wrapped;
44
+ }
45
+ }
@@ -0,0 +1,49 @@
1
+ import SyncStore from "../store"
2
+
3
+ type ErrorDetails = Record<string, unknown>
4
+ type Without<TRecord, T extends string> = {
5
+ [K in T]?: never
6
+ } & TRecord
7
+
8
+ export class SyncError extends Error {
9
+ private _reported = false
10
+ readonly details?: ErrorDetails
11
+
12
+ constructor(message: string, details?: ErrorDetails) {
13
+ super(message)
14
+ this.name = 'SyncError'
15
+ Object.setPrototypeOf(this, new.target.prototype)
16
+
17
+ this.details = details
18
+ }
19
+
20
+ markReported() {
21
+ this._reported = true
22
+ }
23
+
24
+ isReported() {
25
+ return this._reported
26
+ }
27
+
28
+ getDetails() {
29
+ return this.details
30
+ }
31
+ }
32
+
33
+ export class SyncStoreError extends SyncError {
34
+ constructor(message: string, store: SyncStore, details?: Without<ErrorDetails, 'store'>) {
35
+ super(message, { ...details, store })
36
+ this.name = 'SyncStoreError'
37
+ Object.setPrototypeOf(this, new.target.prototype)
38
+ }
39
+ }
40
+
41
+ // useful for transforming non-sync-related errors into one
42
+ // that captures surrounding details (e.g., table name)
43
+ export class SyncUnexpectedError extends SyncError {
44
+ constructor(message: string, details?: ErrorDetails) {
45
+ super(message, details)
46
+ this.name = 'SyncUnexpectedError'
47
+ Object.setPrototypeOf(this, new.target.prototype)
48
+ }
49
+ }