musora-content-services 2.90.0 → 2.92.6

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 (177) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/package.json +11 -3
  3. package/src/index.d.ts +9 -31
  4. package/src/index.js +12 -34
  5. package/src/services/content-org/learning-paths.ts +33 -3
  6. package/src/services/contentAggregator.js +2 -2
  7. package/src/services/contentLikes.js +6 -39
  8. package/src/services/contentProgress.js +181 -479
  9. package/src/services/dataContext.js +0 -2
  10. package/src/services/progress-row/method-card.js +2 -1
  11. package/src/services/railcontent.js +12 -135
  12. package/src/services/sentry/.indexignore +0 -0
  13. package/src/services/sentry/index.ts +23 -0
  14. package/src/services/sync/.indexignore +0 -0
  15. package/src/services/sync/adapters/factory.ts +26 -0
  16. package/src/services/sync/adapters/lokijs.ts +1 -0
  17. package/src/services/sync/adapters/sqlite.ts +1 -0
  18. package/src/services/sync/concurrency-safety.ts +4 -0
  19. package/src/services/sync/context/index.ts +43 -0
  20. package/src/services/sync/context/providers/base.ts +4 -0
  21. package/src/services/sync/context/providers/connectivity.ts +14 -0
  22. package/src/services/sync/context/providers/durability.ts +5 -0
  23. package/src/services/sync/context/providers/index.ts +5 -0
  24. package/src/services/sync/context/providers/session.ts +8 -0
  25. package/src/services/sync/context/providers/tabs.ts +18 -0
  26. package/src/services/sync/context/providers/visibility.ts +14 -0
  27. package/src/services/sync/database/factory.ts +10 -0
  28. package/src/services/sync/errors/boundary.ts +45 -0
  29. package/src/services/sync/errors/index.ts +49 -0
  30. package/src/services/sync/fetch.ts +313 -0
  31. package/src/services/sync/index.ts +80 -0
  32. package/src/services/sync/manager.ts +139 -0
  33. package/src/services/sync/models/Base.ts +47 -0
  34. package/src/services/sync/models/ContentLike.ts +16 -0
  35. package/src/services/sync/models/ContentProgress.ts +69 -0
  36. package/src/services/sync/models/Practice.ts +72 -0
  37. package/src/services/sync/models/PracticeDayNote.ts +23 -0
  38. package/src/services/sync/models/index.ts +4 -0
  39. package/src/services/sync/repositories/base.ts +247 -0
  40. package/src/services/sync/repositories/content-likes.ts +26 -0
  41. package/src/services/sync/repositories/content-progress.ts +160 -0
  42. package/src/services/sync/repositories/index.ts +4 -0
  43. package/src/services/sync/repositories/practice-day-notes.ts +4 -0
  44. package/src/services/sync/repositories/practices.ts +52 -0
  45. package/src/services/sync/repository-proxy.ts +48 -0
  46. package/src/services/sync/resolver.ts +84 -0
  47. package/src/services/sync/retry.ts +88 -0
  48. package/src/services/sync/run-scope.ts +30 -0
  49. package/src/services/sync/schema/index.ts +66 -0
  50. package/src/services/sync/serializers/index.ts +2 -0
  51. package/src/services/sync/serializers/model.ts +32 -0
  52. package/src/services/sync/serializers/raw.ts +21 -0
  53. package/src/services/sync/store/index.ts +779 -0
  54. package/src/services/sync/store/push-coalescer.ts +57 -0
  55. package/src/services/sync/store-configs.ts +41 -0
  56. package/src/services/sync/strategies/base.ts +21 -0
  57. package/src/services/sync/strategies/index.ts +12 -0
  58. package/src/services/sync/strategies/initial.ts +11 -0
  59. package/src/services/sync/strategies/polling.ts +54 -0
  60. package/src/services/sync/telemetry/index.ts +140 -0
  61. package/src/services/sync/telemetry/sampling.ts +91 -0
  62. package/src/services/sync/utils/event-emitter.ts +24 -0
  63. package/src/services/sync/utils/index.ts +1 -0
  64. package/src/services/sync/utils/throttle.ts +93 -0
  65. package/src/services/sync/utils/timers.ts +9 -0
  66. package/src/services/userActivity.js +83 -148
  67. package/test/contentProgress.test.js +6 -39
  68. package/test/live/contentProgressLive.test.js +2 -31
  69. package/tools/generate-index.cjs +10 -4
  70. package/babel.config.cjs +0 -3
  71. package/docs/Content.html +0 -269
  72. package/docs/ContentOrganization.html +0 -245
  73. package/docs/Forums.html +0 -269
  74. package/docs/Gamification.html +0 -245
  75. package/docs/TestUser.html +0 -260
  76. package/docs/UserManagementSystem.html +0 -317
  77. package/docs/api_types.js.html +0 -97
  78. package/docs/config.js.html +0 -140
  79. package/docs/content-org_content-org.js.html +0 -76
  80. package/docs/content-org_guided-courses.ts.html +0 -110
  81. package/docs/content-org_learning-paths.ts.html +0 -379
  82. package/docs/content-org_playlists-types.js.html +0 -128
  83. package/docs/content-org_playlists.js.html +0 -440
  84. package/docs/content.js.html +0 -603
  85. package/docs/content_artist.ts.html +0 -206
  86. package/docs/content_content.ts.html +0 -77
  87. package/docs/content_genre.ts.html +0 -209
  88. package/docs/content_instructor.ts.html +0 -206
  89. package/docs/fonts/Montserrat/Montserrat-Bold.eot +0 -0
  90. package/docs/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
  91. package/docs/fonts/Montserrat/Montserrat-Bold.woff +0 -0
  92. package/docs/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
  93. package/docs/fonts/Montserrat/Montserrat-Regular.eot +0 -0
  94. package/docs/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
  95. package/docs/fonts/Montserrat/Montserrat-Regular.woff +0 -0
  96. package/docs/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
  97. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
  98. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +0 -978
  99. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
  100. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
  101. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
  102. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
  103. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +0 -1049
  104. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
  105. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
  106. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
  107. package/docs/forums_categories.ts.html +0 -156
  108. package/docs/forums_discussions.js.html +0 -95
  109. package/docs/forums_forum.js.html +0 -95
  110. package/docs/forums_forums.ts.html +0 -160
  111. package/docs/forums_posts.ts.html +0 -284
  112. package/docs/forums_threads.ts.html +0 -284
  113. package/docs/gamification_awards.js.html +0 -165
  114. package/docs/gamification_awards.ts.html +0 -195
  115. package/docs/gamification_gamification.js.html +0 -76
  116. package/docs/gamification_types.js.html +0 -80
  117. package/docs/global.html +0 -6019
  118. package/docs/index.html +0 -167
  119. package/docs/liveTesting.ts.html +0 -103
  120. package/docs/module-Accounts.html +0 -2283
  121. package/docs/module-Artist.html +0 -993
  122. package/docs/module-Awards.html +0 -836
  123. package/docs/module-Categories.html +0 -711
  124. package/docs/module-Config.html +0 -431
  125. package/docs/module-Content-Services-V2.html +0 -2998
  126. package/docs/module-ForumCategories.html +0 -687
  127. package/docs/module-ForumDiscussions.html +0 -370
  128. package/docs/module-Forums.html +0 -16599
  129. package/docs/module-Genre.html +0 -981
  130. package/docs/module-GuidedCourses.html +0 -108
  131. package/docs/module-Instructor.html +0 -929
  132. package/docs/module-Interests.html +0 -1066
  133. package/docs/module-LearningPaths.html +0 -2298
  134. package/docs/module-Onboarding.html +0 -882
  135. package/docs/module-Payments.html +0 -392
  136. package/docs/module-Permissions.html +0 -406
  137. package/docs/module-Playlists.html +0 -3030
  138. package/docs/module-ProgressRow.html +0 -108
  139. package/docs/module-Railcontent-Services.html +0 -6735
  140. package/docs/module-Sanity-Services.html +0 -8244
  141. package/docs/module-Sessions.html +0 -575
  142. package/docs/module-Threads.html +0 -1119
  143. package/docs/module-UserActivity.html +0 -4580
  144. package/docs/module-UserChat.html +0 -410
  145. package/docs/module-UserManagement.html +0 -1932
  146. package/docs/module-UserMemberships.html +0 -829
  147. package/docs/module-UserNotifications.html +0 -2595
  148. package/docs/module-UserProfile.html +0 -370
  149. package/docs/progress-row_method-card.js.html +0 -183
  150. package/docs/railcontent.js.html +0 -847
  151. package/docs/sanity.js.html +0 -2322
  152. package/docs/scripts/collapse.js +0 -39
  153. package/docs/scripts/commonNav.js +0 -28
  154. package/docs/scripts/linenumber.js +0 -25
  155. package/docs/scripts/nav.js +0 -12
  156. package/docs/scripts/polyfill.js +0 -4
  157. package/docs/scripts/prettify/Apache-License-2.0.txt +0 -202
  158. package/docs/scripts/prettify/lang-css.js +0 -2
  159. package/docs/scripts/prettify/prettify.js +0 -28
  160. package/docs/scripts/search.js +0 -99
  161. package/docs/styles/jsdoc.css +0 -776
  162. package/docs/styles/prettify.css +0 -80
  163. package/docs/userActivity.js.html +0 -1577
  164. package/docs/user_account.ts.html +0 -265
  165. package/docs/user_chat.js.html +0 -98
  166. package/docs/user_interests.js.html +0 -150
  167. package/docs/user_management.js.html +0 -258
  168. package/docs/user_memberships.js.html +0 -144
  169. package/docs/user_memberships.ts.html +0 -292
  170. package/docs/user_notifications.js.html +0 -374
  171. package/docs/user_onboarding.ts.html +0 -325
  172. package/docs/user_payments.ts.html +0 -146
  173. package/docs/user_permissions.js.html +0 -110
  174. package/docs/user_profile.js.html +0 -115
  175. package/docs/user_sessions.js.html +0 -170
  176. package/docs/user_types.js.html +0 -224
  177. package/docs/user_user-management-system.js.html +0 -79
@@ -0,0 +1,84 @@
1
+ import { RecordId } from "@nozbe/watermelondb";
2
+ import { SyncEntry, SyncEntryNonDeleted } from ".";
3
+ import BaseModel from "./models/Base";
4
+
5
+ export type SyncResolution = {
6
+ entriesForCreate: SyncEntry[]
7
+ tuplesForUpdate: [BaseModel, SyncEntry][]
8
+ tuplesForRestore: [BaseModel, SyncEntry][]
9
+ idsForDestroy: RecordId[]
10
+ }
11
+
12
+ export type SyncResolverComparator<T extends BaseModel = BaseModel> = (serverEntry: SyncEntryNonDeleted<T>, localModel: T) => 'SERVER' | 'LOCAL'
13
+
14
+ export const updatedAtComparator: SyncResolverComparator = (server, local) => {
15
+ return server.meta.lifecycle.updated_at >= local.updated_at ? 'SERVER' : 'LOCAL'
16
+ }
17
+ export default class SyncResolver {
18
+ private resolution: SyncResolution
19
+ private comparator: SyncResolverComparator
20
+
21
+ constructor(comparator?: SyncResolverComparator) {
22
+ this.comparator = comparator || updatedAtComparator
23
+ this.resolution = {
24
+ entriesForCreate: [],
25
+ tuplesForUpdate: [],
26
+ tuplesForRestore: [],
27
+ idsForDestroy: []
28
+ }
29
+ }
30
+
31
+ get result() {
32
+ return { ...this.resolution }
33
+ }
34
+
35
+ againstNone(server: SyncEntry) {
36
+ if (!server.meta.lifecycle.deleted_at) {
37
+ this.resolution.entriesForCreate.push(server)
38
+ }
39
+ }
40
+
41
+ againstSynced(local: BaseModel, server: SyncEntry) {
42
+ if (server.meta.lifecycle.deleted_at) {
43
+ this.resolution.idsForDestroy.push(local.id)
44
+ }
45
+ // take care that the server stamp isn't older than the current local
46
+ // (imagine a race condition where a pull request resolves long after a second one)
47
+ else if (this.comparator(server as SyncEntryNonDeleted<BaseModel>, local) !== 'LOCAL') {
48
+ this.resolution.tuplesForUpdate.push([local, server])
49
+ }
50
+ }
51
+
52
+ // can happen if one tab notifies another of a created record, pushes to server, and other tab pulls
53
+ againstCreated(local: BaseModel, server: SyncEntry) {
54
+ if (server.meta.lifecycle.deleted_at) {
55
+ // delete local even though user has newer changes
56
+ // (we don't ever try to resurrect records here)
57
+ this.resolution.idsForDestroy.push(local.id)
58
+ } else if (this.comparator(server as SyncEntryNonDeleted<BaseModel>, local) !== 'LOCAL') {
59
+ // local is older, so update it with server's
60
+ this.resolution.tuplesForUpdate.push([local, server])
61
+ }
62
+ }
63
+
64
+ againstUpdated(local: BaseModel, server: SyncEntry) {
65
+ if (server.meta.lifecycle.deleted_at) {
66
+ // delete local even though user has newer changes
67
+ // (we don't ever try to resurrect records here)
68
+ this.resolution.idsForDestroy.push(local.id);
69
+ } else if (this.comparator(server as SyncEntryNonDeleted<BaseModel>, local) !== 'LOCAL') {
70
+ // local is older, so update it with server's
71
+ this.resolution.tuplesForUpdate.push([local, server])
72
+ }
73
+ }
74
+
75
+ againstDeleted(local: BaseModel, server: SyncEntry) {
76
+ if (server.meta.lifecycle.deleted_at) {
77
+ this.resolution.idsForDestroy.push(local.id)
78
+ } else if (server.meta.lifecycle.updated_at >= local.updated_at) {
79
+ this.resolution.tuplesForRestore.push([local, server])
80
+ } else {
81
+ this.resolution.idsForDestroy.push(local.id);
82
+ }
83
+ }
84
+ }
@@ -0,0 +1,88 @@
1
+ import SyncContext from "./context"
2
+ import { SyncResponse } from "./fetch"
3
+ import { SyncTelemetry, Span, StartSpanOptions } from "./telemetry/index"
4
+
5
+ export default class SyncRetry {
6
+ private readonly BASE_BACKOFF = 1_000
7
+ private readonly MAX_BACKOFF = 8_000
8
+ private readonly MAX_ATTEMPTS = 4
9
+
10
+ private paused = false
11
+ private backoffUntil = 0
12
+ private failureCount = 0
13
+
14
+ private unsubscribeConnectivity: () => void
15
+
16
+ constructor(private readonly context: SyncContext, private readonly telemetry: SyncTelemetry) {}
17
+
18
+ start() {
19
+ this.unsubscribeConnectivity = this.context.connectivity.subscribe(isOnline => {
20
+ if (isOnline && this.paused) {
21
+ this.paused = false
22
+ this.resetBackoff()
23
+ }
24
+ })
25
+ }
26
+
27
+ stop() {
28
+ this.unsubscribeConnectivity?.()
29
+ }
30
+
31
+ /**
32
+ * Runs the given syncFn with automatic retries.
33
+ * Returns the first successful result or the last failed result after retries.
34
+ */
35
+ async request<T extends SyncResponse>(spanOpts: StartSpanOptions, syncFn: (span: Span) => Promise<T>) {
36
+ let attempt = 0
37
+
38
+ while (true) {
39
+ if (!this.context.connectivity.getValue()) {
40
+ this.telemetry.debug('[Retry] No connectivity - skipping')
41
+ this.paused = true
42
+ return { ok: false } as T
43
+ }
44
+
45
+ const now = Date.now()
46
+ if (now < this.backoffUntil) {
47
+ await this.sleep(this.backoffUntil - now)
48
+ }
49
+
50
+ attempt++
51
+
52
+ const spanOptions = { ...spanOpts, name: `${spanOpts.name}:attempt:${attempt}/${this.MAX_ATTEMPTS}`, op: `${spanOpts.op}:attempt` }
53
+ const result = await this.telemetry.trace(spanOptions, span => syncFn(span))
54
+
55
+ if (result.ok) {
56
+ this.resetBackoff()
57
+ return result
58
+ } else {
59
+ this.scheduleBackoff()
60
+ if (attempt >= this.MAX_ATTEMPTS) return result
61
+ }
62
+ }
63
+ }
64
+
65
+ private resetBackoff() {
66
+ if (this.backoffUntil !== 0 || this.failureCount !== 0) {
67
+ this.telemetry.debug('[Retry] Resetting backoff')
68
+ this.backoffUntil = 0
69
+ this.failureCount = 0
70
+ }
71
+ }
72
+
73
+ private scheduleBackoff() {
74
+ this.failureCount++
75
+
76
+ const exponentialDelay = this.BASE_BACKOFF * Math.pow(2, this.failureCount - 1)
77
+ const jitter = exponentialDelay * 0.25 * (Math.random() - 0.5)
78
+ const delayWithJitter = exponentialDelay + jitter
79
+
80
+ this.backoffUntil = Date.now() + Math.min(this.MAX_BACKOFF, delayWithJitter)
81
+
82
+ this.telemetry.debug('[Retry] Scheduling backoff', { failureCount: this.failureCount, backoffUntil: this.backoffUntil })
83
+ }
84
+
85
+ private sleep(ms: number) {
86
+ return new Promise(resolve => setTimeout(resolve, ms))
87
+ }
88
+ }
@@ -0,0 +1,30 @@
1
+ export default class SyncRunScope {
2
+ private abortController: AbortController
3
+
4
+ constructor() {
5
+ this.abortController = new AbortController()
6
+ }
7
+
8
+ get signal(): AbortSignal {
9
+ return this.abortController.signal
10
+ }
11
+
12
+ abort(): void {
13
+ this.abortController.abort()
14
+ }
15
+
16
+ abortable<T>(fn: () => Promise<T>): Promise<T> {
17
+ return new Promise((resolve, reject) => {
18
+ if (this.signal.aborted) {
19
+ reject(this.signal.reason)
20
+ return
21
+ }
22
+
23
+ fn().then(resolve).catch(reject)
24
+
25
+ this.signal.addEventListener('abort', () => {
26
+ reject(this.signal.reason)
27
+ })
28
+ })
29
+ }
30
+ }
@@ -0,0 +1,66 @@
1
+ import { appSchema, tableSchema } from '@nozbe/watermelondb'
2
+
3
+ export const SYNC_TABLES = {
4
+ CONTENT_LIKES: 'content_likes',
5
+ CONTENT_PROGRESS: 'progress',
6
+ PRACTICES: 'practices',
7
+ PRACTICE_DAY_NOTES: 'practice_day_notes'
8
+ }
9
+
10
+ const contentLikesTable = tableSchema({
11
+ name: SYNC_TABLES.CONTENT_LIKES,
12
+ columns: [
13
+ { name: 'content_id', type: 'number', isIndexed: true },
14
+ { name: 'created_at', type: 'number' },
15
+ { name: 'updated_at', type: 'number' }
16
+ ]
17
+ })
18
+ const contentProgressTable = tableSchema({
19
+ name: SYNC_TABLES.CONTENT_PROGRESS,
20
+ columns: [
21
+ { name: 'content_id', type: 'number', isIndexed: true },
22
+ { name: 'content_brand', type: 'string', isIndexed: true },
23
+ { name: 'collection_type', type: 'string', isOptional: true, isIndexed: true },
24
+ { name: 'collection_id', type: 'number', isOptional: true, isIndexed: true },
25
+ { name: 'state', type: 'string', isIndexed: true },
26
+ { name: 'progress_percent', type: 'number' },
27
+ { name: 'resume_time_seconds', type: 'number' },
28
+ { name: 'created_at', type: 'number' },
29
+ { name: 'updated_at', type: 'number', isIndexed: true }
30
+ ]
31
+ })
32
+ const practicesTable = tableSchema({
33
+ name: SYNC_TABLES.PRACTICES,
34
+ columns: [
35
+ { name: 'manual_id', type: 'string', isOptional: true },
36
+ { name: 'content_id', type: 'number', isOptional: true, isIndexed: true },
37
+ { name: 'date', type: 'string', isIndexed: true },
38
+ { name: 'auto', type: 'boolean', isIndexed: true },
39
+ { name: 'duration_seconds', type: 'number' },
40
+ { name: 'title', type: 'string', isOptional: true },
41
+ { name: 'thumbnail_url', type: 'string', isOptional: true },
42
+ { name: 'category_id', type: 'number', isOptional: true },
43
+ { name: 'instrument_id', type: 'number', isOptional: true },
44
+ { name: 'created_at', type: 'number' },
45
+ { name: 'updated_at', type: 'number', isIndexed: true }
46
+ ]
47
+ })
48
+ const practiceDayNotesTable = tableSchema({
49
+ name: SYNC_TABLES.PRACTICE_DAY_NOTES,
50
+ columns: [
51
+ { name: 'date', type: 'string', isIndexed: true },
52
+ { name: 'notes', type: 'string' },
53
+ { name: 'created_at', type: 'number' },
54
+ { name: 'updated_at', type: 'number', isIndexed: true }
55
+ ]
56
+ })
57
+
58
+ export default appSchema({
59
+ version: 1,
60
+ tables: [
61
+ contentLikesTable,
62
+ contentProgressTable,
63
+ practicesTable,
64
+ practiceDayNotesTable
65
+ ]
66
+ })
@@ -0,0 +1,2 @@
1
+ export { default as RawSerializer, type RawSerialized } from './raw'
2
+ export { default as ModelSerializer, type ModelSerialized } from './model'
@@ -0,0 +1,32 @@
1
+ import { Model, RecordId } from "@nozbe/watermelondb"
2
+ import BaseModel from "../models/Base"
3
+
4
+ export type ModelSerialized<TModel extends Model> = ExtractGetters<TModel>
5
+ type ExtractGetters<T> = {
6
+ [K in keyof T as T[K] extends Function ? never : K]: T[K];
7
+ } & { id: RecordId }
8
+
9
+ // serializes a record to a POJO based on its model getters
10
+ // (essentially strips out all watermelon properties)
11
+ // useful for consumption in components, etc.
12
+
13
+ export default class ModelSerializer<TModel extends Model = Model> {
14
+ toPlainObject(record: TModel) {
15
+ const proto = Object.getPrototypeOf(record)
16
+ const keys = [
17
+ ...Object.getOwnPropertyNames(proto),
18
+ ...Object.getOwnPropertyNames(BaseModel.prototype)
19
+ ]
20
+
21
+ const result = {}
22
+ for (const key of keys) {
23
+ const desc = Object.getOwnPropertyDescriptor(proto, key) || Object.getOwnPropertyDescriptor(BaseModel.prototype, key)
24
+ if (desc?.get) {
25
+ result[key] = desc.get.call(record)
26
+ }
27
+ }
28
+ result['id'] = record.id
29
+
30
+ return result as ModelSerialized<TModel>
31
+ }
32
+ }
@@ -0,0 +1,21 @@
1
+ import { Model } from "@nozbe/watermelondb"
2
+
3
+ export type RawSerialized<TModel extends Model> = Omit<TModel['_raw'], '_changed' | '_status'>
4
+
5
+ // serializes a record to a POJO based on its _raw attributes
6
+ // useful for sending to back-end for sync
7
+
8
+ export default class RawSerializer<TModel extends Model = Model> {
9
+ toPlainObject(model: TModel) {
10
+ const result = {}
11
+ const raw = model._raw
12
+
13
+ for (const key in raw) {
14
+ if (key !== '_changed' && key !== '_status') {
15
+ result[key] = raw[key]
16
+ }
17
+ }
18
+
19
+ return result as RawSerialized<TModel>
20
+ }
21
+ }