musora-content-services 2.117.8 → 2.119.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 +52 -0
- package/package.json +1 -1
- package/src/contentMetaData.js +9 -5
- package/src/index.d.ts +2 -0
- package/src/index.js +2 -0
- package/src/services/content.js +54 -15
- package/src/services/dataContext.js +41 -0
- package/src/services/progress-row/base.js +13 -3
- package/src/services/recommendations.js +5 -2
- package/src/services/sync/manager.ts +4 -0
- package/src/services/sync/repositories/base.ts +18 -0
- package/src/services/sync/store/index.ts +154 -33
- package/src/services/user/sessions.js +9 -2
- package/src/services/userActivity.js +5 -48
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,58 @@
|
|
|
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.119.0](https://github.com/railroadmedia/musora-content-services/compare/v2.118.1...v2.119.0) (2026-01-14)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* adds restore methods (for progress deletion undo) ([#705](https://github.com/railroadmedia/musora-content-services/issues/705)) ([b01a718](https://github.com/railroadmedia/musora-content-services/commit/b01a71892175b38789d62fffac0ce5f8e2e4513d))
|
|
11
|
+
|
|
12
|
+
### [2.118.1](https://github.com/railroadmedia/musora-content-services/compare/v2.118.0...v2.118.1) (2026-01-14)
|
|
13
|
+
|
|
14
|
+
## [2.118.0](https://github.com/railroadmedia/musora-content-services/compare/v2.107.4...v2.118.0) (2026-01-14)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
* adds column and implements for hiding cards ([#679](https://github.com/railroadmedia/musora-content-services/issues/679)) ([35ac42c](https://github.com/railroadmedia/musora-content-services/commit/35ac42cebe397c557d0b197d4fab38907ca08ab7))
|
|
20
|
+
* **BEH-1421:** lesson type migrations ([#660](https://github.com/railroadmedia/musora-content-services/issues/660)) ([7f5ab7e](https://github.com/railroadmedia/musora-content-services/commit/7f5ab7e64693b51aea754e0fb13f10edbd7a7958))
|
|
21
|
+
* **BEH-1442:** old method migration + course collection updates ([#673](https://github.com/railroadmedia/musora-content-services/issues/673)) ([24dd6bf](https://github.com/railroadmedia/musora-content-services/commit/24dd6bf6a1604ef3ee639979cf2ffab5abc05a24))
|
|
22
|
+
* **BEH-1491:** proper card ordering (and hiding) on progress row ([#686](https://github.com/railroadmedia/musora-content-services/issues/686)) ([e519c35](https://github.com/railroadmedia/musora-content-services/commit/e519c352ac5e8d09a910b89fa03baf31490da102))
|
|
23
|
+
* extra data for method card ([#691](https://github.com/railroadmedia/musora-content-services/issues/691)) ([36b034c](https://github.com/railroadmedia/musora-content-services/commit/36b034c83b86f0ff825dfc00af6a6b97b793c4c6))
|
|
24
|
+
* **MU2-1323:** Add dynamic filtering to lessons/songs excluding those without avaialable content ([072a471](https://github.com/railroadmedia/musora-content-services/commit/072a471a8b37bb31fc1421375563be0b2f124beb))
|
|
25
|
+
* **MU2-1323:** Remove redundant constant ([768befd](https://github.com/railroadmedia/musora-content-services/commit/768befdf393723cb0e091605f3c7e6ef4d523a92))
|
|
26
|
+
* **MU2-1323:** Use filterTypes for get all content types ([8065100](https://github.com/railroadmedia/musora-content-services/commit/80651009fcc9dae6661b3cbaecf55d5c3585d044))
|
|
27
|
+
* **T3Ps-1324:** Rename Tiered Courses filter to Course Collections ([97efe76](https://github.com/railroadmedia/musora-content-services/commit/97efe76fff3860d769593809bea2e59fb317b528))
|
|
28
|
+
* **T3PS-1413:** homepage progress row learning path lessons need need_access flag ([#688](https://github.com/railroadmedia/musora-content-services/issues/688)) ([cde7359](https://github.com/railroadmedia/musora-content-services/commit/cde73595c76c634b711e580497e291fa074b3d51))
|
|
29
|
+
* **TP-1046:** expose error hooks ([#655](https://github.com/railroadmedia/musora-content-services/issues/655)) ([233fa10](https://github.com/railroadmedia/musora-content-services/commit/233fa1009038448c763d130ec942b9de5a49a875))
|
|
30
|
+
* Use Get requests for Sanity if query under character limit ([#694](https://github.com/railroadmedia/musora-content-services/issues/694)) ([da5c384](https://github.com/railroadmedia/musora-content-services/commit/da5c384e7f538cf7c72a3d8f742787f24a4ed570))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
### Bug Fixes
|
|
34
|
+
|
|
35
|
+
* adds methods to set/remove http token ([0a460b6](https://github.com/railroadmedia/musora-content-services/commit/0a460b6d002fb7d85c340095d62c1e92ce253d64))
|
|
36
|
+
* **auth:** await for local storage ([69ee2ca](https://github.com/railroadmedia/musora-content-services/commit/69ee2ca780c71a707dfa452ede98240f0cfe8436))
|
|
37
|
+
* **auth:** client platform header on login ([52d5927](https://github.com/railroadmedia/musora-content-services/commit/52d5927fddd79db807440cccd541c09ae8ce770a))
|
|
38
|
+
* **auth:** login data ([a1b72d8](https://github.com/railroadmedia/musora-content-services/commit/a1b72d8145e230cfc850e208f147080c7ef8c6df))
|
|
39
|
+
* **BEH-878:** correct brand song type names ([#695](https://github.com/railroadmedia/musora-content-services/issues/695)) ([2b1cde9](https://github.com/railroadmedia/musora-content-services/commit/2b1cde9978dd744129bd906aff3a9bb03e01b04e))
|
|
40
|
+
* change js import to ts ([#683](https://github.com/railroadmedia/musora-content-services/issues/683)) ([1285cbe](https://github.com/railroadmedia/musora-content-services/commit/1285cbee851a3a2c68a0e36f37909e67173f82b8))
|
|
41
|
+
* changes lesson type mapping ([#690](https://github.com/railroadmedia/musora-content-services/issues/690)) ([af4dda9](https://github.com/railroadmedia/musora-content-services/commit/af4dda9e03eecfd49a341d2c1aea8afcf217e233))
|
|
42
|
+
* daniel merged bad ([#675](https://github.com/railroadmedia/musora-content-services/issues/675)) ([1c5863b](https://github.com/railroadmedia/musora-content-services/commit/1c5863bc921967fe16875367d9589c8dcf4e0ac3))
|
|
43
|
+
* establishes a positive progress validation ([#672](https://github.com/railroadmedia/musora-content-services/issues/672)) ([e9bc211](https://github.com/railroadmedia/musora-content-services/commit/e9bc211659262b282e1073c2746c3c8824371c35))
|
|
44
|
+
* fix explore all results ([#698](https://github.com/railroadmedia/musora-content-services/issues/698)) ([e9eb8a5](https://github.com/railroadmedia/musora-content-services/commit/e9eb8a5744b1d24c2b1b8e7ba5573cd128454463))
|
|
45
|
+
* fixes small bug with aggregator & adds safety for saveContentProgress ([#681](https://github.com/railroadmedia/musora-content-services/issues/681)) ([b3fd0a7](https://github.com/railroadmedia/musora-content-services/commit/b3fd0a7acf1fe01e2aff567148554fb291bac8a7))
|
|
46
|
+
* oops ([#693](https://github.com/railroadmedia/musora-content-services/issues/693)) ([0be66cb](https://github.com/railroadmedia/musora-content-services/commit/0be66cbaef0b33dad3074a22b9670cee4ddd0024))
|
|
47
|
+
* **pins:** user pins local storage ([#704](https://github.com/railroadmedia/musora-content-services/issues/704)) ([ca7c172](https://github.com/railroadmedia/musora-content-services/commit/ca7c1722b759c1a4711a1f6ee2213b7392d2632e))
|
|
48
|
+
* resolve foryou issue on mpf ([#703](https://github.com/railroadmedia/musora-content-services/issues/703)) ([3e8f0dd](https://github.com/railroadmedia/musora-content-services/commit/3e8f0ddc60eb51a5c510df6433e1cca1c5f5496a))
|
|
49
|
+
* Single lessons, Skill Packs and Entertainment tabs not display content and Courses display any content type ([9793443](https://github.com/railroadmedia/musora-content-services/commit/97934438bf8262fa27349eab562cecf1323c720a))
|
|
50
|
+
* some fixes from Rob ([#676](https://github.com/railroadmedia/musora-content-services/issues/676)) ([7e068ee](https://github.com/railroadmedia/musora-content-services/commit/7e068eee3cabc4de1d0620fe58eadf138532ab56))
|
|
51
|
+
* **T3PS-1187:** award progress optimizations ([#689](https://github.com/railroadmedia/musora-content-services/issues/689)) ([5d063b8](https://github.com/railroadmedia/musora-content-services/commit/5d063b8227e9ee5c70c30bc31263296bfcb4aa63))
|
|
52
|
+
* **T3PS-1289:** navigateTo calculation ([#677](https://github.com/railroadmedia/musora-content-services/issues/677)) ([12f4ca3](https://github.com/railroadmedia/musora-content-services/commit/12f4ca38b05a2175b33a9260e7c00f9aebfb8a87))
|
|
53
|
+
* **T3PS-1347:** Update getUserWeeklyStats to include query for past 60 days for streak calculation, retain original contract of the function ([d798acf](https://github.com/railroadmedia/musora-content-services/commit/d798acf2a01f25551746030722a41ccb81030ed9))
|
|
54
|
+
* **T3PS:** Cleanup ([07f057f](https://github.com/railroadmedia/musora-content-services/commit/07f057f867c8a3931ae6cbf5ecca9ae471f23d00))
|
|
55
|
+
* **TP-1051:** group contentProgress upsert pushes ([#665](https://github.com/railroadmedia/musora-content-services/issues/665)) ([27ff11f](https://github.com/railroadmedia/musora-content-services/commit/27ff11fb1b10075313e614cf895356a221f0c0d1))
|
|
56
|
+
|
|
5
57
|
### [2.117.8](https://github.com/railroadmedia/musora-content-services/compare/v2.117.7...v2.117.8) (2026-01-13)
|
|
6
58
|
|
|
7
59
|
|
package/package.json
CHANGED
package/src/contentMetaData.js
CHANGED
|
@@ -53,7 +53,7 @@ export class Tabs {
|
|
|
53
53
|
static Collections = { name: 'Collections', short_name: 'Collections', value: 'type,collections', cardType: 'big' }
|
|
54
54
|
static ExploreAll = { name: 'Explore All', short_name: 'Explore All', value: 'tab,explore all', icon: 'icon-filters', cardType: 'big'}
|
|
55
55
|
static All = { name: 'All', short_name: 'All', value: '' }
|
|
56
|
-
static Courses = { name: 'Courses', short_name: 'Courses', value: 'type,Courses' }
|
|
56
|
+
static Courses = { name: 'Courses', short_name: 'Courses', value: 'type,Courses', recSysSection: 'lesson', }
|
|
57
57
|
static SkillLevel = { name: 'Skill Level', short_name: 'SKILL LEVEL', is_group_by: true, value: 'difficulty_string' }
|
|
58
58
|
static Genres = { name: 'Genres', short_name: 'Genres', is_group_by: true, value: 'genre' }
|
|
59
59
|
static Completed = {
|
|
@@ -82,12 +82,14 @@ export class Tabs {
|
|
|
82
82
|
short_name: 'Tutorials',
|
|
83
83
|
value: 'type,tutorials',
|
|
84
84
|
cardType: 'big',
|
|
85
|
+
recSysSection: 'song',
|
|
85
86
|
}
|
|
86
87
|
static Transcriptions = {
|
|
87
88
|
name: 'Transcriptions',
|
|
88
89
|
short_name: 'Transcriptions',
|
|
89
90
|
value: 'type,transcriptions',
|
|
90
91
|
cardType: 'small',
|
|
92
|
+
recSysSection: 'song',
|
|
91
93
|
}
|
|
92
94
|
static SheetMusic = {
|
|
93
95
|
name: 'Sheet Music',
|
|
@@ -100,12 +102,14 @@ export class Tabs {
|
|
|
100
102
|
short_name: 'Tabs',
|
|
101
103
|
value: 'type,transcriptions',
|
|
102
104
|
cardType: 'small',
|
|
105
|
+
recSysSection: 'song',
|
|
103
106
|
}
|
|
104
107
|
static PlayAlongs = {
|
|
105
108
|
name: 'Play-Alongs',
|
|
106
109
|
short_name: 'Play-Alongs',
|
|
107
|
-
value: 'type,play
|
|
110
|
+
value: 'type,play-along',
|
|
108
111
|
cardType: 'small',
|
|
112
|
+
recSysSection: 'song',
|
|
109
113
|
}
|
|
110
114
|
static JamTracks = {
|
|
111
115
|
name: 'Jam Tracks',
|
|
@@ -121,9 +125,9 @@ export class Tabs {
|
|
|
121
125
|
static RecentActivityPosts = { name: 'Posts', short_name: 'Posts' }
|
|
122
126
|
static RecentActivityComments = { name: 'Comments', short_name: 'Comments' }
|
|
123
127
|
// new tabs - 29.10
|
|
124
|
-
static SingleLessons = { name: 'Single Lessons', short_name: 'Single Lessons', value: 'type,Single Lessons' }
|
|
125
|
-
static SkillPacks = { name: 'Skill Packs', short_name: 'Skill Packs', value: 'type,Skill Packs' }
|
|
126
|
-
static Entertainment = { name: 'Entertainment', short_name: 'Entertainment', value: 'type,Entertainment' }
|
|
128
|
+
static SingleLessons = { name: 'Single Lessons', short_name: 'Single Lessons', value: 'type,Single Lessons', recSysSection: 'lesson', }
|
|
129
|
+
static SkillPacks = { name: 'Skill Packs', short_name: 'Skill Packs', value: 'type,Skill Packs', recSysSection: 'lesson', }
|
|
130
|
+
static Entertainment = { name: 'Entertainment', short_name: 'Entertainment', value: 'type,Entertainment', recSysSection: 'lesson', }
|
|
127
131
|
}
|
|
128
132
|
|
|
129
133
|
/**
|
package/src/index.d.ts
CHANGED
|
@@ -128,6 +128,7 @@ import {
|
|
|
128
128
|
} from './services/contentProgress.js';
|
|
129
129
|
|
|
130
130
|
import {
|
|
131
|
+
clearAllCachedData,
|
|
131
132
|
verifyLocalDataContext
|
|
132
133
|
} from './services/dataContext.js';
|
|
133
134
|
|
|
@@ -444,6 +445,7 @@ declare module 'musora-content-services' {
|
|
|
444
445
|
buildEntityAndTotalQuery,
|
|
445
446
|
buildImageSRC,
|
|
446
447
|
calculateLongestStreaks,
|
|
448
|
+
clearAllCachedData,
|
|
447
449
|
closeComment,
|
|
448
450
|
completeLearningPathIntroVideo,
|
|
449
451
|
completeMethodIntroVideo,
|
package/src/index.js
CHANGED
|
@@ -132,6 +132,7 @@ import {
|
|
|
132
132
|
} from './services/contentProgress.js';
|
|
133
133
|
|
|
134
134
|
import {
|
|
135
|
+
clearAllCachedData,
|
|
135
136
|
verifyLocalDataContext
|
|
136
137
|
} from './services/dataContext.js';
|
|
137
138
|
|
|
@@ -443,6 +444,7 @@ export {
|
|
|
443
444
|
buildEntityAndTotalQuery,
|
|
444
445
|
buildImageSRC,
|
|
445
446
|
calculateLongestStreaks,
|
|
447
|
+
clearAllCachedData,
|
|
446
448
|
closeComment,
|
|
447
449
|
completeLearningPathIntroVideo,
|
|
448
450
|
completeMethodIntroVideo,
|
package/src/services/content.js
CHANGED
|
@@ -18,7 +18,10 @@ import {recommendations, rankCategories, rankItems} from "./recommendations";
|
|
|
18
18
|
import {addContextToContent} from "./contentAggregator.js";
|
|
19
19
|
import {globalConfig} from "./config";
|
|
20
20
|
import {getUserData} from "./user/management";
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
lessonTypesMapping,
|
|
23
|
+
ownedContentTypes
|
|
24
|
+
} from "../contentTypeConfig";
|
|
22
25
|
import {getPermissionsAdapter} from "./permissions/index.ts";
|
|
23
26
|
import {MEMBERSHIP_PERMISSIONS} from "../constants/membership-permissions.ts";
|
|
24
27
|
|
|
@@ -100,6 +103,7 @@ export async function getTabResults(brand, pageName, tabName, {
|
|
|
100
103
|
tabObj => tabObj.name.toLowerCase() === tabName.toLowerCase()
|
|
101
104
|
)
|
|
102
105
|
const tabValue = tabMatch?.value || ''
|
|
106
|
+
const tabRecSysSection = tabMatch?.recSysSection || ''
|
|
103
107
|
const mergedIncludedFields = tabValue ? [...filteredSelectedFilters, tabValue] : filteredSelectedFilters;
|
|
104
108
|
|
|
105
109
|
// Fetch data
|
|
@@ -112,23 +116,58 @@ export async function getTabResults(brand, pageName, tabName, {
|
|
|
112
116
|
addProgressPercentage: true,
|
|
113
117
|
addProgressStatus: true
|
|
114
118
|
})
|
|
119
|
+
} else if (sort === 'recommended') {
|
|
120
|
+
const contentTypes = lessonTypesMapping[tabName.toLowerCase()] || []
|
|
121
|
+
const allRecommendations = await recommendations(brand, { contentTypes, section: tabRecSysSection })
|
|
122
|
+
|
|
123
|
+
let contentToDisplay
|
|
124
|
+
if (allRecommendations.length > 0) {
|
|
125
|
+
// Fetch and sort recommended content
|
|
126
|
+
let recommendedContent = await fetchByRailContentIds(allRecommendations, 'tab-data', brand, true)
|
|
127
|
+
recommendedContent.sort((a, b) => allRecommendations.indexOf(a.id) - allRecommendations.indexOf(b.id))
|
|
128
|
+
|
|
129
|
+
const start = (page - 1) * limit
|
|
130
|
+
const end = start + limit
|
|
131
|
+
|
|
132
|
+
// Need more content beyond recommendations?
|
|
133
|
+
if (recommendedContent.length < end) {
|
|
134
|
+
const additionalNeeded = end - recommendedContent.length;
|
|
135
|
+
const tabData = await fetchTabData(brand, pageName, {
|
|
136
|
+
page: Math.ceil(additionalNeeded / limit),
|
|
137
|
+
limit: additionalNeeded + limit,
|
|
138
|
+
sort: '-published_on',
|
|
139
|
+
includedFields: mergedIncludedFields,
|
|
140
|
+
progress: progressValue
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
// Filter out duplicates and combine
|
|
144
|
+
const recommendedIds = new Set(recommendedContent.map(c => c.id))
|
|
145
|
+
const additionalContent = tabData.entity.filter(c => !recommendedIds.has(c.id))
|
|
146
|
+
|
|
147
|
+
contentToDisplay = [...recommendedContent, ...additionalContent].slice(start, end)
|
|
148
|
+
} else {
|
|
149
|
+
contentToDisplay = recommendedContent.slice(start, end)
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
// No recommendations - use normal flow
|
|
153
|
+
const temp = await fetchTabData(brand, pageName, { page, limit, sort: '-published_on', includedFields: mergedIncludedFields, progress: progressValue })
|
|
154
|
+
contentToDisplay = temp.entity
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
results = await addContextToContent(() => contentToDisplay, {
|
|
158
|
+
addNextLesson: true,
|
|
159
|
+
addNavigateTo: true,
|
|
160
|
+
addProgressPercentage: true,
|
|
161
|
+
addProgressStatus: true
|
|
162
|
+
})
|
|
115
163
|
} else {
|
|
116
164
|
let temp = await fetchTabData(brand, pageName, { page, limit, sort, includedFields: mergedIncludedFields, progress: progressValue });
|
|
117
165
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
addProgressPercentage: true,
|
|
124
|
-
addProgressStatus: true
|
|
125
|
-
})
|
|
126
|
-
]);
|
|
127
|
-
|
|
128
|
-
results = ranking.length === 0 ? contextResults : contextResults.sort((a, b) => {
|
|
129
|
-
const indexA = ranking.indexOf(a.id);
|
|
130
|
-
const indexB = ranking.indexOf(b.id);
|
|
131
|
-
return (indexA === -1 ? Infinity : indexA) - (indexB === -1 ? Infinity : indexB);
|
|
166
|
+
results = await addContextToContent(() => temp.entity, {
|
|
167
|
+
addNextLesson: true,
|
|
168
|
+
addNavigateTo: true,
|
|
169
|
+
addProgressPercentage: true,
|
|
170
|
+
addProgressStatus: true
|
|
132
171
|
})
|
|
133
172
|
}
|
|
134
173
|
|
|
@@ -147,3 +147,44 @@ export class DataContext {
|
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Clears all dataContext cached data from localStorage.
|
|
153
|
+
* Should be called on logout to prevent data leakage between users.
|
|
154
|
+
* Note: Does not clear user_pin_progress_row keys as they are user-specific.
|
|
155
|
+
*/
|
|
156
|
+
export async function clearAllCachedData() {
|
|
157
|
+
const storage = globalConfig.localStorage
|
|
158
|
+
|
|
159
|
+
if (storage) {
|
|
160
|
+
const keysToRemove = []
|
|
161
|
+
|
|
162
|
+
// For React Native AsyncStorage
|
|
163
|
+
if (globalConfig.isMA && storage.getAllKeys) {
|
|
164
|
+
const allKeys = await storage.getAllKeys()
|
|
165
|
+
keysToRemove.push(...allKeys.filter(key =>
|
|
166
|
+
key.startsWith('dataContext_')
|
|
167
|
+
))
|
|
168
|
+
}
|
|
169
|
+
// For web localStorage
|
|
170
|
+
else if (typeof storage.length !== 'undefined') {
|
|
171
|
+
const allKeys = []
|
|
172
|
+
for (let i = 0; i < storage.length; i++) {
|
|
173
|
+
const key = storage.key(i)
|
|
174
|
+
if (key) {
|
|
175
|
+
allKeys.push(key)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
keysToRemove.push(...allKeys.filter(key =>
|
|
179
|
+
key.startsWith('dataContext_')
|
|
180
|
+
))
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Use multiRemove for React Native AsyncStorage, removeItem for web localStorage
|
|
184
|
+
if (storage.multiRemove && typeof storage.multiRemove === 'function') {
|
|
185
|
+
await storage.multiRemove(keysToRemove)
|
|
186
|
+
} else {
|
|
187
|
+
keysToRemove.forEach(key => storage.removeItem(key))
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -18,6 +18,14 @@ import { PUT } from '../../infrastructure/http/HttpClient.ts'
|
|
|
18
18
|
|
|
19
19
|
export const USER_PIN_PROGRESS_KEY = 'user_pin_progress_row'
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Gets the localStorage key for user pinned progress, scoped by user ID
|
|
23
|
+
*/
|
|
24
|
+
function getUserPinProgressKey() {
|
|
25
|
+
const userId = globalConfig.sessionConfig?.userId || globalConfig.railcontentConfig?.userId
|
|
26
|
+
return userId ? `user_pin_progress_row_${userId}` : USER_PIN_PROGRESS_KEY
|
|
27
|
+
}
|
|
28
|
+
|
|
21
29
|
/**
|
|
22
30
|
* Fetches and combines recent user progress rows and playlists, excluding certain types and parents.
|
|
23
31
|
*
|
|
@@ -102,7 +110,8 @@ export async function unpinProgressRow(brand) {
|
|
|
102
110
|
}
|
|
103
111
|
|
|
104
112
|
async function getUserPinnedItem(brand) {
|
|
105
|
-
const
|
|
113
|
+
const key = getUserPinProgressKey()
|
|
114
|
+
const pinnedProgressRaw = await globalConfig.localStorage.getItem(key)
|
|
106
115
|
let pinnedProgress = pinnedProgressRaw ? JSON.parse(pinnedProgressRaw) : {}
|
|
107
116
|
pinnedProgress = pinnedProgress || {}
|
|
108
117
|
return pinnedProgress[brand] ?? null
|
|
@@ -200,9 +209,10 @@ function mergeAndSortItems(items, limit) {
|
|
|
200
209
|
}
|
|
201
210
|
|
|
202
211
|
async function updateUserPinnedProgressRow(brand, pinnedData) {
|
|
203
|
-
const
|
|
212
|
+
const key = getUserPinProgressKey()
|
|
213
|
+
const pinnedProgressRaw = await globalConfig.localStorage.getItem(key)
|
|
204
214
|
let pinnedProgress = pinnedProgressRaw ? JSON.parse(pinnedProgressRaw) : {}
|
|
205
215
|
pinnedProgress = pinnedProgress || {}
|
|
206
216
|
pinnedProgress[brand] = pinnedData
|
|
207
|
-
await globalConfig.localStorage.setItem(
|
|
217
|
+
await globalConfig.localStorage.setItem(key, JSON.stringify(pinnedProgress))
|
|
208
218
|
}
|
|
@@ -126,9 +126,12 @@ export async function rankItems(brand, content_ids) {
|
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
export async function recommendations(brand, { section = '' } = {}) {
|
|
129
|
+
export async function recommendations(brand, { section = '', contentTypes = [] } = {}) {
|
|
130
130
|
section = section.toUpperCase().replace('-', '_')
|
|
131
131
|
const sectionString = section ? `§ion=${section}` : ''
|
|
132
|
-
const
|
|
132
|
+
const contentTypesString = contentTypes.length > 0
|
|
133
|
+
? contentTypes.map(type => `&content_types[]=${encodeURIComponent(type)}`).join('')
|
|
134
|
+
: ''
|
|
135
|
+
const url = `/api/content/v1/recommendations?brand=${brand}${sectionString}${contentTypesString}`
|
|
133
136
|
return await GET(url)
|
|
134
137
|
}
|
|
@@ -165,6 +165,10 @@ export default class SyncManager {
|
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
try {
|
|
168
|
+
Object.values(this.storesRegistry).forEach((store) => {
|
|
169
|
+
store.destroy()
|
|
170
|
+
})
|
|
171
|
+
|
|
168
172
|
this.runScope.abort()
|
|
169
173
|
this.strategyMap.forEach(({ strategies }) => strategies.forEach((strategy) => strategy.stop()))
|
|
170
174
|
effectTeardowns.forEach((teardown) => teardown())
|
|
@@ -47,6 +47,10 @@ export default class SyncRepository<TModel extends BaseModel> {
|
|
|
47
47
|
return this._respondToRead(() => this.store.queryAllIds(...args))
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
protected async queryAllDeletedIds(...args: Q.Clause[]) {
|
|
51
|
+
return this._respondToRead(() => this.store.queryAllDeletedIds(...args))
|
|
52
|
+
}
|
|
53
|
+
|
|
50
54
|
protected async fetchOne(id: RecordId) {
|
|
51
55
|
return this._fetch(() => this.store.readOne(id))
|
|
52
56
|
}
|
|
@@ -131,6 +135,20 @@ export default class SyncRepository<TModel extends BaseModel> {
|
|
|
131
135
|
)
|
|
132
136
|
}
|
|
133
137
|
|
|
138
|
+
protected async restoreOne(id: RecordId) {
|
|
139
|
+
return this.store.telemetry.trace(
|
|
140
|
+
{ name: `restoreOne:${this.store.model.table}`, op: 'restore', attributes: { ...this.context.session.toJSON() } },
|
|
141
|
+
(span) => this._respondToWrite(() => this.store.restoreOne(id, span), span)
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
protected async restoreSome(ids: RecordId[]) {
|
|
146
|
+
return this.store.telemetry.trace(
|
|
147
|
+
{ name: `restoreSome:${this.store.model.table}`, op: 'restore', attributes: { ...this.context.session.toJSON() } },
|
|
148
|
+
(span) => this._respondToWrite(() => this.store.restoreSome(ids, span), span)
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
|
|
134
152
|
private async _respondToWrite<T extends SyncWriteRecordData<TModel>>(create: () => Promise<T>, span?: Span) {
|
|
135
153
|
const data = await create()
|
|
136
154
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Database, Q, type Collection, type RecordId } from '@nozbe/watermelondb'
|
|
1
|
+
import { Database, Q, Query, type Collection, type RecordId } from '@nozbe/watermelondb'
|
|
2
2
|
import { RawSerializer, ModelSerializer } from '../serializers'
|
|
3
3
|
import { ModelClass, SyncToken, SyncEntry, SyncContext, EpochMs } from '..'
|
|
4
4
|
import { SyncPullResponse, SyncPushResponse, SyncPullFetchFailureResponse, PushPayload, SyncStorePushResultSuccess, SyncStorePushResultFailure } from '../fetch'
|
|
@@ -17,7 +17,6 @@ import { type WriterInterface } from '@nozbe/watermelondb/Database/WorkQueue'
|
|
|
17
17
|
import type LokiJSAdapter from '@nozbe/watermelondb/adapters/lokijs'
|
|
18
18
|
import { SyncError } from '../errors'
|
|
19
19
|
|
|
20
|
-
|
|
21
20
|
type SyncPull = (
|
|
22
21
|
session: BaseSessionProvider,
|
|
23
22
|
previousFetchToken: SyncToken | null,
|
|
@@ -39,6 +38,8 @@ export type SyncStoreConfig<TModel extends BaseModel = BaseModel> = {
|
|
|
39
38
|
export default class SyncStore<TModel extends BaseModel = BaseModel> {
|
|
40
39
|
static readonly PULL_THROTTLE_INTERVAL = 2_000
|
|
41
40
|
static readonly PUSH_THROTTLE_INTERVAL = 1_000
|
|
41
|
+
static readonly DELETED_RECORD_GRACE_PERIOD = 60_000 // 60s
|
|
42
|
+
static readonly CLEANUP_INTERVAL = 60_000 * 60 // 1hr
|
|
42
43
|
|
|
43
44
|
readonly telemetry: SyncTelemetry
|
|
44
45
|
readonly context: SyncContext
|
|
@@ -60,6 +61,7 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
|
|
|
60
61
|
private pushCoalescer = new PushCoalescer()
|
|
61
62
|
|
|
62
63
|
private emitter = new EventEmitter()
|
|
64
|
+
private cleanupTimer: NodeJS.Timeout | null = null
|
|
63
65
|
|
|
64
66
|
private lastFetchTokenKey: string
|
|
65
67
|
|
|
@@ -91,12 +93,18 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
|
|
|
91
93
|
this.lastFetchTokenKey = `last_fetch_token:${this.model.table}`
|
|
92
94
|
|
|
93
95
|
this.telemetry = telemetry
|
|
96
|
+
|
|
97
|
+
this.startCleanupTimer()
|
|
94
98
|
}
|
|
95
99
|
|
|
96
100
|
on = this.emitter.on.bind(this.emitter)
|
|
97
101
|
off = this.emitter.off.bind(this.emitter)
|
|
98
102
|
private emit = this.emitter.emit.bind(this.emitter)
|
|
99
103
|
|
|
104
|
+
destroy() {
|
|
105
|
+
this.stopCleanupTimer()
|
|
106
|
+
}
|
|
107
|
+
|
|
100
108
|
async requestSync(reason: string) {
|
|
101
109
|
inBoundary(ctx => {
|
|
102
110
|
this.telemetry.trace(
|
|
@@ -176,6 +184,10 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
|
|
|
176
184
|
return this.queryRecordIds(...args)
|
|
177
185
|
}
|
|
178
186
|
|
|
187
|
+
async queryAllDeletedIds(...args: Q.Clause[]) {
|
|
188
|
+
return this.queryMaybeDeletedRecordIds(...args)
|
|
189
|
+
}
|
|
190
|
+
|
|
179
191
|
async queryOne(...args: Q.Clause[]) {
|
|
180
192
|
const record = await this.queryRecord(...args)
|
|
181
193
|
return record ? this.modelSerializer.toPlainObject(record) : null
|
|
@@ -359,6 +371,41 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
|
|
|
359
371
|
})
|
|
360
372
|
}
|
|
361
373
|
|
|
374
|
+
async restoreOne(id: RecordId, span?: Span) {
|
|
375
|
+
return this.restoreSome([id], span).then(r => r[0])
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async restoreSome(ids: RecordId[], span?: Span) {
|
|
379
|
+
return this.runScope.abortable(async () => {
|
|
380
|
+
const records = await this.telemeterizedWrite(span, async writer => {
|
|
381
|
+
const records = await writer.callReader(() => this.queryMaybeDeletedRecords(
|
|
382
|
+
Q.where('id', Q.oneOf(ids)),
|
|
383
|
+
Q.where('_status', 'deleted')
|
|
384
|
+
))
|
|
385
|
+
|
|
386
|
+
const destroyBuilds = records.map(record => new this.model(this.collection, { id: record.id }).prepareDestroyPermanently())
|
|
387
|
+
const createBuilds = records.map(record => this.collection.prepareCreate((r) => {
|
|
388
|
+
Object.keys(record._raw).forEach((key) => {
|
|
389
|
+
r._raw[key] = record._raw[key]
|
|
390
|
+
})
|
|
391
|
+
r._raw._status = 'updated'
|
|
392
|
+
}))
|
|
393
|
+
|
|
394
|
+
await writer.batch(...destroyBuilds)
|
|
395
|
+
await writer.batch(...createBuilds)
|
|
396
|
+
|
|
397
|
+
return createBuilds
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
this.emit('upserted', records)
|
|
401
|
+
|
|
402
|
+
this.pushUnsyncedWithRetry(span)
|
|
403
|
+
await this.ensurePersistence()
|
|
404
|
+
|
|
405
|
+
return records.map((record) => this.modelSerializer.toPlainObject(record))
|
|
406
|
+
})
|
|
407
|
+
}
|
|
408
|
+
|
|
362
409
|
async importUpsert(recordRaws: TModel['_raw'][]) {
|
|
363
410
|
await this.runScope.abortable(async () => {
|
|
364
411
|
await this.telemeterizedWrite(undefined, async writer => {
|
|
@@ -637,31 +684,7 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
|
|
|
637
684
|
return this.db.read(async () => {
|
|
638
685
|
const undeletedRecords = await this.collection.query(...args).fetch()
|
|
639
686
|
|
|
640
|
-
const
|
|
641
|
-
const adjustedQuery = {
|
|
642
|
-
...serializedQuery,
|
|
643
|
-
description: {
|
|
644
|
-
...serializedQuery.description,
|
|
645
|
-
where: [
|
|
646
|
-
// remove the default "not deleted" clause added by WatermelonDB
|
|
647
|
-
...serializedQuery.description.where.filter(
|
|
648
|
-
(w) =>
|
|
649
|
-
!(
|
|
650
|
-
w.type === 'where' &&
|
|
651
|
-
w.left === '_status' &&
|
|
652
|
-
w.comparison &&
|
|
653
|
-
w.comparison.operator === 'notEq' &&
|
|
654
|
-
w.comparison.right &&
|
|
655
|
-
'value' in w.comparison.right &&
|
|
656
|
-
w.comparison.right.value === 'deleted'
|
|
657
|
-
)
|
|
658
|
-
),
|
|
659
|
-
|
|
660
|
-
// and add our own "include deleted" clause
|
|
661
|
-
Q.where('_status', Q.eq('deleted'))
|
|
662
|
-
],
|
|
663
|
-
},
|
|
664
|
-
}
|
|
687
|
+
const adjustedQuery = this.maybeDeletedQuery(this.collection.query(...args))
|
|
665
688
|
|
|
666
689
|
// NOTE: constructing models in this way is a bit of a hack,
|
|
667
690
|
// but since deleted records aren't "resurrectable" in WatermelonDB anyway,
|
|
@@ -682,6 +705,54 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
|
|
|
682
705
|
})
|
|
683
706
|
}
|
|
684
707
|
|
|
708
|
+
/**
|
|
709
|
+
* Query records including ones marked as deleted
|
|
710
|
+
* WatermelonDB by default excludes deleted records from queries
|
|
711
|
+
*/
|
|
712
|
+
private async queryMaybeDeletedRecordIds(...args: Q.Clause[]) {
|
|
713
|
+
return this.db.read(async () => {
|
|
714
|
+
const undeletedRecordIds = await this.collection.query(...args).fetchIds()
|
|
715
|
+
|
|
716
|
+
const adjustedQuery = this.maybeDeletedQuery(this.collection.query(...args))
|
|
717
|
+
const deletedRecordIds = (await this.db.adapter.unsafeQueryRaw(adjustedQuery)).map(r => r.id)
|
|
718
|
+
|
|
719
|
+
return [
|
|
720
|
+
...undeletedRecordIds,
|
|
721
|
+
...deletedRecordIds,
|
|
722
|
+
]
|
|
723
|
+
})
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
private maybeDeletedQuery(query: Query<TModel>) {
|
|
727
|
+
const serializedQuery = query.serialize()
|
|
728
|
+
const adjustedQuery = {
|
|
729
|
+
...serializedQuery,
|
|
730
|
+
description: {
|
|
731
|
+
...serializedQuery.description,
|
|
732
|
+
where: [
|
|
733
|
+
// remove the default "not deleted" clause added by WatermelonDB
|
|
734
|
+
...serializedQuery.description.where.filter(
|
|
735
|
+
(w) =>
|
|
736
|
+
!(
|
|
737
|
+
w.type === 'where' &&
|
|
738
|
+
w.left === '_status' &&
|
|
739
|
+
w.comparison &&
|
|
740
|
+
w.comparison.operator === 'notEq' &&
|
|
741
|
+
w.comparison.right &&
|
|
742
|
+
'value' in w.comparison.right &&
|
|
743
|
+
w.comparison.right.value === 'deleted'
|
|
744
|
+
)
|
|
745
|
+
),
|
|
746
|
+
|
|
747
|
+
// and add our own "include deleted" clause
|
|
748
|
+
Q.where('_status', Q.eq('deleted'))
|
|
749
|
+
],
|
|
750
|
+
},
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
return adjustedQuery
|
|
754
|
+
}
|
|
755
|
+
|
|
685
756
|
// Avoid lazy persistence to IndexedDB
|
|
686
757
|
// to eliminate data loss risk due to tab close/crash before flush to IndexedDB
|
|
687
758
|
// https://github.com/Nozbe/WatermelonDB/issues/1329
|
|
@@ -739,6 +810,9 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
|
|
|
739
810
|
}
|
|
740
811
|
|
|
741
812
|
private async buildWriteBatchesFromEntries(writer: WriterInterface, entries: SyncEntry[], freshSync: boolean) {
|
|
813
|
+
// Clean up old deleted records during pull operations
|
|
814
|
+
await this.cleanupOldDeletedRecords(writer)
|
|
815
|
+
|
|
742
816
|
// if this is a fresh sync and there are no existing records, we can skip more sophisticated conflict resolution
|
|
743
817
|
if (freshSync) {
|
|
744
818
|
if ((await writer.callReader(() => this.queryMaybeDeletedRecords())).length === 0) {
|
|
@@ -747,7 +821,7 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
|
|
|
747
821
|
.filter((e) => !e.meta.lifecycle.deleted_at)
|
|
748
822
|
.forEach((entry) => resolver.againstNone(entry))
|
|
749
823
|
|
|
750
|
-
return this.prepareRecords(resolver.result)
|
|
824
|
+
return this.prepareRecords(resolver.result, new Map())
|
|
751
825
|
}
|
|
752
826
|
}
|
|
753
827
|
|
|
@@ -789,17 +863,26 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
|
|
|
789
863
|
}
|
|
790
864
|
})
|
|
791
865
|
|
|
792
|
-
return this.prepareRecords(resolver.result)
|
|
866
|
+
return this.prepareRecords(resolver.result, existingRecordsMap)
|
|
793
867
|
}
|
|
794
868
|
|
|
795
|
-
private prepareRecords(result: SyncResolution) {
|
|
869
|
+
private prepareRecords(result: SyncResolution, existingRecordsMap: Map<RecordId, TModel>) {
|
|
796
870
|
if (Object.values(result).find((c) => c.length)) {
|
|
797
871
|
this.telemetry.debug(`[store:${this.model.table}] Writing changes`, { changes: result })
|
|
798
872
|
}
|
|
799
873
|
|
|
800
|
-
const destroyedBuilds = result.idsForDestroy
|
|
801
|
-
|
|
802
|
-
|
|
874
|
+
const destroyedBuilds = result.idsForDestroy
|
|
875
|
+
.filter(id => {
|
|
876
|
+
// Only permanently delete if updated_at is older than grace period
|
|
877
|
+
const record = existingRecordsMap.get(id)
|
|
878
|
+
if (!record) return true // If no record found, safe to destroy
|
|
879
|
+
|
|
880
|
+
const gracePeriodAgo = Date.now() - SyncStore.DELETED_RECORD_GRACE_PERIOD
|
|
881
|
+
return record.updated_at < gracePeriodAgo
|
|
882
|
+
})
|
|
883
|
+
.map((id) => {
|
|
884
|
+
return new this.model(this.collection, { id }).prepareDestroyPermanently()
|
|
885
|
+
})
|
|
803
886
|
const createdBuilds = result.entriesForCreate.map((entry) => {
|
|
804
887
|
return this.collection.prepareCreate((r) => {
|
|
805
888
|
Object.entries(entry.record!).forEach(([key, value]) => {
|
|
@@ -856,4 +939,42 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
|
|
|
856
939
|
private isLokiAdapter(adapter: any): adapter is LokiJSAdapter {
|
|
857
940
|
return adapter._driver && 'loki' in adapter._driver
|
|
858
941
|
}
|
|
942
|
+
|
|
943
|
+
private startCleanupTimer() {
|
|
944
|
+
this.cleanupTimer = setInterval(() => {
|
|
945
|
+
this.runScope.abortable(async () => {
|
|
946
|
+
this.telemeterizedWrite(undefined, async (writer) => {
|
|
947
|
+
await this.cleanupOldDeletedRecords(writer)
|
|
948
|
+
})
|
|
949
|
+
})
|
|
950
|
+
}, SyncStore.CLEANUP_INTERVAL)
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
private stopCleanupTimer() {
|
|
954
|
+
if (this.cleanupTimer) {
|
|
955
|
+
clearInterval(this.cleanupTimer)
|
|
956
|
+
this.cleanupTimer = null
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
/** Destroy permanently records past their grace period
|
|
961
|
+
* (we need to keep records around after being marked deleted
|
|
962
|
+
* for undo purposes, so we don't discard them in writeEntries
|
|
963
|
+
* (after a server push), but instead every hour or so)
|
|
964
|
+
*/
|
|
965
|
+
private async cleanupOldDeletedRecords(writer: WriterInterface) {
|
|
966
|
+
const gracePeriodAgo = Date.now() - SyncStore.DELETED_RECORD_GRACE_PERIOD
|
|
967
|
+
|
|
968
|
+
const oldDeletedRecords = await writer.callReader(() => this.queryMaybeDeletedRecords(
|
|
969
|
+
Q.where('_status', 'deleted'),
|
|
970
|
+
Q.where('updated_at', Q.lt(gracePeriodAgo))
|
|
971
|
+
))
|
|
972
|
+
|
|
973
|
+
if (oldDeletedRecords.length > 0) {
|
|
974
|
+
this.telemetry.debug(`[store:${this.model.table}] Cleaning up ${oldDeletedRecords.length} old deleted records`)
|
|
975
|
+
|
|
976
|
+
const destroyBuilds = oldDeletedRecords.map(record => record.prepareDestroyPermanently())
|
|
977
|
+
return writer.batch(...destroyBuilds)
|
|
978
|
+
}
|
|
979
|
+
}
|
|
859
980
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* @module Sessions
|
|
3
3
|
*/
|
|
4
4
|
import { globalConfig } from '../config.js'
|
|
5
|
+
import { clearAllCachedData } from '../dataContext.js'
|
|
5
6
|
import { USER_PIN_PROGRESS_KEY } from '../progress-row/base.js'
|
|
6
7
|
import './types.js'
|
|
7
8
|
|
|
@@ -50,9 +51,11 @@ export async function login(email, password, deviceName, deviceToken, platform)
|
|
|
50
51
|
|
|
51
52
|
// TODO: refactor this. I don't think this is the place for it but we need it fixed for the system test
|
|
52
53
|
if (res.ok) {
|
|
54
|
+
const userId = data.user?.id
|
|
55
|
+
const userPinKey = userId ? `user_pin_progress_row_${userId}` : USER_PIN_PROGRESS_KEY
|
|
53
56
|
await globalConfig.localStorage.setItem(
|
|
54
|
-
|
|
55
|
-
JSON.stringify(data.
|
|
57
|
+
userPinKey,
|
|
58
|
+
JSON.stringify(data.user?.brand_pinned_progress || {})
|
|
56
59
|
)
|
|
57
60
|
}
|
|
58
61
|
|
|
@@ -91,6 +94,7 @@ export async function login(email, password, deviceName, deviceToken, platform)
|
|
|
91
94
|
|
|
92
95
|
/**
|
|
93
96
|
* Logs the user out of the current session.
|
|
97
|
+
* Clears all cached data to prevent data leakage between users.
|
|
94
98
|
*
|
|
95
99
|
* @returns {Promise<void>}
|
|
96
100
|
*
|
|
@@ -108,6 +112,9 @@ export async function logout() {
|
|
|
108
112
|
'Content-Type': 'application/json',
|
|
109
113
|
},
|
|
110
114
|
})
|
|
115
|
+
|
|
116
|
+
// Clear all locally cached data to prevent data leakage between users
|
|
117
|
+
await clearAllCachedData()
|
|
111
118
|
}
|
|
112
119
|
|
|
113
120
|
/**
|
|
@@ -341,7 +341,7 @@ export async function removeUserPractice(id) {
|
|
|
341
341
|
}
|
|
342
342
|
|
|
343
343
|
/**
|
|
344
|
-
* Restores a previously deleted user's practice session by ID
|
|
344
|
+
* Restores a previously deleted user's practice session by ID
|
|
345
345
|
*
|
|
346
346
|
* @param {number} id - The unique identifier of the practice session to be restored.
|
|
347
347
|
* @returns {Promise<Object>} - A promise that resolves to the response containing the restored practice session data.
|
|
@@ -353,35 +353,7 @@ export async function removeUserPractice(id) {
|
|
|
353
353
|
* .catch(error => console.error(error));
|
|
354
354
|
*/
|
|
355
355
|
export async function restoreUserPractice(id) {
|
|
356
|
-
|
|
357
|
-
const response = await PUT(url, null)
|
|
358
|
-
if (response?.data?.length) {
|
|
359
|
-
const restoredPractice = response.data.find((p) => p.id === id)
|
|
360
|
-
if (restoredPractice) {
|
|
361
|
-
await userActivityContext.updateLocal(async function (localContext) {
|
|
362
|
-
if (!localContext.data[DATA_KEY_PRACTICES][restoredPractice.day]) {
|
|
363
|
-
localContext.data[DATA_KEY_PRACTICES][restoredPractice.day] = []
|
|
364
|
-
}
|
|
365
|
-
response.data.forEach((restoredPractice) => {
|
|
366
|
-
localContext.data[DATA_KEY_PRACTICES][restoredPractice.day].push({
|
|
367
|
-
id: restoredPractice.id,
|
|
368
|
-
duration_seconds: restoredPractice.duration_seconds,
|
|
369
|
-
})
|
|
370
|
-
})
|
|
371
|
-
})
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
const formattedMeta = await formatPracticeMeta(response.data || [])
|
|
375
|
-
const practiceDuration = formattedMeta.reduce(
|
|
376
|
-
(total, practice) => total + (practice.duration || 0),
|
|
377
|
-
0
|
|
378
|
-
)
|
|
379
|
-
return {
|
|
380
|
-
data: formattedMeta,
|
|
381
|
-
message: response.message,
|
|
382
|
-
version: response.version,
|
|
383
|
-
practiceDuration,
|
|
384
|
-
}
|
|
356
|
+
return await db.practices.restoreOne(id)
|
|
385
357
|
}
|
|
386
358
|
|
|
387
359
|
/**
|
|
@@ -422,25 +394,10 @@ export async function deletePracticeSession(day) {
|
|
|
422
394
|
* .catch(error => console.error("Restore failed:", error));
|
|
423
395
|
*/
|
|
424
396
|
export async function restorePracticeSession(date) {
|
|
425
|
-
const
|
|
426
|
-
const response = await
|
|
427
|
-
|
|
428
|
-
if (response?.data) {
|
|
429
|
-
await userActivityContext.updateLocal(async function (localContext) {
|
|
430
|
-
if (!localContext.data[DATA_KEY_PRACTICES][date]) {
|
|
431
|
-
localContext.data[DATA_KEY_PRACTICES][date] = []
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
response.data.forEach((restoredPractice) => {
|
|
435
|
-
localContext.data[DATA_KEY_PRACTICES][date].push({
|
|
436
|
-
id: restoredPractice.id,
|
|
437
|
-
duration_seconds: restoredPractice.duration_seconds,
|
|
438
|
-
})
|
|
439
|
-
})
|
|
440
|
-
})
|
|
441
|
-
}
|
|
397
|
+
const ids = await db.practices.queryAllDeletedIds(Q.where('date', date))
|
|
398
|
+
const response = await db.practices.restoreSome(ids.data)
|
|
442
399
|
|
|
443
|
-
const formattedMeta = await formatPracticeMeta(response
|
|
400
|
+
const formattedMeta = await formatPracticeMeta(response.data)
|
|
444
401
|
const practiceDuration = formattedMeta.reduce(
|
|
445
402
|
(total, practice) => total + (practice.duration || 0),
|
|
446
403
|
0
|