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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "musora-content-services",
3
- "version": "2.124.1",
3
+ "version": "2.126.0",
4
4
  "description": "A package for Musoras content services ",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -52,7 +52,7 @@ export const DEFAULT_FIELDS = [
52
52
  "'image': thumbnail.asset->url",
53
53
  "'thumbnail': thumbnail.asset->url",
54
54
  'difficulty',
55
- difficultyStringField(),
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') {
@@ -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
- ${difficultyStringField()},
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
- ${difficultyStringField()},
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
- ${difficultyStringField()},
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, ${difficultyStringField()}, artist->, "permission_id": permission_v2, "genre": genre[]->name, "parent_id": parent_content_data[0].id`
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, ${difficultyStringField()}, railcontent_id, artist->,"permission_id": permission_v2,_type, "genre": genre[]->name`
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
- ${difficultyStringField()},
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 { BaseSessionProvider } from "./context/providers"
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, session: BaseSessionProvider) => Request {
122
- return (userId, session) => new Request(globalConfig.baseUrl + input, {
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, session: BaseSessionProvider) => Request) {
140
- return async function(userId: number, session: BaseSessionProvider, lastFetchToken: SyncToken | null, signal?: AbortSignal): Promise<SyncPullResponse> {
141
- const generatedRequest = callback(userId, session)
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, session: BaseSessionProvider) => Request) {
194
- return async function(userId: number, session: BaseSessionProvider, payload: PushPayload, signal?: AbortSignal): Promise<SyncPushResponse> {
195
- const generatedRequest = callback(userId, session)
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',
@@ -275,6 +275,10 @@ export default class SyncManager {
275
275
  return store as unknown as SyncStore<TModel>
276
276
  }
277
277
 
278
+ getAllStores() {
279
+ return this.storesRegistry
280
+ }
281
+
278
282
  getTelemetry() {
279
283
  return this.telemetry
280
284
  }
@@ -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>(spanOpts: StartSpanOptions, syncFn: (span: Span) => Promise<T>) {
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 (result.failureType === 'fetch' && result.isRetryable) {
72
+ if ('isRetryable' in result && result.isRetryable) {
68
73
  this.scheduleBackoff()
69
- if (attempt >= this.MAX_ATTEMPTS) return result
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
- type SyncPull = (
21
- intendedUserId: number,
22
- session: BaseSessionProvider,
23
- previousFetchToken: SyncToken | null,
24
- signal: AbortSignal
25
- ) => Promise<SyncPullResponse>
26
- type SyncPush = (
27
- intendedUserId: number,
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 emitter = new EventEmitter()
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.session, lastFetchToken, this.runScope.signal)
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.session, payload, this.runScope.signal)
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.debug(`[trace:start] ${options.name} (${desc})`)
104
+ this._log(SeverityLevel.DEBUG, 'info', `[trace:start] ${options.name} (${desc})`, true)
105
105
  const result = callback(span)
106
- Promise.resolve(result).finally(() => this.debug(`[trace:end] ${options.name} (${desc})`))
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', thread: { category_id: 12, id: 456 }})
220
- * // Returns: "/drumeo/forums/threads/12/456"
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/threads/${post.thread.category_id}/${post.thread.id}`
219
+ const path = `/${post.brand}/forums/jump-to-post/`
227
220
 
228
221
  if (withDomain) {
229
222
  return globalConfig.frontendUrl + path
@@ -1,9 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(rg:*)",
5
- "Bash(npm run lint:*)"
6
- ],
7
- "deny": []
8
- }
9
- }