musora-content-services 2.90.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.
- package/CHANGELOG.md +24 -0
- package/docs/ContentOrganization.html +2 -2
- package/docs/Forums.html +2 -2
- package/docs/Gamification.html +2 -2
- package/docs/TestUser.html +2 -2
- package/docs/UserManagementSystem.html +2 -2
- package/docs/api_types.js.html +2 -2
- package/docs/config.js.html +2 -2
- package/docs/content-org_content-org.js.html +2 -2
- package/docs/content-org_guided-courses.ts.html +2 -2
- package/docs/content-org_learning-paths.ts.html +52 -40
- package/docs/content-org_playlists-types.js.html +2 -2
- package/docs/content-org_playlists.js.html +2 -2
- package/docs/content.js.html +2 -2
- package/docs/content_artist.ts.html +2 -2
- package/docs/content_genre.ts.html +2 -2
- package/docs/content_instructor.ts.html +2 -2
- package/docs/forums_categories.ts.html +2 -2
- package/docs/forums_forums.ts.html +2 -2
- package/docs/forums_posts.ts.html +2 -2
- package/docs/forums_threads.ts.html +2 -2
- package/docs/gamification_awards.ts.html +2 -2
- package/docs/gamification_gamification.js.html +2 -2
- package/docs/global.html +2 -2
- package/docs/index.html +2 -2
- package/docs/liveTesting.ts.html +2 -2
- package/docs/module-Accounts.html +2 -2
- package/docs/module-Artist.html +2 -2
- package/docs/module-Awards.html +2 -2
- package/docs/module-Config.html +2 -2
- package/docs/module-Content-Services-V2.html +2 -2
- package/docs/module-Forums.html +2 -2
- package/docs/module-Genre.html +2 -2
- package/docs/module-GuidedCourses.html +2 -2
- package/docs/module-Instructor.html +2 -2
- package/docs/module-Interests.html +2 -2
- package/docs/module-LearningPaths.html +269 -143
- package/docs/module-Onboarding.html +3 -3
- package/docs/module-Payments.html +2 -2
- package/docs/module-Permissions.html +2 -2
- package/docs/module-Playlists.html +2 -2
- package/docs/module-ProgressRow.html +2 -2
- package/docs/module-Railcontent-Services.html +34 -893
- package/docs/module-Sanity-Services.html +2 -2
- package/docs/module-Sessions.html +2 -2
- package/docs/module-UserActivity.html +70 -116
- package/docs/module-UserChat.html +2 -2
- package/docs/module-UserManagement.html +2 -2
- package/docs/module-UserMemberships.html +2 -2
- package/docs/module-UserNotifications.html +2 -2
- package/docs/module-UserProfile.html +2 -2
- package/docs/progress-row_method-card.js.html +3 -2
- package/docs/railcontent.js.html +14 -137
- package/docs/sanity.js.html +2 -2
- package/docs/userActivity.js.html +85 -150
- package/docs/user_account.ts.html +2 -2
- package/docs/user_chat.js.html +2 -2
- package/docs/user_interests.js.html +2 -2
- package/docs/user_management.js.html +2 -2
- package/docs/user_memberships.ts.html +2 -2
- package/docs/user_notifications.js.html +2 -2
- package/docs/user_onboarding.ts.html +10 -6
- package/docs/user_payments.ts.html +2 -2
- package/docs/user_permissions.js.html +2 -2
- package/docs/user_profile.js.html +2 -2
- package/docs/user_sessions.js.html +2 -2
- package/docs/user_types.js.html +2 -2
- package/docs/user_user-management-system.js.html +2 -2
- package/package.json +11 -3
- package/src/index.d.ts +7 -31
- package/src/index.js +10 -34
- package/src/services/content-org/learning-paths.ts +30 -0
- package/src/services/contentAggregator.js +2 -2
- package/src/services/contentLikes.js +6 -39
- package/src/services/contentProgress.js +181 -479
- package/src/services/dataContext.js +0 -2
- package/src/services/progress-row/method-card.js +1 -0
- package/src/services/railcontent.js +12 -135
- package/src/services/sentry/.indexignore +0 -0
- package/src/services/sentry/index.ts +23 -0
- package/src/services/sync/.indexignore +0 -0
- package/src/services/sync/adapters/factory.ts +26 -0
- package/src/services/sync/adapters/lokijs.ts +1 -0
- package/src/services/sync/adapters/sqlite.ts +1 -0
- package/src/services/sync/concurrency-safety.ts +4 -0
- package/src/services/sync/context/index.ts +43 -0
- package/src/services/sync/context/providers/base.ts +4 -0
- package/src/services/sync/context/providers/connectivity.ts +14 -0
- package/src/services/sync/context/providers/durability.ts +5 -0
- package/src/services/sync/context/providers/index.ts +5 -0
- package/src/services/sync/context/providers/session.ts +8 -0
- package/src/services/sync/context/providers/tabs.ts +18 -0
- package/src/services/sync/context/providers/visibility.ts +14 -0
- package/src/services/sync/database/factory.ts +10 -0
- package/src/services/sync/errors/boundary.ts +45 -0
- package/src/services/sync/errors/index.ts +49 -0
- package/src/services/sync/fetch.ts +310 -0
- package/src/services/sync/index.ts +80 -0
- package/src/services/sync/manager.ts +139 -0
- package/src/services/sync/models/Base.ts +47 -0
- package/src/services/sync/models/ContentLike.ts +16 -0
- package/src/services/sync/models/ContentProgress.ts +69 -0
- package/src/services/sync/models/Practice.ts +72 -0
- package/src/services/sync/models/PracticeDayNote.ts +23 -0
- package/src/services/sync/models/index.ts +4 -0
- package/src/services/sync/repositories/base.ts +247 -0
- package/src/services/sync/repositories/content-likes.ts +26 -0
- package/src/services/sync/repositories/content-progress.ts +160 -0
- package/src/services/sync/repositories/index.ts +4 -0
- package/src/services/sync/repositories/practice-day-notes.ts +4 -0
- package/src/services/sync/repositories/practices.ts +52 -0
- package/src/services/sync/repository-proxy.ts +48 -0
- package/src/services/sync/resolver.ts +84 -0
- package/src/services/sync/retry.ts +88 -0
- package/src/services/sync/run-scope.ts +30 -0
- package/src/services/sync/schema/index.ts +66 -0
- package/src/services/sync/serializers/index.ts +2 -0
- package/src/services/sync/serializers/model.ts +32 -0
- package/src/services/sync/serializers/raw.ts +21 -0
- package/src/services/sync/store/index.ts +779 -0
- package/src/services/sync/store/push-coalescer.ts +57 -0
- package/src/services/sync/store-configs.ts +41 -0
- package/src/services/sync/strategies/base.ts +21 -0
- package/src/services/sync/strategies/index.ts +12 -0
- package/src/services/sync/strategies/initial.ts +11 -0
- package/src/services/sync/strategies/polling.ts +54 -0
- package/src/services/sync/telemetry/index.ts +140 -0
- package/src/services/sync/telemetry/sampling.ts +91 -0
- package/src/services/sync/utils/event-emitter.ts +24 -0
- package/src/services/sync/utils/index.ts +1 -0
- package/src/services/sync/utils/throttle.ts +93 -0
- package/src/services/sync/utils/timers.ts +9 -0
- package/src/services/userActivity.js +83 -148
- package/test/contentProgress.test.js +6 -39
- package/test/live/contentProgressLive.test.js +2 -31
- package/tools/generate-index.cjs +10 -4
- package/babel.config.cjs +0 -3
|
@@ -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,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,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
|
+
})
|