musora-content-services 2.124.1 → 2.126.0
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 +28 -0
- package/package.json +1 -1
- package/src/contentTypeConfig.js +1 -8
- package/src/services/forums/posts.ts +1 -16
- package/src/services/reporting/reporting.ts +1 -5
- package/src/services/sanity.js +6 -7
- package/src/services/sync/effects/index.ts +1 -0
- package/src/services/sync/effects/push-failure-notification.ts +34 -0
- package/src/services/sync/fetch.ts +83 -14
- package/src/services/sync/manager.ts +4 -0
- package/src/services/sync/retry.ts +12 -4
- package/src/services/sync/store/index.ts +26 -17
- package/src/services/sync/telemetry/index.ts +5 -3
- package/src/services/urlBuilder.ts +3 -10
- package/.claude/settings.local.json +0 -9
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
## [2.126.0](https://github.com/railroadmedia/musora-content-services/compare/v2.125.0...v2.126.0) (2026-01-29)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* adds sync store push blocking ([#754](https://github.com/railroadmedia/musora-content-services/issues/754)) ([e9a7c23](https://github.com/railroadmedia/musora-content-services/commit/e9a7c23c91f5e707c08c4e0765af52213ba6263b))
|
|
11
|
+
* melon push failure notifs ([#753](https://github.com/railroadmedia/musora-content-services/issues/753)) ([cce60ad](https://github.com/railroadmedia/musora-content-services/commit/cce60ad3e618106473fd0c7da141aebaced835d5))
|
|
12
|
+
|
|
13
|
+
## [2.125.0](https://github.com/railroadmedia/musora-content-services/compare/v2.122.7...v2.125.0) (2026-01-29)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
* **BEH-1505:** new award templates ([#740](https://github.com/railroadmedia/musora-content-services/issues/740)) ([e11f5f0](https://github.com/railroadmedia/musora-content-services/commit/e11f5f0db218132409b1d228a4c0a20500f73922))
|
|
19
|
+
* better user impersonation ([#731](https://github.com/railroadmedia/musora-content-services/issues/731)) ([768022d](https://github.com/railroadmedia/musora-content-services/commit/768022df839b2c2e206d02985c82075b0d4de008))
|
|
20
|
+
* **T3PS-1537:** Playbass Recommendations V2 ([#744](https://github.com/railroadmedia/musora-content-services/issues/744)) ([b88ffbd](https://github.com/railroadmedia/musora-content-services/commit/b88ffbd85a9982371e108d9f23190a9217cf29d0))
|
|
21
|
+
* **TP-1080:** ignore resume time until past 10s window ([#749](https://github.com/railroadmedia/musora-content-services/issues/749)) ([c22f4c2](https://github.com/railroadmedia/musora-content-services/commit/c22f4c201ec60dbf9475eebbcee199dfea967bbf))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
### Bug Fixes
|
|
25
|
+
|
|
26
|
+
* **auth:** import ([2686f30](https://github.com/railroadmedia/musora-content-services/commit/2686f3004bef54ab548ec87a2665ff918c385421))
|
|
27
|
+
* **auth:** use http client for auth functions ([#751](https://github.com/railroadmedia/musora-content-services/issues/751)) ([6958adc](https://github.com/railroadmedia/musora-content-services/commit/6958adc87689a7dd433a8c66ccaff60317e6e962))
|
|
28
|
+
* makes methodIntroComplete function send formatted date matching getDailySession ([#725](https://github.com/railroadmedia/musora-content-services/issues/725)) ([6ac47a0](https://github.com/railroadmedia/musora-content-services/commit/6ac47a04dfa2ea4a0d355409b4be8fa743af1b48))
|
|
29
|
+
* resets trickled/bubbled records that are set to 0% in progress ([#746](https://github.com/railroadmedia/musora-content-services/issues/746)) ([056949b](https://github.com/railroadmedia/musora-content-services/commit/056949bc65b61aceb39e14fd5a485ef283817108))
|
|
30
|
+
* **T3PS-1715:** lp lesson practices and activity mapping ([#732](https://github.com/railroadmedia/musora-content-services/issues/732)) ([922e455](https://github.com/railroadmedia/musora-content-services/commit/922e4558b720b17533b3923fd08939ef69afa4f2))
|
|
31
|
+
* use jump-to-post url for notifications/reporting system ([91ff74f](https://github.com/railroadmedia/musora-content-services/commit/91ff74f227c350589f9558a60a76575abb71b0e2))
|
|
32
|
+
|
|
5
33
|
### [2.124.1](https://github.com/railroadmedia/musora-content-services/compare/v2.124.0...v2.124.1) (2026-01-29)
|
|
6
34
|
|
|
7
35
|
|
package/package.json
CHANGED
package/src/contentTypeConfig.js
CHANGED
|
@@ -52,7 +52,7 @@ export const DEFAULT_FIELDS = [
|
|
|
52
52
|
"'image': thumbnail.asset->url",
|
|
53
53
|
"'thumbnail': thumbnail.asset->url",
|
|
54
54
|
'difficulty',
|
|
55
|
-
|
|
55
|
+
'difficulty_string',
|
|
56
56
|
'published_on',
|
|
57
57
|
"'type': _type",
|
|
58
58
|
"'length_in_seconds' : coalesce(length_in_seconds, soundslice[0].soundslice_length_in_second)",
|
|
@@ -760,10 +760,6 @@ export function artistOrInstructorNameAsArray(key = 'artists') {
|
|
|
760
760
|
return `'${key}': select(artist->name != null => [artist->name], instructor[]->name)`
|
|
761
761
|
}
|
|
762
762
|
|
|
763
|
-
export function difficultyStringField(key = 'difficulty_string') {
|
|
764
|
-
return `'${key}': select(difficulty_string == 'Novice' => 'Introductory', difficulty_string)`
|
|
765
|
-
}
|
|
766
|
-
|
|
767
763
|
export async function getFieldsForContentTypeWithFilteredChildren(
|
|
768
764
|
contentType,
|
|
769
765
|
asQueryString = true
|
|
@@ -829,9 +825,6 @@ const filterHandlers = {
|
|
|
829
825
|
style: (value) => `"${value}" in genre[]->name`,
|
|
830
826
|
|
|
831
827
|
difficulty: (value) => {
|
|
832
|
-
if (value === 'Introductory') {
|
|
833
|
-
return `(difficulty_string == "Novice" || difficulty_string == "Introductory")`
|
|
834
|
-
}
|
|
835
828
|
return `difficulty_string == "${value}"`
|
|
836
829
|
},
|
|
837
830
|
|
|
@@ -24,18 +24,10 @@ export interface CreatePostParams {
|
|
|
24
24
|
*/
|
|
25
25
|
export async function createPost(threadId: number, params: CreatePostParams): Promise<ForumPost> {
|
|
26
26
|
const { generateForumPostUrl } = await import('../urlBuilder.ts')
|
|
27
|
-
const { fetchThread } = await import('./threads.ts')
|
|
28
|
-
|
|
29
|
-
// Fetch thread to get category_id for URL generation
|
|
30
|
-
const thread = await fetchThread(threadId, params.brand)
|
|
31
27
|
|
|
32
28
|
// Generate forum post URL
|
|
33
29
|
const contentUrl = generateForumPostUrl({
|
|
34
30
|
brand: params.brand,
|
|
35
|
-
thread: {
|
|
36
|
-
category_id: thread.category_id,
|
|
37
|
-
id: threadId
|
|
38
|
-
}
|
|
39
31
|
}, false)
|
|
40
32
|
|
|
41
33
|
const httpClient = new HttpClient(globalConfig.baseUrl)
|
|
@@ -124,16 +116,9 @@ export async function fetchPosts(
|
|
|
124
116
|
export async function likePost(postId: number, brand: string): Promise<void> {
|
|
125
117
|
const { generateForumPostUrl } = await import('../urlBuilder.ts')
|
|
126
118
|
|
|
127
|
-
// Fetch post to get thread info for URL generation
|
|
128
|
-
const post = await fetchPost(postId, brand)
|
|
129
|
-
|
|
130
119
|
// Generate forum post URL
|
|
131
120
|
const contentUrl = generateForumPostUrl({
|
|
132
|
-
brand
|
|
133
|
-
thread: {
|
|
134
|
-
category_id: post.thread.category_id,
|
|
135
|
-
id: post.thread.id
|
|
136
|
-
}
|
|
121
|
+
brand
|
|
137
122
|
}, false)
|
|
138
123
|
|
|
139
124
|
const httpClient = new HttpClient(globalConfig.baseUrl)
|
|
@@ -127,11 +127,7 @@ export async function report<T extends ReportableType>(
|
|
|
127
127
|
|
|
128
128
|
if (post?.thread) {
|
|
129
129
|
requestBody.content_url = generateForumPostUrl({
|
|
130
|
-
brand: params.brand
|
|
131
|
-
thread: {
|
|
132
|
-
category_id: post.thread.category_id,
|
|
133
|
-
id: post.thread.id
|
|
134
|
-
}
|
|
130
|
+
brand: params.brand
|
|
135
131
|
})
|
|
136
132
|
}
|
|
137
133
|
} else if (params.type === 'comment') {
|
package/src/services/sanity.js
CHANGED
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
contentTypeConfig,
|
|
10
10
|
DEFAULT_FIELDS,
|
|
11
11
|
descriptionField,
|
|
12
|
-
difficultyStringField,
|
|
13
12
|
filtersToGroq,
|
|
14
13
|
getChildFieldsForContentType,
|
|
15
14
|
getFieldsForContentType,
|
|
@@ -332,7 +331,7 @@ export async function fetchNewReleases(
|
|
|
332
331
|
"instructor": ${instructorField},
|
|
333
332
|
"artists": instructor[]->name,
|
|
334
333
|
difficulty,
|
|
335
|
-
|
|
334
|
+
difficulty_string,
|
|
336
335
|
length_in_seconds,
|
|
337
336
|
published_on,
|
|
338
337
|
"type": _type,
|
|
@@ -370,7 +369,7 @@ export async function fetchUpcomingEvents(brand, { page = 1, limit = 10 } = {})
|
|
|
370
369
|
"artists": instructor[]->name,
|
|
371
370
|
"instructor": ${instructorField},
|
|
372
371
|
difficulty,
|
|
373
|
-
|
|
372
|
+
difficulty_string,
|
|
374
373
|
length_in_seconds,
|
|
375
374
|
published_on,
|
|
376
375
|
"type": _type,
|
|
@@ -423,7 +422,7 @@ export async function fetchScheduledReleases(brand, { page = 1, limit = 10 }) {
|
|
|
423
422
|
"instructor": ${instructorField},
|
|
424
423
|
"artists": instructor[]->name,
|
|
425
424
|
difficulty,
|
|
426
|
-
|
|
425
|
+
difficulty_string,
|
|
427
426
|
length_in_seconds,
|
|
428
427
|
published_on,
|
|
429
428
|
"type": _type,
|
|
@@ -1106,7 +1105,7 @@ export async function fetchSiblingContent(railContentId, brand = null) {
|
|
|
1106
1105
|
}).buildFilter()
|
|
1107
1106
|
|
|
1108
1107
|
const brandString = brand ? ` && brand == "${brand}"` : ''
|
|
1109
|
-
const queryFields = `_id, "id":railcontent_id, published_on, "instructor": instructor[0]->name, title, "thumbnail":thumbnail.asset->url, length_in_seconds, status, "type": _type, difficulty,
|
|
1108
|
+
const queryFields = `_id, "id":railcontent_id, published_on, "instructor": instructor[0]->name, title, "thumbnail":thumbnail.asset->url, length_in_seconds, status, "type": _type, difficulty, difficulty_string, artist->, "permission_id": permission_v2, "genre": genre[]->name, "parent_id": parent_content_data[0].id`
|
|
1110
1109
|
|
|
1111
1110
|
const query = `*[railcontent_id == ${railContentId}${brandString}]{
|
|
1112
1111
|
_type, parent_type, 'parent_id': parent_content_data[0].id, railcontent_id,
|
|
@@ -1153,7 +1152,7 @@ export async function fetchRelatedLessons(railContentId) {
|
|
|
1153
1152
|
{ showMembershipRestrictedContent: true }
|
|
1154
1153
|
).buildFilter()
|
|
1155
1154
|
|
|
1156
|
-
const queryFields = `_id, "id":railcontent_id, published_on, "instructor": instructor[0]->name, title, "thumbnail":thumbnail.asset->url, length_in_seconds, status, "type": _type, difficulty,
|
|
1155
|
+
const queryFields = `_id, "id":railcontent_id, published_on, "instructor": instructor[0]->name, title, "thumbnail":thumbnail.asset->url, length_in_seconds, status, "type": _type, difficulty, difficulty_string, railcontent_id, artist->,"permission_id": permission_v2,_type, "genre": genre[]->name`
|
|
1157
1156
|
|
|
1158
1157
|
const query = `*[railcontent_id == ${railContentId} && (!defined(permission) || references(*[_type=='permission']._id))]{
|
|
1159
1158
|
_type, parent_type, railcontent_id,
|
|
@@ -1992,7 +1991,7 @@ export async function fetchScheduledAndNewReleases(
|
|
|
1992
1991
|
${artistOrInstructorName()},
|
|
1993
1992
|
"artists": instructor[]->name,
|
|
1994
1993
|
difficulty,
|
|
1995
|
-
|
|
1994
|
+
difficulty_string,
|
|
1996
1995
|
length_in_seconds,
|
|
1997
1996
|
published_on,
|
|
1998
1997
|
"type": _type,
|
|
@@ -4,3 +4,4 @@ import type SyncStore from "../store"
|
|
|
4
4
|
export type SyncEffect = (context: SyncContext, stores: SyncStore[]) => () => void
|
|
5
5
|
|
|
6
6
|
export { default as createLogoutWarningEffect } from './logout-warning'
|
|
7
|
+
export { default as createPushFailureNotificationEffect } from './push-failure-notification'
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type SyncEffect } from '.'
|
|
2
|
+
|
|
3
|
+
const NOTIFICATION_COOLDOWN = 60_000 * 10 // 10 mins
|
|
4
|
+
const MUTE_PERIOD = 60_000 * 60 * 3 // 3 hours
|
|
5
|
+
|
|
6
|
+
const createPushFailureNotificationEffect = (notifyCallback: (opts: { mute: () => void }) => void) => {
|
|
7
|
+
let lastNotifiedAt = 0
|
|
8
|
+
let mutedUntil = 0
|
|
9
|
+
|
|
10
|
+
const mute = () => {
|
|
11
|
+
mutedUntil = Date.now() + MUTE_PERIOD
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const pushFailureToast: SyncEffect = function (_context, stores) {
|
|
15
|
+
const maybeNotify = () => {
|
|
16
|
+
const now = Date.now()
|
|
17
|
+
if (now - lastNotifiedAt < NOTIFICATION_COOLDOWN) return
|
|
18
|
+
if (mutedUntil && now < mutedUntil) return
|
|
19
|
+
|
|
20
|
+
lastNotifiedAt = now
|
|
21
|
+
notifyCallback({ mute })
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const teardowns = stores.map((store) => store.on('failedPush', maybeNotify))
|
|
25
|
+
|
|
26
|
+
return () => {
|
|
27
|
+
teardowns.forEach((teardown) => teardown())
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return pushFailureToast
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default createPushFailureNotificationEffect
|
|
@@ -4,7 +4,27 @@ import { EpochMs } from "."
|
|
|
4
4
|
import { globalConfig } from '../config.js'
|
|
5
5
|
import { RecordId } from "@nozbe/watermelondb"
|
|
6
6
|
import BaseModel from "./models/Base"
|
|
7
|
-
import
|
|
7
|
+
import SyncContext from "./context"
|
|
8
|
+
import { SyncTelemetry } from "./telemetry"
|
|
9
|
+
|
|
10
|
+
export type BlockingState = {
|
|
11
|
+
enabled: boolean
|
|
12
|
+
}
|
|
13
|
+
export type SyncPull = (
|
|
14
|
+
tableName: string,
|
|
15
|
+
intendedUserId: number,
|
|
16
|
+
context: SyncContext,
|
|
17
|
+
signal: AbortSignal,
|
|
18
|
+
previousFetchToken: SyncToken | null
|
|
19
|
+
) => Promise<SyncPullResponse>
|
|
20
|
+
export type SyncPush = (
|
|
21
|
+
tableName: string,
|
|
22
|
+
intendedUserId: number,
|
|
23
|
+
context: SyncContext,
|
|
24
|
+
payload: PushPayload,
|
|
25
|
+
signal: AbortSignal,
|
|
26
|
+
blockingState: BlockingState
|
|
27
|
+
) => Promise<SyncPushResponse>
|
|
8
28
|
|
|
9
29
|
interface RawPullResponse {
|
|
10
30
|
meta: {
|
|
@@ -23,7 +43,7 @@ interface RawPushResponse {
|
|
|
23
43
|
}
|
|
24
44
|
|
|
25
45
|
export type SyncResponse = SyncPushResponse | SyncPullResponse
|
|
26
|
-
export type SyncPushResponse = SyncPushSuccessResponse | SyncPushFetchFailureResponse | SyncPushFailureResponse
|
|
46
|
+
export type SyncPushResponse = SyncPushSuccessResponse | SyncPushFetchFailureResponse | SyncPushAbortResponse | SyncPushBlockedResponse | SyncPushFailureResponse
|
|
27
47
|
|
|
28
48
|
type SyncPushSuccessResponse = SyncResponseBase & {
|
|
29
49
|
ok: true
|
|
@@ -34,6 +54,15 @@ type SyncPushFetchFailureResponse = SyncResponseBase & {
|
|
|
34
54
|
failureType: 'fetch'
|
|
35
55
|
isRetryable: boolean
|
|
36
56
|
}
|
|
57
|
+
type SyncPushAbortResponse = SyncResponseBase & {
|
|
58
|
+
ok: false,
|
|
59
|
+
failureType: 'abort'
|
|
60
|
+
}
|
|
61
|
+
type SyncPushBlockedResponse = SyncResponseBase & {
|
|
62
|
+
ok: false,
|
|
63
|
+
failureType: 'blocked'
|
|
64
|
+
isRetryable: boolean
|
|
65
|
+
}
|
|
37
66
|
type SyncPushFailureResponse = SyncResponseBase & {
|
|
38
67
|
ok: false,
|
|
39
68
|
failureType: 'error'
|
|
@@ -63,7 +92,7 @@ interface SyncStorePushResultBase {
|
|
|
63
92
|
type: 'success' | 'failure'
|
|
64
93
|
}
|
|
65
94
|
|
|
66
|
-
export type SyncPullResponse = SyncPullSuccessResponse | SyncPullFailureResponse | SyncPullFetchFailureResponse
|
|
95
|
+
export type SyncPullResponse = SyncPullSuccessResponse | SyncPullFailureResponse | SyncPullAbortResponse | SyncPullFetchFailureResponse
|
|
67
96
|
|
|
68
97
|
type SyncPullSuccessResponse = SyncResponseBase & {
|
|
69
98
|
ok: true
|
|
@@ -82,6 +111,10 @@ type SyncPullFailureResponse = SyncResponseBase & {
|
|
|
82
111
|
failureType: 'error'
|
|
83
112
|
originalError: Error
|
|
84
113
|
}
|
|
114
|
+
type SyncPullAbortResponse = SyncResponseBase & {
|
|
115
|
+
ok: false,
|
|
116
|
+
failureType: 'abort'
|
|
117
|
+
}
|
|
85
118
|
export interface SyncResponseBase {
|
|
86
119
|
ok: boolean
|
|
87
120
|
}
|
|
@@ -118,8 +151,8 @@ interface ServerPushPayload {
|
|
|
118
151
|
}[]
|
|
119
152
|
}
|
|
120
153
|
|
|
121
|
-
export function makeFetchRequest(input: RequestInfo, init?: RequestInit): (userId: number,
|
|
122
|
-
return (userId,
|
|
154
|
+
export function makeFetchRequest(input: RequestInfo, init?: RequestInit): (userId: number, context: SyncContext) => Request {
|
|
155
|
+
return (userId, context) => new Request(globalConfig.baseUrl + input, {
|
|
123
156
|
...init,
|
|
124
157
|
headers: {
|
|
125
158
|
...init?.headers,
|
|
@@ -127,18 +160,18 @@ export function makeFetchRequest(input: RequestInfo, init?: RequestInit): (userI
|
|
|
127
160
|
'Authorization': `Bearer ${globalConfig.sessionConfig.token}`
|
|
128
161
|
} : {},
|
|
129
162
|
'Content-Type': 'application/json',
|
|
130
|
-
'X-Sync-Client-Id': session.getClientId(),
|
|
131
|
-
...(session.getSessionId() ? {
|
|
132
|
-
'X-Sync-Client-Session-Id': session.getSessionId()!
|
|
163
|
+
'X-Sync-Client-Id': context.session.getClientId(),
|
|
164
|
+
...(context.session.getSessionId() ? {
|
|
165
|
+
'X-Sync-Client-Session-Id': context.session.getSessionId()!
|
|
133
166
|
} : {}),
|
|
134
167
|
'X-Sync-Intended-User-Id': userId.toString()
|
|
135
168
|
}
|
|
136
169
|
})
|
|
137
170
|
}
|
|
138
171
|
|
|
139
|
-
export function handlePull(callback: (userId: number,
|
|
140
|
-
return async function(
|
|
141
|
-
const generatedRequest = callback(userId,
|
|
172
|
+
export function handlePull(callback: (userId: number, context: SyncContext) => Request): SyncPull {
|
|
173
|
+
return async function(_tableName, userId, context, signal, lastFetchToken) {
|
|
174
|
+
const generatedRequest = callback(userId, context)
|
|
142
175
|
const url = serializePullUrlQuery(generatedRequest.url, lastFetchToken)
|
|
143
176
|
const request = new Request(url, {
|
|
144
177
|
credentials: 'include',
|
|
@@ -150,6 +183,19 @@ export function handlePull(callback: (userId: number, session: BaseSessionProvid
|
|
|
150
183
|
try {
|
|
151
184
|
response = await fetch(request)
|
|
152
185
|
} catch (e) {
|
|
186
|
+
if (e.name === 'AbortError') {
|
|
187
|
+
return {
|
|
188
|
+
ok: false,
|
|
189
|
+
failureType: 'abort'
|
|
190
|
+
}
|
|
191
|
+
} else if (e instanceof TypeError) {
|
|
192
|
+
return {
|
|
193
|
+
ok: false,
|
|
194
|
+
failureType: 'fetch',
|
|
195
|
+
isRetryable: context.connectivity.getValue() !== false
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
153
199
|
return {
|
|
154
200
|
ok: false,
|
|
155
201
|
failureType: 'error',
|
|
@@ -190,9 +236,9 @@ export function handlePull(callback: (userId: number, session: BaseSessionProvid
|
|
|
190
236
|
}
|
|
191
237
|
}
|
|
192
238
|
|
|
193
|
-
export function handlePush(callback: (userId: number,
|
|
194
|
-
return async function(userId
|
|
195
|
-
const generatedRequest = callback(userId,
|
|
239
|
+
export function handlePush(callback: (userId: number, context: SyncContext) => Request): SyncPush {
|
|
240
|
+
return async function(tableName, userId, context, payload, signal, blockingState) {
|
|
241
|
+
const generatedRequest = callback(userId, context)
|
|
196
242
|
const serverPayload = serializePushPayload(payload)
|
|
197
243
|
const request = new Request(generatedRequest, {
|
|
198
244
|
credentials: 'include',
|
|
@@ -200,10 +246,33 @@ export function handlePush(callback: (userId: number, session: BaseSessionProvid
|
|
|
200
246
|
signal
|
|
201
247
|
})
|
|
202
248
|
|
|
249
|
+
if (blockingState.enabled) {
|
|
250
|
+
SyncTelemetry.getInstance().debug(`[sync:${tableName}] Push blocked`)
|
|
251
|
+
await new Promise(resolve => setTimeout(resolve, Math.random() * 1000 + 30))
|
|
252
|
+
return {
|
|
253
|
+
ok: false,
|
|
254
|
+
failureType: 'blocked',
|
|
255
|
+
isRetryable: true
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
203
259
|
let response: Response | null = null
|
|
204
260
|
try {
|
|
205
261
|
response = await fetch(request)
|
|
206
262
|
} catch (e) {
|
|
263
|
+
if (e.name === 'AbortError') {
|
|
264
|
+
return {
|
|
265
|
+
ok: false,
|
|
266
|
+
failureType: 'abort'
|
|
267
|
+
}
|
|
268
|
+
} else if (e instanceof TypeError) {
|
|
269
|
+
return {
|
|
270
|
+
ok: false,
|
|
271
|
+
failureType: 'fetch',
|
|
272
|
+
isRetryable: context.connectivity.getValue() !== false
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
207
276
|
return {
|
|
208
277
|
ok: false,
|
|
209
278
|
failureType: 'error',
|
|
@@ -11,7 +11,7 @@ export default class SyncRetry {
|
|
|
11
11
|
private backoffUntil = 0
|
|
12
12
|
private failureCount = 0
|
|
13
13
|
|
|
14
|
-
private unsubscribeConnectivity: () => void
|
|
14
|
+
private unsubscribeConnectivity: (() => void) | null = null
|
|
15
15
|
|
|
16
16
|
constructor(private readonly context: SyncContext, private readonly telemetry: SyncTelemetry) {}
|
|
17
17
|
|
|
@@ -26,13 +26,18 @@ export default class SyncRetry {
|
|
|
26
26
|
|
|
27
27
|
stop() {
|
|
28
28
|
this.unsubscribeConnectivity?.()
|
|
29
|
+
this.unsubscribeConnectivity = null
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
/**
|
|
32
33
|
* Runs the given syncFn with automatic retries.
|
|
33
34
|
* Returns the first successful result or the last failed result after retries.
|
|
34
35
|
*/
|
|
35
|
-
async request<T extends SyncResponse>(
|
|
36
|
+
async request<T extends SyncResponse>(
|
|
37
|
+
spanOpts: StartSpanOptions,
|
|
38
|
+
syncFn: (span: Span) => Promise<T>,
|
|
39
|
+
options: { onFail?: () => void } = {}
|
|
40
|
+
) {
|
|
36
41
|
let attempt = 0
|
|
37
42
|
|
|
38
43
|
while (true) {
|
|
@@ -64,9 +69,12 @@ export default class SyncRetry {
|
|
|
64
69
|
this.resetBackoff()
|
|
65
70
|
return result
|
|
66
71
|
} else {
|
|
67
|
-
if (
|
|
72
|
+
if ('isRetryable' in result && result.isRetryable) {
|
|
68
73
|
this.scheduleBackoff()
|
|
69
|
-
if (attempt >= this.MAX_ATTEMPTS)
|
|
74
|
+
if (attempt >= this.MAX_ATTEMPTS) {
|
|
75
|
+
options.onFail?.()
|
|
76
|
+
return result
|
|
77
|
+
}
|
|
70
78
|
} else {
|
|
71
79
|
return result
|
|
72
80
|
}
|
|
@@ -11,24 +11,19 @@ import { default as Resolver, type SyncResolution, type SyncResolverComparator }
|
|
|
11
11
|
import PushCoalescer from './push-coalescer'
|
|
12
12
|
import { SyncTelemetry, Span } from '../telemetry/index'
|
|
13
13
|
import { inBoundary } from '../errors/boundary'
|
|
14
|
-
import { BaseSessionProvider } from '../context/providers'
|
|
15
14
|
import { dropThrottle, queueThrottle, createThrottleState, type ThrottleState } from '../utils'
|
|
16
15
|
import { type WriterInterface } from '@nozbe/watermelondb/Database/WorkQueue'
|
|
17
16
|
import type LokiJSAdapter from '@nozbe/watermelondb/adapters/lokijs'
|
|
18
17
|
import { SyncError } from '../errors'
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
session: BaseSessionProvider,
|
|
29
|
-
payload: PushPayload,
|
|
30
|
-
signal: AbortSignal
|
|
31
|
-
) => Promise<SyncPushResponse>
|
|
18
|
+
import type { SyncPull, SyncPush, BlockingState } from '../fetch'
|
|
19
|
+
|
|
20
|
+
type SyncStoreEvents<TModel extends BaseModel> = {
|
|
21
|
+
upserted: [TModel[]]
|
|
22
|
+
deleted: [RecordId[]]
|
|
23
|
+
pullCompleted: []
|
|
24
|
+
pushCompleted: []
|
|
25
|
+
failedPush: []
|
|
26
|
+
}
|
|
32
27
|
|
|
33
28
|
export type SyncStoreConfig<TModel extends BaseModel = BaseModel> = {
|
|
34
29
|
model: ModelClass<TModel>
|
|
@@ -63,7 +58,9 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
|
|
|
63
58
|
private pushThrottleState: ThrottleState<SyncPushResponse>
|
|
64
59
|
private pushCoalescer = new PushCoalescer()
|
|
65
60
|
|
|
66
|
-
private
|
|
61
|
+
private pushBlockingState: BlockingState = { enabled: false }
|
|
62
|
+
|
|
63
|
+
private emitter = new EventEmitter<SyncStoreEvents<TModel>>()
|
|
67
64
|
private cleanupTimer: NodeJS.Timeout | null = null
|
|
68
65
|
|
|
69
66
|
private lastFetchTokenKey: string
|
|
@@ -146,6 +143,13 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
|
|
|
146
143
|
}, { table: this.model.table, reason })
|
|
147
144
|
}
|
|
148
145
|
|
|
146
|
+
isPushBlocked() {
|
|
147
|
+
return this.pushBlockingState.enabled
|
|
148
|
+
}
|
|
149
|
+
togglePushBlocking() {
|
|
150
|
+
this.pushBlockingState.enabled = !this.pushBlockingState.enabled
|
|
151
|
+
}
|
|
152
|
+
|
|
149
153
|
async getLastFetchToken() {
|
|
150
154
|
return (await this.db.localStorage.get<SyncToken | null>(this.lastFetchTokenKey)) ?? null
|
|
151
155
|
}
|
|
@@ -547,6 +551,11 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
|
|
|
547
551
|
}
|
|
548
552
|
|
|
549
553
|
return this.executePush(recordsToPush, span)
|
|
554
|
+
},
|
|
555
|
+
{
|
|
556
|
+
onFail: () => {
|
|
557
|
+
this.emit('failedPush')
|
|
558
|
+
},
|
|
550
559
|
}
|
|
551
560
|
)
|
|
552
561
|
})
|
|
@@ -577,7 +586,7 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
|
|
|
577
586
|
attributes: { lastFetchToken: lastFetchToken ?? undefined, ...this.context.session.toJSON() },
|
|
578
587
|
parentSpan: pullSpan,
|
|
579
588
|
},
|
|
580
|
-
() => this.puller(this.userScope.initialId, this.context
|
|
589
|
+
() => this.puller(this.model.table, this.userScope.initialId, this.context, this.runScope.signal, lastFetchToken)
|
|
581
590
|
)
|
|
582
591
|
|
|
583
592
|
if (response.ok) {
|
|
@@ -621,7 +630,7 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
|
|
|
621
630
|
attributes: { ...this.context.session.toJSON() },
|
|
622
631
|
parentSpan: pushSpan,
|
|
623
632
|
},
|
|
624
|
-
() => this.pusher(this.userScope.initialId, this.context
|
|
633
|
+
() => this.pusher(this.model.table, this.userScope.initialId, this.context, payload, this.runScope.signal, this.pushBlockingState)
|
|
625
634
|
)
|
|
626
635
|
|
|
627
636
|
if (response.ok) {
|
|
@@ -101,9 +101,9 @@ export class SyncTelemetry {
|
|
|
101
101
|
let desc = span['_spanId'].slice(0, 4)
|
|
102
102
|
desc += span['_parentSpanId'] ? ` (< ${span['_parentSpanId'].slice(0, 4)})` : ''
|
|
103
103
|
|
|
104
|
-
this.
|
|
104
|
+
this._log(SeverityLevel.DEBUG, 'info', `[trace:start] ${options.name} (${desc})`, true)
|
|
105
105
|
const result = callback(span)
|
|
106
|
-
Promise.resolve(result).finally(() => this.
|
|
106
|
+
Promise.resolve(result).finally(() => this._log(SeverityLevel.DEBUG, 'info', `[trace:end] ${options.name} (${desc})`, true))
|
|
107
107
|
|
|
108
108
|
return result
|
|
109
109
|
})
|
|
@@ -179,12 +179,14 @@ export class SyncTelemetry {
|
|
|
179
179
|
this._log(SeverityLevel.FATAL, 'error', message, extra)
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
-
_log(level: SeverityLevel, consoleMethod: 'info' | 'log' | 'warn' | 'error', message: unknown, extra?: any) {
|
|
182
|
+
_log(level: SeverityLevel, consoleMethod: 'info' | 'log' | 'warn' | 'error', message: unknown, extra?: any, skipSentry = false) {
|
|
183
183
|
if (this.level > level || this.shouldIgnoreMessage(message)) return
|
|
184
184
|
this._ignoreConsole = true
|
|
185
185
|
console[consoleMethod](...this.formattedConsoleMessage(message, extra))
|
|
186
186
|
this._ignoreConsole = false
|
|
187
187
|
|
|
188
|
+
if (skipSentry) return;
|
|
189
|
+
|
|
188
190
|
if (level >= SeverityLevel.WARNING) {
|
|
189
191
|
this.Sentry.captureMessage(message instanceof Error ? message.message : String(message), severityLevelToSentryLevel[level])
|
|
190
192
|
} else {
|
|
@@ -46,13 +46,6 @@ export interface ContentUrlParams {
|
|
|
46
46
|
export interface ForumPostUrlParams {
|
|
47
47
|
/** Brand (drumeo, pianote, etc) */
|
|
48
48
|
brand: Brand
|
|
49
|
-
/** Thread information */
|
|
50
|
-
thread: {
|
|
51
|
-
/** Thread category ID */
|
|
52
|
-
category_id: number
|
|
53
|
-
/** Thread ID */
|
|
54
|
-
id: number
|
|
55
|
-
}
|
|
56
49
|
}
|
|
57
50
|
|
|
58
51
|
/**
|
|
@@ -216,14 +209,14 @@ export function generateContentUrlWithDomain(params: ContentUrlParams): string {
|
|
|
216
209
|
* @returns Forum post URL
|
|
217
210
|
*
|
|
218
211
|
* @example
|
|
219
|
-
* generateForumPostUrl({ brand: 'drumeo'
|
|
220
|
-
* // Returns: "/drumeo/forums/
|
|
212
|
+
* generateForumPostUrl({ brand: 'drumeo'})
|
|
213
|
+
* // Returns: "/drumeo/forums/jump-to-post/"
|
|
221
214
|
*/
|
|
222
215
|
export function generateForumPostUrl(
|
|
223
216
|
post: ForumPostUrlParams,
|
|
224
217
|
withDomain: boolean = false
|
|
225
218
|
): string {
|
|
226
|
-
const path = `/${post.brand}/forums/
|
|
219
|
+
const path = `/${post.brand}/forums/jump-to-post/`
|
|
227
220
|
|
|
228
221
|
if (withDomain) {
|
|
229
222
|
return globalConfig.frontendUrl + path
|