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,72 @@
1
+ import { SYNC_TABLES } from '../schema'
2
+ import BaseModel from './Base'
3
+
4
+ export default class Practice extends BaseModel<{
5
+ manual_id: string | null
6
+ content_id: number | null
7
+ date: string
8
+ auto: boolean
9
+ duration_seconds: number
10
+ title: string | null
11
+ thumbnail_url: string | null
12
+ category_id: number | null
13
+ instrument_id: number | null
14
+ }> {
15
+ static table = SYNC_TABLES.PRACTICES
16
+
17
+ get manual_id() {
18
+ return this._getRaw('manual_id') as string | null
19
+ }
20
+ get content_id() {
21
+ return this._getRaw('content_id') as number | null
22
+ }
23
+ get date() {
24
+ return this._getRaw('date') as string
25
+ }
26
+ get auto() {
27
+ return this._getRaw('auto') as boolean
28
+ }
29
+ get duration_seconds() {
30
+ return this._getRaw('duration_seconds') as number
31
+ }
32
+ get title() {
33
+ return this._getRaw('title') as string | null
34
+ }
35
+ get thumbnail_url() {
36
+ return this._getRaw('thumbnail_url') as string | null
37
+ }
38
+ get category_id() {
39
+ return this._getRaw('category_id') as number | null
40
+ }
41
+ get instrument_id() {
42
+ return this._getRaw('instrument_id') as number | null
43
+ }
44
+
45
+ set manual_id(value: string | null) {
46
+ this._setRaw('manual_id', value)
47
+ }
48
+ set content_id(value: number | null) {
49
+ this._setRaw('content_id', value)
50
+ }
51
+ set date(value: string) {
52
+ this._setRaw('date', value)
53
+ }
54
+ set auto(value: boolean) {
55
+ this._setRaw('auto', value)
56
+ }
57
+ set duration_seconds(value: number) {
58
+ this._setRaw('duration_seconds', value)
59
+ }
60
+ set title(value: string | null) {
61
+ this._setRaw('title', value)
62
+ }
63
+ set thumbnail_url(value: string | null) {
64
+ this._setRaw('thumbnail_url', value)
65
+ }
66
+ set category_id(value: number | null) {
67
+ this._setRaw('category_id', value)
68
+ }
69
+ set instrument_id(value: number | null) {
70
+ this._setRaw('instrument_id', value)
71
+ }
72
+ }
@@ -0,0 +1,23 @@
1
+ import { SYNC_TABLES } from '../schema'
2
+ import BaseModel from './Base'
3
+
4
+ export default class PracticeDayNote extends BaseModel<{
5
+ date: string
6
+ notes: string
7
+ }> {
8
+ static table = SYNC_TABLES.PRACTICE_DAY_NOTES
9
+
10
+ get date() {
11
+ return this._getRaw('date') as string
12
+ }
13
+ get notes() {
14
+ return this._getRaw('notes') as string
15
+ }
16
+
17
+ set date(value: string) {
18
+ this._setRaw('date', value)
19
+ }
20
+ set notes(value: string) {
21
+ this._setRaw('notes', value)
22
+ }
23
+ }
@@ -0,0 +1,4 @@
1
+ export { default as ContentLike } from './ContentLike'
2
+ export { default as ContentProgress } from './ContentProgress'
3
+ export { default as Practice } from './Practice'
4
+ export { default as PracticeDayNote } from './PracticeDayNote'
@@ -0,0 +1,247 @@
1
+ import SyncStore from '../store'
2
+ import SyncContext from '../context'
3
+ import BaseModel from '../models/Base'
4
+ import { RecordId } from '@nozbe/watermelondb'
5
+ import type { Span } from '../telemetry/index'
6
+
7
+ import { SyncError, SyncExistsDTO, SyncReadDTO, SyncReadData, SyncWriteDTO, SyncWriteIdData, SyncWriteRecordData, SyncRemoteWriteDTO} from '..'
8
+ import { SyncPushResponse } from '../fetch'
9
+
10
+ import { Q } from '@nozbe/watermelondb'
11
+ export { Q }
12
+
13
+ export default class SyncRepository<TModel extends BaseModel> {
14
+ context: SyncContext
15
+ store: SyncStore<TModel>
16
+
17
+ constructor(store: SyncStore<TModel>) {
18
+ this.context = store.context
19
+ this.store = store
20
+ }
21
+
22
+ protected async readOne(id: RecordId) {
23
+ return this._respondToRead(() => this.store.readOne(id))
24
+ }
25
+
26
+ protected async readSome(ids: RecordId[]) {
27
+ return this._respondToRead(() => this.store.readSome(ids))
28
+ }
29
+
30
+ protected async readAll() {
31
+ return this._respondToRead(() => this.store.readAll())
32
+ }
33
+
34
+ protected async queryOne(...args: Q.Clause[]) {
35
+ return this._respondToRead(() => this.store.queryOne(...args))
36
+ }
37
+
38
+ protected async queryOneId(...args: Q.Clause[]) {
39
+ return this._respondToRead(() => this.store.queryOneId(...args))
40
+ }
41
+
42
+ protected async queryAll(...args: Q.Clause[]) {
43
+ return this._respondToRead(() => this.store.queryAll(...args))
44
+ }
45
+
46
+ protected async queryAllIds(...args: Q.Clause[]) {
47
+ return this._respondToRead(() => this.store.queryAllIds(...args))
48
+ }
49
+
50
+ protected async fetchOne(id: RecordId) {
51
+ return this._fetch(() => this.store.readOne(id))
52
+ }
53
+
54
+ protected async fetchAll() {
55
+ return this._fetch(() => this.store.readAll())
56
+ }
57
+
58
+ protected async existOne(id: RecordId) {
59
+ const r = await this.readOne(id)
60
+ return {
61
+ ...r,
62
+ data: r.data !== null
63
+ }
64
+ }
65
+
66
+ protected async existSome(ids: RecordId[]) {
67
+ const read = await this.readSome(ids)
68
+ const map = new Map<RecordId, (typeof read.data)[0]>()
69
+ read.data.forEach((record) => map.set(record.id, record))
70
+
71
+ const result: SyncExistsDTO<TModel, boolean[]> = {
72
+ ...read,
73
+ data: ids.map((id) => map.has(id)),
74
+ }
75
+ return result
76
+ }
77
+
78
+ protected async insertOne(builder: (record: TModel) => void) {
79
+ return this.store.telemetry.trace(
80
+ { name: `insertOne:${this.store.model.table}`, op: 'insert' },
81
+ (span) => this._respondToWrite(() => this.store.insertOne(builder, span), span)
82
+ )
83
+ }
84
+
85
+ protected async updateOneId(id: RecordId, builder: (record: TModel) => void) {
86
+ return this.store.telemetry.trace(
87
+ { name: `updateOne:${this.store.model.table}`, op: 'update' },
88
+ (span) => this._respondToWrite(() => this.store.updateOneId(id, builder, span), span)
89
+ )
90
+ }
91
+
92
+ protected async upsertOneRemote(id: RecordId, builder: (record: TModel) => void) {
93
+ return this.store.telemetry.trace(
94
+ { name: `upsertOneRemote:${this.store.model.table}`, op: 'upsert' },
95
+ (span) => this._respondToRemoteWriteOne(() => this.store.upsertOneRemote(id, builder, span), id, span)
96
+ )
97
+ }
98
+
99
+ protected async upsertOne(id: RecordId, builder: (record: TModel) => void) {
100
+ return this.store.telemetry.trace(
101
+ { name: `upsertOne:${this.store.model.table}`, op: 'upsert' },
102
+ (span) => this._respondToWrite(() => this.store.upsertOne(id, builder, span), span)
103
+ )
104
+ }
105
+
106
+ protected async upsertOneTentative(id: RecordId, builder: (record: TModel) => void) {
107
+ return this.store.telemetry.trace(
108
+ { name: `upsertOneTentative:${this.store.model.table}`, op: 'upsert' },
109
+ (span) => this._respondToWrite(() => this.store.upsertOneTentative(id, builder, span), span)
110
+ )
111
+ }
112
+
113
+ protected async upsertSome(builders: Record<RecordId, (record: TModel) => void>) {
114
+ return this.store.telemetry.trace(
115
+ { name: `upsertSome:${this.store.model.table}`, op: 'upsert' },
116
+ (span) => this._respondToWrite(() => this.store.upsertSome(builders, span), span)
117
+ )
118
+ }
119
+
120
+ protected async upsertSomeTentative(builders: Record<RecordId, (record: TModel) => void>) {
121
+ return this.store.telemetry.trace(
122
+ { name: `upsertSomeTentative:${this.store.model.table}`, op: 'upsert' },
123
+ (span) => this._respondToWrite(() => this.store.upsertSomeTentative(builders, span), span)
124
+ )
125
+ }
126
+
127
+ protected async deleteOne(id: RecordId) {
128
+ return this.store.telemetry.trace(
129
+ { name: `delete:${this.store.model.table}`, op: 'delete' },
130
+ (span) => this._respondToWriteIds(() => this.store.deleteOne(id, span), span)
131
+ )
132
+ }
133
+
134
+ protected async deleteSome(ids: RecordId[]) {
135
+ return this.store.telemetry.trace(
136
+ { name: `deleteSome:${this.store.model.table}`, op: 'delete' },
137
+ (span) => this._respondToWriteIds(() => this.store.deleteSome(ids, span), span)
138
+ )
139
+ }
140
+
141
+ private async _respondToWrite<T extends SyncWriteRecordData<TModel>>(create: () => Promise<T>, span?: Span) {
142
+ const data = await create()
143
+
144
+ let response: SyncPushResponse | null = null
145
+ if (!this.context.durability.getValue()) {
146
+ response = await this.store.pushRecordIdsImpatiently('id' in data ? [data.id] : data.map(r => r.id), span)
147
+
148
+ if (!response.ok) {
149
+ throw new SyncError('Failed to push records', { response })
150
+ }
151
+ }
152
+
153
+ const ret: SyncWriteDTO<TModel, T> = {
154
+ data,
155
+ status: response ? 'synced' : 'unsynced',
156
+ pushStatus: response ? 'success' : 'pending',
157
+ }
158
+ return ret
159
+ }
160
+
161
+ private async _respondToWriteIds<T extends SyncWriteIdData<TModel>>(create: () => Promise<T>, span?: Span) {
162
+ const data = await create()
163
+
164
+ let response: SyncPushResponse | null = null
165
+ if (!this.context.durability.getValue()) {
166
+ response = await this.store.pushRecordIdsImpatiently(typeof data === 'string' ? [data] : data, span)
167
+
168
+ if (!response.ok) {
169
+ throw new SyncError('Failed to push records', { response })
170
+ }
171
+ }
172
+
173
+ const ret: SyncWriteDTO<TModel, T> = {
174
+ data,
175
+ status: response ? 'synced' : 'unsynced',
176
+ pushStatus: response ? 'success' : 'pending',
177
+ }
178
+ return ret
179
+ }
180
+
181
+ private async _respondToRemoteWriteOne<T extends SyncPushResponse>(push: () => Promise<T>, id: RecordId, span?: Span) {
182
+ const response = await push()
183
+
184
+ if (!response.ok) {
185
+ throw new SyncError('Failed to push records', { response })
186
+ }
187
+
188
+ const data = await this.store.readOne(id)
189
+
190
+ const ret: SyncRemoteWriteDTO<TModel> = {
191
+ data,
192
+ status: 'synced',
193
+ pushStatus: 'success'
194
+ }
195
+ return ret
196
+ }
197
+
198
+ // read from local db, but pull (and throw (!) if it fails) if it's never been synced before
199
+ private async _respondToRead<T extends SyncReadData<TModel>>(query: () => Promise<T>) {
200
+ const fetchToken = await this.store.getLastFetchToken()
201
+ const everPulled = !!fetchToken
202
+ let pull: Awaited<ReturnType<typeof this.store.pullRecords>> | null = null
203
+
204
+ if (!everPulled) {
205
+ pull = await this.store.pullRecords()
206
+ if (!pull.ok) {
207
+ throw new SyncError('Failed to pull records', { pull })
208
+ }
209
+ }
210
+
211
+ const data = await query()
212
+
213
+ const result: SyncReadDTO<TModel, T> = {
214
+ data,
215
+ status: pull?.ok ? 'fresh' : 'stale',
216
+ pullStatus: pull?.ok ? 'success' : 'failure',
217
+ lastFetchToken: fetchToken,
218
+ }
219
+ return result
220
+ }
221
+
222
+ private async _fetch<T extends SyncReadData<TModel>>(query: () => Promise<T>) {
223
+ const [response, fetchToken] = await Promise.all([
224
+ this.store.pullRecords(),
225
+ this.store.getLastFetchToken(),
226
+ ])
227
+ const data = await query()
228
+
229
+ if (!response.ok) {
230
+ const result: SyncReadDTO<TModel, T> = {
231
+ data,
232
+ status: 'stale',
233
+ pullStatus: 'failure',
234
+ lastFetchToken: fetchToken,
235
+ }
236
+ return result
237
+ }
238
+
239
+ const result: SyncReadDTO<TModel, T> = {
240
+ data,
241
+ status: 'fresh',
242
+ pullStatus: 'success',
243
+ lastFetchToken: response.token,
244
+ }
245
+ return result
246
+ }
247
+ }
@@ -0,0 +1,26 @@
1
+ import SyncRepository from "./base";
2
+ import ContentLike from "../models/ContentLike";
3
+
4
+ export default class LikesRepository extends SyncRepository<ContentLike> {
5
+ async isLiked(contentId: number) {
6
+ return await this.existOne(LikesRepository.generateId(contentId))
7
+ }
8
+
9
+ async areLiked(contentIds: number[]) {
10
+ return await this.existSome(contentIds.map(LikesRepository.generateId))
11
+ }
12
+
13
+ async like(contentId: number) {
14
+ return await this.upsertOne(LikesRepository.generateId(contentId), r => {
15
+ r.content_id = contentId;
16
+ })
17
+ }
18
+
19
+ async unlike(contentId: number) {
20
+ return await this.deleteOne(LikesRepository.generateId(contentId))
21
+ }
22
+
23
+ private static generateId(contentId: number) {
24
+ return contentId.toString();
25
+ }
26
+ }
@@ -0,0 +1,160 @@
1
+ import SyncRepository, { Q } from './base'
2
+ import ContentProgress, { COLLECTION_TYPE, STATE } from '../models/ContentProgress'
3
+
4
+ export default class ProgressRepository extends SyncRepository<ContentProgress> {
5
+ // null collection only
6
+ async startedIds(limit?: number) {
7
+ return this.queryAll(
8
+ Q.where('collection_type', null),
9
+ Q.where('collection_id', null),
10
+
11
+ Q.where('state', STATE.STARTED),
12
+ Q.sortBy('updated_at', 'desc'),
13
+ Q.take(limit || Infinity)
14
+ )
15
+ }
16
+
17
+ // null collection only
18
+ async completedIds(limit?: number) {
19
+ return this.queryAllIds(
20
+ Q.where('state', STATE.COMPLETED),
21
+ Q.sortBy('updated_at', 'desc'),
22
+ Q.take(limit || Infinity)
23
+ )
24
+ }
25
+
26
+ // null collection only
27
+ async startedOrCompleted(opts: Parameters<typeof this.startedOrCompletedClauses>[0] = {}) {
28
+ return this.queryAll(...this.startedOrCompletedClauses(opts))
29
+ }
30
+
31
+ // null collection only
32
+ async startedOrCompletedIds(opts: Parameters<typeof this.startedOrCompletedClauses>[0] = {}) {
33
+ return this.queryAllIds(...this.startedOrCompletedClauses(opts))
34
+ }
35
+
36
+ // null collection only
37
+ private startedOrCompletedClauses(
38
+ opts: {
39
+ brand?: string
40
+ updatedAfter?: number
41
+ limit?: number
42
+ } = {}
43
+ ) {
44
+ const clauses: Q.Clause[] = [
45
+ Q.where('collection_type', null),
46
+ Q.where('collection_id', null),
47
+
48
+ Q.or(Q.where('state', STATE.STARTED), Q.where('state', STATE.COMPLETED)),
49
+ Q.sortBy('updated_at', 'desc'),
50
+ ]
51
+
52
+ if (opts.updatedAfter) {
53
+ clauses.push(Q.where('updated_at', Q.gte(opts.updatedAfter)))
54
+ }
55
+
56
+ if (opts.brand) {
57
+ clauses.push(Q.where('content_brand', opts.brand))
58
+ }
59
+
60
+ if (opts.limit) {
61
+ clauses.push(Q.take(opts.limit))
62
+ }
63
+
64
+ return clauses
65
+ }
66
+
67
+ async mostRecentlyUpdatedId(contentIds: number[], collection: { type: COLLECTION_TYPE; id: number } | null = null) {
68
+ return this.queryOneId(
69
+ Q.where('content_id', Q.oneOf(contentIds)),
70
+ Q.where('collection_type', collection?.type ?? null),
71
+ Q.where('collection_id', collection?.id ?? null),
72
+
73
+ Q.sortBy('updated_at', 'desc')
74
+ )
75
+ }
76
+
77
+ async getOneProgressByContentId(
78
+ contentId: number,
79
+ { collection }: { collection?: { type: COLLECTION_TYPE; id: number } | null } = {}
80
+ ) {
81
+ const clauses = [Q.where('content_id', contentId)]
82
+ if (typeof collection != 'undefined') {
83
+ clauses.push(
84
+ ...[
85
+ Q.where('collection_type', collection?.type ?? null),
86
+ Q.where('collection_id', collection?.id ?? null),
87
+ ]
88
+ )
89
+ }
90
+
91
+ return await this.queryOne(...clauses)
92
+ }
93
+
94
+ async getSomeProgressByContentIds(
95
+ contentIds: number[],
96
+ collection: { type: COLLECTION_TYPE; id: number } | null = null
97
+ ) {
98
+ const clauses = [Q.where('content_id', Q.oneOf(contentIds))]
99
+ if (typeof collection != 'undefined') {
100
+ clauses.push(
101
+ ...[
102
+ Q.where('collection_type', collection?.type ?? null),
103
+ Q.where('collection_id', collection?.id ?? null),
104
+ ]
105
+ )
106
+ }
107
+
108
+ return await this.queryAll(...clauses)
109
+ }
110
+
111
+ recordProgressRemotely(contentId: number, collection: { type: COLLECTION_TYPE; id: number } | null, progressPct: number, resumeTime?: number) {
112
+ const id = ProgressRepository.generateId(contentId, collection)
113
+
114
+ return this.upsertOneRemote(id, (r) => {
115
+ r.content_id = contentId
116
+ r.collection_type = collection?.type ?? null
117
+ r.collection_id = collection?.id ?? null
118
+
119
+ r.state = progressPct === 100 ? STATE.COMPLETED : STATE.STARTED
120
+ r.progress_percent = progressPct
121
+
122
+ if (typeof resumeTime != 'undefined') {
123
+ r.resume_time_seconds = Math.floor(resumeTime)
124
+ }
125
+ })
126
+ }
127
+
128
+ recordProgressesTentative(contentProgresses: Map<number, number>, collection: { type: COLLECTION_TYPE; id: number } | null) {
129
+ return this.upsertSomeTentative(
130
+ Object.fromEntries(
131
+ Array.from(contentProgresses, ([contentId, progressPct]) => [
132
+ ProgressRepository.generateId(contentId, null),
133
+ (r) => {
134
+ r.content_id = contentId
135
+ r.collection_type = collection?.type ?? null
136
+ r.collection_id = collection?.id ?? null
137
+
138
+ r.state = progressPct === 100 ? STATE.COMPLETED : STATE.STARTED
139
+ r.progress_percent = progressPct
140
+ },
141
+ ])
142
+ )
143
+ )
144
+ }
145
+
146
+ eraseProgress(contentId: number, collection: { type: COLLECTION_TYPE; id: number } | null) {
147
+ return this.deleteOne(ProgressRepository.generateId(contentId, collection))
148
+ }
149
+
150
+ private static generateId(
151
+ contentId: number,
152
+ collection: { type: COLLECTION_TYPE; id: number } | null
153
+ ) {
154
+ if (collection) {
155
+ return `${contentId}:${collection.type}:${collection.id}`
156
+ } else {
157
+ return `${contentId}`
158
+ }
159
+ }
160
+ }
@@ -0,0 +1,4 @@
1
+ export { default as ContentLikesRepository } from './content-likes'
2
+ export { default as ContentProgressRepository } from './content-progress'
3
+ export { default as PracticesRepository } from './practices'
4
+ export { default as PracticeDayNotesRepository } from './practice-day-notes'
@@ -0,0 +1,4 @@
1
+ import SyncRepository from "./base";
2
+ import PracticeDayNote from "../models/PracticeDayNote";
3
+
4
+ export default class PracticeDayNotesRepository extends SyncRepository<PracticeDayNote> {}
@@ -0,0 +1,52 @@
1
+ import SyncRepository from "./base";
2
+ import Practice from "../models/Practice";
3
+ import { RecordId } from "@nozbe/watermelondb";
4
+
5
+ export default class PracticesRepository extends SyncRepository<Practice> {
6
+ async trackAutoPractice(contentId: number, date: string, incrementalDurationSeconds: number) {
7
+ return await this.upsertOne(PracticesRepository.generateAutoId(contentId, date), r => {
8
+ r._raw.id = PracticesRepository.generateAutoId(contentId, date);
9
+ r.auto = true;
10
+ r.content_id = contentId;
11
+ r.date = date;
12
+
13
+ r.duration_seconds = Math.min((r.duration_seconds || 0) + incrementalDurationSeconds, 59999);
14
+ })
15
+ }
16
+
17
+ async recordManualPractice(date: string, durationSeconds: number, details: Partial<Pick<Practice, 'title' | 'instrument_id' | 'category_id' | 'thumbnail_url'>> = {}) {
18
+ return await this.insertOne((r) => {
19
+ const manualId = r._raw.id; // yoink watermelon's autogenerated id
20
+ r._raw.id = PracticesRepository.generateManualId(manualId);
21
+
22
+ r.manual_id = manualId;
23
+ r.auto = false;
24
+
25
+ r.date = date;
26
+ r.duration_seconds = Math.min(durationSeconds, 59999);
27
+
28
+ r.title = details.title ?? null;
29
+ r.thumbnail_url = details.thumbnail_url ?? null;
30
+ r.category_id = details.category_id ?? null;
31
+ r.instrument_id = details.instrument_id ?? null;
32
+ });
33
+ }
34
+
35
+ async updateDetails(id: RecordId, details: Partial<Pick<Practice, 'duration_seconds' | 'title' | 'thumbnail_url' | 'category_id' | 'instrument_id'>>) {
36
+ return await this.updateOneId(id, r => {
37
+ r.duration_seconds = Math.min(details.duration_seconds, 59999) ?? r.duration_seconds;
38
+ r.title = details.title ?? r.title;
39
+ r.thumbnail_url = details.thumbnail_url ?? r.thumbnail_url;
40
+ r.category_id = details.category_id ?? r.category_id;
41
+ r.instrument_id = details.instrument_id ?? r.instrument_id;
42
+ })
43
+ }
44
+
45
+ private static generateAutoId(contentId: number, date: string) {
46
+ return ['auto', contentId.toString(), date].join(':');
47
+ }
48
+
49
+ private static generateManualId(manualId: string) {
50
+ return `manual:${manualId}`;
51
+ }
52
+ }
@@ -0,0 +1,48 @@
1
+ import SyncManager from "./manager"
2
+ import { SyncError } from "./errors"
3
+
4
+ import {
5
+ ContentLikesRepository,
6
+ ContentProgressRepository,
7
+ PracticesRepository,
8
+ PracticeDayNotesRepository
9
+ } from "./repositories"
10
+ import {
11
+ ContentLike,
12
+ ContentProgress,
13
+ Practice,
14
+ PracticeDayNote
15
+ } from "./models"
16
+
17
+ interface SyncRepositories {
18
+ likes: ContentLikesRepository
19
+ contentProgress: ContentProgressRepository
20
+ practices: PracticesRepository
21
+ practiceDayNotes: PracticeDayNotesRepository
22
+ }
23
+
24
+ export default new Proxy({} as SyncRepositories, {
25
+ get(target: SyncRepositories, prop: keyof SyncRepositories) {
26
+ if (!target[prop]) {
27
+ const manager = SyncManager.getInstance()
28
+
29
+ switch (prop) {
30
+ case 'likes':
31
+ target[prop] = new ContentLikesRepository(manager.getStore(ContentLike))
32
+ break
33
+ case 'contentProgress':
34
+ target[prop] = new ContentProgressRepository(manager.getStore(ContentProgress))
35
+ break
36
+ case 'practices':
37
+ target[prop] = new PracticesRepository(manager.getStore(Practice))
38
+ break
39
+ case 'practiceDayNotes':
40
+ target[prop] = new PracticeDayNotesRepository(manager.getStore(PracticeDayNote))
41
+ break
42
+ default:
43
+ throw new SyncError(`Repository '${prop}' not found`)
44
+ }
45
+ }
46
+ return target[prop]
47
+ }
48
+ })