musora-content-services 2.135.3 → 2.136.1

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,20 @@
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.136.1](https://github.com/railroadmedia/musora-content-services/compare/v2.136.0...v2.136.1) (2026-02-19)
6
+
7
+ ## [2.136.0](https://github.com/railroadmedia/musora-content-services/compare/v2.135.3...v2.136.0) (2026-02-19)
8
+
9
+
10
+ ### Features
11
+
12
+ * melon configurable purge grace period ([#816](https://github.com/railroadmedia/musora-content-services/issues/816)) ([f87eb21](https://github.com/railroadmedia/musora-content-services/commit/f87eb21a3cc9eac28f146b0793b4bf7136bd8d0c))
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+ * allows strategies to ask for pull only instead of pull+push ([#817](https://github.com/railroadmedia/musora-content-services/issues/817)) ([c8aa114](https://github.com/railroadmedia/musora-content-services/commit/c8aa1147f218de9ff9d55d1929049de977bdaaeb))
18
+
5
19
  ### [2.135.3](https://github.com/railroadmedia/musora-content-services/compare/v2.135.2...v2.135.3) (2026-02-19)
6
20
 
7
21
  ### [2.135.2](https://github.com/railroadmedia/musora-content-services/compare/v2.135.1...v2.135.2) (2026-02-19)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "musora-content-services",
3
- "version": "2.135.3",
3
+ "version": "2.136.1",
4
4
  "description": "A package for Musoras content services ",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -1496,13 +1496,13 @@ export async function fetchSanity(
1496
1496
  if (result.result) {
1497
1497
  let results = isList ? result.result : result.result[0]
1498
1498
  if (!results) {
1499
- throw new Error('No results found')
1499
+ return null
1500
1500
  }
1501
1501
  results = processNeedAccess ? await needsAccessDecorator(results, userPermissions) : results
1502
1502
  results = processPageType ? pageTypeDecorator(results) : results
1503
1503
  return customPostProcess ? customPostProcess(results) : results
1504
1504
  } else {
1505
- throw new Error('No results found')
1505
+ return null
1506
1506
  }
1507
1507
  } catch (error) {
1508
1508
  console.error('fetchSanity: Fetch error:', { error, query })
@@ -1656,7 +1656,7 @@ export async function fetchMetadata(brand, type, options = {}) {
1656
1656
  export async function fetchChatAndLiveEnvent(brand, forcedId = null) {
1657
1657
  const liveEvent =
1658
1658
  forcedId !== null ? await fetchByRailContentIds([forcedId]) : [await fetchLiveEvent(brand)]
1659
- if (liveEvent.length === 0 || (liveEvent.length === 1 && liveEvent[0] === undefined)) {
1659
+ if (liveEvent.length === 0 || (liveEvent.length === 1 && !liveEvent[0])) {
1660
1660
  return null
1661
1661
  }
1662
1662
  let url = `/content/live-chat?brand=${brand}`
@@ -158,8 +158,10 @@ export default class SyncManager {
158
158
  strategies.forEach((strategy) => {
159
159
  models.forEach((model) => {
160
160
  const store = this.storesRegistry[model.table]
161
- strategy.onTrigger(store, (reason) => {
162
- store.requestSync(reason)
161
+ strategy.onTrigger(store, {
162
+ callback: (reason) => store.requestSync(reason),
163
+ requestSync: (reason) => store.requestSync(reason),
164
+ requestPull: (reason) => store.requestPull(reason),
163
165
  })
164
166
  })
165
167
  strategy.start()
@@ -30,12 +30,12 @@ export type SyncStoreConfig<TModel extends BaseModel = BaseModel> = {
30
30
  comparator?: TModel extends BaseModel ? SyncResolverComparator<TModel> : SyncResolverComparator
31
31
  pull: SyncPull
32
32
  push: SyncPush
33
+ purgeGracePeriod?: EpochMs
33
34
  }
34
35
 
35
36
  export default class SyncStore<TModel extends BaseModel = BaseModel> {
36
37
  static readonly PULL_THROTTLE_INTERVAL = 2_000
37
38
  static readonly PUSH_THROTTLE_INTERVAL = 1_000
38
- static readonly DELETED_RECORD_GRACE_PERIOD = 60_000 // 60s
39
39
  static readonly CLEANUP_INTERVAL = 60_000 * 60 // 1hr
40
40
 
41
41
  readonly telemetry: SyncTelemetry
@@ -58,6 +58,7 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
58
58
  private pushThrottleState: ThrottleState<SyncPushResponse>
59
59
  private pushCoalescer = new PushCoalescer()
60
60
 
61
+ private purgeGracePeriod: EpochMs
61
62
  private pushBlockingState: BlockingState = { enabled: false }
62
63
 
63
64
  private emitter = new EventEmitter<SyncStoreEvents<TModel>>()
@@ -66,7 +67,7 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
66
67
  private lastFetchTokenKey: string
67
68
 
68
69
  constructor(
69
- { model, comparator, pull, push }: SyncStoreConfig<TModel>,
70
+ { model, comparator, pull, push, purgeGracePeriod }: SyncStoreConfig<TModel>,
70
71
  userScope: SyncUserScope,
71
72
  context: SyncContext,
72
73
  db: Database,
@@ -90,6 +91,8 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
90
91
  this.pushThrottleState = createThrottleState(SyncStore.PUSH_THROTTLE_INTERVAL)
91
92
  this.pullThrottleState = createThrottleState(SyncStore.PULL_THROTTLE_INTERVAL)
92
93
 
94
+ this.purgeGracePeriod = purgeGracePeriod ?? (0 as EpochMs)
95
+
93
96
  this.puller = pull
94
97
  this.pusher = push
95
98
  this.lastFetchTokenKey = `last_fetch_token:${this.model.table}`
@@ -141,6 +144,21 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
141
144
  }, { table: this.model.table, reason })
142
145
  }
143
146
 
147
+ /**
148
+ * Request a retrying pull - does not return
149
+ */
150
+ async requestPull(reason: string) {
151
+ inBoundary(ctx => {
152
+ this.telemetry.trace(
153
+ { name: `sync:${this.model.table}`, op: 'pull-request', attributes: { ...ctx, ...this.context.session.toJSON() } },
154
+ async span => await this.pullRecordsWithRetry(span)
155
+ )
156
+ }, { table: this.model.table, reason })
157
+ }
158
+
159
+ /**
160
+ * Request a retrying push - does not return
161
+ */
144
162
  async requestPush(reason: string) {
145
163
  inBoundary(ctx => {
146
164
  this.telemetry.trace(
@@ -525,6 +543,9 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
525
543
  )()
526
544
  }
527
545
 
546
+ // TODO - slight annoying bug that occurs with purge grace period
547
+ // since we send ALL unsynced records, any that are dirty deleted
548
+ // will keep on getting sent multiple times for as long as the grace period is active
528
549
  public async pushUnsyncedWithRetry(span?: Span, cause?: string | Record<string, string>) {
529
550
  const records = await this.queryMaybeDeletedRecords(Q.where('_status', Q.notEq('synced')))
530
551
 
@@ -907,7 +928,7 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
907
928
  const record = existingRecordsMap.get(id)
908
929
  if (!record) return true // If no record found, safe to destroy
909
930
 
910
- const gracePeriodAgo = Date.now() - SyncStore.DELETED_RECORD_GRACE_PERIOD
931
+ const gracePeriodAgo = Date.now() - this.purgeGracePeriod
911
932
  return record.updated_at < gracePeriodAgo
912
933
  })
913
934
  .map((id) => {
@@ -999,7 +1020,7 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
999
1020
  * (after a server push), but instead every hour or so)
1000
1021
  */
1001
1022
  private async cleanupOldDeletedRecords(writer: WriterInterface) {
1002
- const gracePeriodAgo = Date.now() - SyncStore.DELETED_RECORD_GRACE_PERIOD
1023
+ const gracePeriodAgo = Date.now() - this.purgeGracePeriod
1003
1024
 
1004
1025
  const oldDeletedRecords = await writer.callReader(() => this.queryMaybeDeletedRecords(
1005
1026
  Q.where('_status', 'deleted'),
@@ -3,6 +3,7 @@ import { ContentLike, ContentProgress, Practice, UserAwardProgress, PracticeDayN
3
3
  import { handlePull, handlePush, makeFetchRequest } from "./fetch"
4
4
 
5
5
  import type BaseModel from "./models/Base"
6
+ import { EpochMs } from "./index"
6
7
 
7
8
  // keeps type-safety in each entry
8
9
  const c = <TModel extends BaseModel>(config: SyncStoreConfig<TModel>) => config
@@ -32,12 +33,14 @@ export default function createStoresFromConfig() {
32
33
  model: Practice,
33
34
  pull: handlePull(makeFetchRequest('/api/user/practices/v1')),
34
35
  push: handlePush(makeFetchRequest('/api/user/practices/v1', { method: 'POST' })),
36
+ purgeGracePeriod: 12_000 as EpochMs // delete undo toast duration is 10s
35
37
  }),
36
38
 
37
39
  c({
38
40
  model: PracticeDayNote,
39
41
  pull: handlePull(makeFetchRequest('/api/user/practices/v1/notes')),
40
42
  push: handlePush(makeFetchRequest('/api/user/practices/v1/notes', { method: 'POST' })),
43
+ purgeGracePeriod: 12_000 as EpochMs // delete undo toast duration is 10s
41
44
  }),
42
45
 
43
46
  c({
@@ -2,8 +2,18 @@ import { SyncStrategy } from "./index";
2
2
  import SyncContext from "../context";
3
3
  import SyncStore from "../store";
4
4
 
5
+ type SyncCallback = (reason: string) => void
6
+
7
+ type SyncCallbacks = {
8
+ callback: SyncCallback
9
+ requestSync: SyncCallback
10
+ requestPull: SyncCallback
11
+ }
12
+
13
+ type RegistryEntry = SyncCallbacks & { store: SyncStore }
14
+
5
15
  export default abstract class BaseSyncStrategy implements SyncStrategy {
6
- protected registry: { callback: (reason: string) => void, store: SyncStore }[] = []
16
+ protected registry: RegistryEntry[] = []
7
17
  abstract start(): void
8
18
  abstract stop(): void
9
19
 
@@ -14,8 +24,8 @@ export default abstract class BaseSyncStrategy implements SyncStrategy {
14
24
  this.registry = []
15
25
  }
16
26
 
17
- onTrigger(store: SyncStore, callback: (reason: string) => void): this {
18
- this.registry.push({ callback, store })
27
+ onTrigger(store: SyncStore, callbacks: SyncCallbacks): this {
28
+ this.registry.push({ ...callbacks, store })
19
29
  return this
20
30
  }
21
31
  }
@@ -1,12 +1,14 @@
1
1
  import SyncStore from "../store";
2
+ import type { SyncCallbacks } from './base'
2
3
 
3
4
  export interface SyncStrategy {
4
5
  start(): void
5
6
  stop(): void
6
7
 
7
- onTrigger(store: SyncStore, callback: (reason: string) => void): this
8
+ onTrigger(store: SyncStore, callbacks: SyncCallbacks): this
8
9
  }
9
10
 
10
11
  export { default as BaseStrategy } from './base'
11
12
  export { default as InitialStrategy } from './initial'
12
13
  export { default as PollingStrategy } from './polling'
14
+ export type { SyncCallbacks } from './base'
@@ -1,9 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(rg:*)",
5
- "Bash(npm run lint:*)"
6
- ],
7
- "deny": []
8
- }
9
- }