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 +14 -0
- package/package.json +1 -1
- package/src/services/sanity.js +3 -3
- package/src/services/sync/manager.ts +4 -2
- package/src/services/sync/store/index.ts +25 -4
- package/src/services/sync/store-configs.ts +3 -0
- package/src/services/sync/strategies/base.ts +13 -3
- package/src/services/sync/strategies/index.ts +3 -1
- package/.claude/settings.local.json +0 -9
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
package/src/services/sanity.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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]
|
|
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,
|
|
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() -
|
|
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() -
|
|
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:
|
|
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,
|
|
18
|
-
this.registry.push({
|
|
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,
|
|
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'
|