1+ import * as Sentry from '@sentry/node' ;
12import { ListToken } from '@stately-cloud/client' ;
23import express from 'express' ;
34import asyncHandler from 'express-async-handler' ;
@@ -6,13 +7,28 @@ import { ApiApp } from '../shapes/app.js';
67import { DestinyVersion } from '../shapes/general.js' ;
78import { ProfileResponse } from '../shapes/profile.js' ;
89import { UserInfo } from '../shapes/user.js' ;
9- import { getProfile } from '../stately/bulk-queries.js' ;
10- import { getItemAnnotationsForProfile as getItemAnnotationsForProfileStately } from '../stately/item-annotations-queries.js' ;
11- import { getItemHashTagsForProfile as getItemHashTagsForProfileStately } from '../stately/item-hash-tags-queries.js' ;
12- import { getLoadoutsForProfile as getLoadoutsForProfileStately } from '../stately/loadouts-queries.js' ;
13- import { getSearchesForProfile as getSearchesForProfileStately } from '../stately/searches-queries.js' ;
14- import { querySettings } from '../stately/settings-queries.js' ;
15- import { getTrackedTriumphsForProfile as getTrackedTriumphsForProfileStately } from '../stately/triumphs-queries.js' ;
10+ import { getProfile , syncProfile } from '../stately/bulk-queries.js' ;
11+ import {
12+ getItemAnnotationsForProfile as getItemAnnotationsForProfileStately ,
13+ syncItemAnnotations ,
14+ } from '../stately/item-annotations-queries.js' ;
15+ import {
16+ getItemHashTagsForProfile as getItemHashTagsForProfileStately ,
17+ syncItemHashTags ,
18+ } from '../stately/item-hash-tags-queries.js' ;
19+ import {
20+ getLoadoutsForProfile as getLoadoutsForProfileStately ,
21+ syncLoadouts ,
22+ } from '../stately/loadouts-queries.js' ;
23+ import {
24+ getSearchesForProfile as getSearchesForProfileStately ,
25+ syncSearches ,
26+ } from '../stately/searches-queries.js' ;
27+ import { querySettings , syncSettings } from '../stately/settings-queries.js' ;
28+ import {
29+ getTrackedTriumphsForProfile as getTrackedTriumphsForProfileStately ,
30+ syncTrackedTriumphs ,
31+ } from '../stately/triumphs-queries.js' ;
1632import { badRequest , checkPlatformMembershipId , isValidPlatformMembershipId } from '../utils.js' ;
1733
1834type ProfileComponent = 'settings' | 'loadouts' | 'tags' | 'hashtags' | 'triumphs' | 'searches' ;
@@ -74,13 +90,31 @@ export const profileHandler = asyncHandler(async (req, res) => {
7490 return ;
7591 }
7692
77- const response = await statelyProfile (
78- res ,
79- components ,
80- bungieMembershipId ,
81- platformMembershipId ,
82- destinyVersion ,
83- ) ;
93+ const syncTokens = extractSyncToken ( req . query . sync ?. toString ( ) ) ;
94+
95+ let response : ProfileResponse | undefined ;
96+ try {
97+ response = await statelyProfile (
98+ res ,
99+ components ,
100+ bungieMembershipId ,
101+ platformMembershipId ,
102+ destinyVersion ,
103+ syncTokens ,
104+ ) ;
105+ } catch ( e ) {
106+ Sentry . captureException ( e , { extra : { syncTokens, components, platformMembershipId } } ) ;
107+ if ( syncTokens ) {
108+ // Start over without sync tokens
109+ response = await statelyProfile (
110+ res ,
111+ components ,
112+ bungieMembershipId ,
113+ platformMembershipId ,
114+ destinyVersion ,
115+ ) ;
116+ }
117+ }
84118
85119 if ( ! response ) {
86120 return ; // we've already responded
@@ -92,22 +126,50 @@ export const profileHandler = asyncHandler(async (req, res) => {
92126 res . send ( response ) ;
93127} ) ;
94128
95- // TODO: Probably could enable allowStale, since profiles are cached anyway
129+ function extractSyncToken ( syncTokenParam : string | undefined ) {
130+ if ( syncTokenParam ) {
131+ try {
132+ const tokenMap = JSON . parse ( syncTokenParam ) as { [ component : string ] : string } ;
133+ return Object . entries ( tokenMap ) . reduce < { [ component : string ] : Buffer } > (
134+ ( acc , [ component , token ] ) => {
135+ acc [ component ] = Buffer . from ( token , 'base64' ) ;
136+ return acc ;
137+ } ,
138+ { } ,
139+ ) ;
140+ } catch ( e ) {
141+ Sentry . captureException ( e , { extra : { syncTokenParam } } ) ;
142+ }
143+ }
144+ }
145+
96146// TODO: It'd be nice to pass a signal in so we can abort all the parallel fetches
97147async function statelyProfile (
98148 res : express . Response ,
99149 components : ProfileComponent [ ] ,
100150 bungieMembershipId : number ,
101151 platformMembershipId : string | undefined ,
102152 destinyVersion : DestinyVersion ,
153+ incomingSyncTokens ?: { [ component : string ] : Buffer } ,
103154) {
104- const response : ProfileResponse = { } ;
155+ const response : ProfileResponse = {
156+ sync : Boolean ( incomingSyncTokens ) ,
157+ } ;
158+ const timerPrefix = response . sync ? 'profileSync' : 'profileStately' ;
159+ const counterPrefix = response . sync ? 'sync' : 'stately' ;
105160 const syncTokens : { [ component : string ] : string } = { } ;
106161 const addSyncToken = ( name : string , token : ListToken ) => {
107162 if ( token . canSync ) {
108163 syncTokens [ name ] = Buffer . from ( token . tokenData ) . toString ( 'base64' ) ;
109164 }
110165 } ;
166+ const getSyncToken = ( name : string ) => {
167+ const tokenData = incomingSyncTokens ?. settings ;
168+ if ( incomingSyncTokens && ! tokenData ) {
169+ throw new Error ( `Missing sync token: ${ name } ` ) ;
170+ }
171+ return tokenData ;
172+ } ;
111173
112174 // We'll accumulate promises and await them all at the end
113175 const promises : Promise < void > [ ] = [ ] ;
@@ -116,11 +178,13 @@ async function statelyProfile(
116178 promises . push (
117179 ( async ( ) => {
118180 const start = new Date ( ) ;
119- const { settings : storedSettings , token : settingsToken } =
120- await querySettings ( bungieMembershipId ) ;
181+ const tokenData = getSyncToken ( 'settings' ) ;
182+ const { settings : storedSettings , token : settingsToken } = tokenData
183+ ? await syncSettings ( tokenData )
184+ : await querySettings ( bungieMembershipId ) ;
121185 response . settings = storedSettings ;
122186 addSyncToken ( 'settings' , settingsToken ) ;
123- metrics . timing ( 'profileStately .settings' , start ) ;
187+ metrics . timing ( ` ${ timerPrefix } .settings` , start ) ;
124188 } ) ( ) ,
125189 ) ;
126190 }
@@ -133,17 +197,20 @@ async function statelyProfile(
133197 )
134198 ) {
135199 const start = new Date ( ) ;
136- const { profile : profileResponse , token : profileToken } = await getProfile (
137- platformMembershipId ,
138- destinyVersion ,
139- ) ;
140- metrics . timing ( 'profileStately .allComponents' , start ) ;
200+ const tokenData = getSyncToken ( 'profile' ) ;
201+ const { profile : profileResponse , token : profileToken } = tokenData
202+ ? await syncProfile ( tokenData )
203+ : await getProfile ( platformMembershipId , destinyVersion ) ;
204+ metrics . timing ( ` ${ timerPrefix } .allComponents` , start ) ;
141205 await Promise . all ( promises ) ; // wait for settings
142- metrics . timing ( 'profile.loadouts.numReturned' , profileResponse . loadouts ?. length ?? 0 ) ;
143- metrics . timing ( 'profile.tags.numReturned' , profileResponse . tags ?. length ?? 0 ) ;
144- metrics . timing ( 'profile.hashtags.numReturned' , profileResponse . itemHashTags ?. length ?? 0 ) ;
145- metrics . timing ( 'profile.triumphs.numReturned' , profileResponse . triumphs ?. length ?? 0 ) ;
146- metrics . timing ( 'profile.searches.numReturned' , profileResponse . searches ?. length ?? 0 ) ;
206+ metrics . timing ( `${ counterPrefix } .loadouts.numReturned` , profileResponse . loadouts ?. length ?? 0 ) ;
207+ metrics . timing ( `${ counterPrefix } .tags.numReturned` , profileResponse . tags ?. length ?? 0 ) ;
208+ metrics . timing (
209+ `${ counterPrefix } .hashtags.numReturned` ,
210+ profileResponse . itemHashTags ?. length ?? 0 ,
211+ ) ;
212+ metrics . timing ( `${ counterPrefix } .triumphs.numReturned` , profileResponse . triumphs ?. length ?? 0 ) ;
213+ metrics . timing ( `${ counterPrefix } .searches.numReturned` , profileResponse . searches ?. length ?? 0 ) ;
147214 addSyncToken ( 'profile' , profileToken ) ;
148215 response . syncToken = serializeSyncToken ( syncTokens ) ;
149216 return { ...response , ...profileResponse } ;
@@ -157,14 +224,15 @@ async function statelyProfile(
157224 promises . push (
158225 ( async ( ) => {
159226 const start = new Date ( ) ;
160- const { loadouts , token } = await getLoadoutsForProfileStately (
161- platformMembershipId ,
162- destinyVersion ,
163- ) ;
227+ const tokenData = getSyncToken ( 'loadouts' ) ;
228+ const { loadouts , token , deletedLoadoutIds } = tokenData
229+ ? await syncLoadouts ( tokenData )
230+ : await getLoadoutsForProfileStately ( platformMembershipId , destinyVersion ) ;
164231 response . loadouts = loadouts ;
232+ response . deletedLoadoutIds = deletedLoadoutIds ;
165233 addSyncToken ( 'loadouts' , token ) ;
166- metrics . timing ( 'profile .loadouts.numReturned' , response . loadouts . length ) ;
167- metrics . timing ( 'profileStately .loadouts' , start ) ;
234+ metrics . timing ( ` ${ counterPrefix } .loadouts.numReturned` , response . loadouts . length ) ;
235+ metrics . timing ( ` ${ timerPrefix } .loadouts` , start ) ;
168236 } ) ( ) ,
169237 ) ;
170238 }
@@ -177,14 +245,15 @@ async function statelyProfile(
177245 promises . push (
178246 ( async ( ) => {
179247 const start = new Date ( ) ;
180- const { tags , token } = await getItemAnnotationsForProfileStately (
181- platformMembershipId ,
182- destinyVersion ,
183- ) ;
248+ const tokenData = getSyncToken ( 'tags' ) ;
249+ const { tags , token , deletedTagsIds } = tokenData
250+ ? await syncItemAnnotations ( tokenData )
251+ : await getItemAnnotationsForProfileStately ( platformMembershipId , destinyVersion ) ;
184252 response . tags = tags ;
253+ response . deletedTagsIds = deletedTagsIds ;
185254 addSyncToken ( 'tags' , token ) ;
186- metrics . timing ( 'profile .tags.numReturned' , response . tags . length ) ;
187- metrics . timing ( 'profileStately .tags' , start ) ;
255+ metrics . timing ( ` ${ counterPrefix } .tags.numReturned` , response . tags . length ) ;
256+ metrics . timing ( ` ${ timerPrefix } .tags` , start ) ;
188257 } ) ( ) ,
189258 ) ;
190259 }
@@ -197,11 +266,15 @@ async function statelyProfile(
197266 promises . push (
198267 ( async ( ) => {
199268 const start = new Date ( ) ;
200- const { hashTags, token } = await getItemHashTagsForProfileStately ( platformMembershipId ) ;
269+ const tokenData = getSyncToken ( 'hashtags' ) ;
270+ const { hashTags, token, deletedItemHashTagHashes } = tokenData
271+ ? await syncItemHashTags ( tokenData )
272+ : await getItemHashTagsForProfileStately ( platformMembershipId ) ;
201273 response . itemHashTags = hashTags ;
274+ response . deletedItemHashTagHashes = deletedItemHashTagHashes ;
202275 addSyncToken ( 'hashtags' , token ) ;
203- metrics . timing ( 'profile .hashtags.numReturned' , response . itemHashTags . length ) ;
204- metrics . timing ( 'profileStately .hashtags' , start ) ;
276+ metrics . timing ( ` ${ counterPrefix } .hashtags.numReturned` , response . itemHashTags . length ) ;
277+ metrics . timing ( ` ${ timerPrefix } .hashtags` , start ) ;
205278 } ) ( ) ,
206279 ) ;
207280 }
@@ -214,11 +287,15 @@ async function statelyProfile(
214287 promises . push (
215288 ( async ( ) => {
216289 const start = new Date ( ) ;
217- const { triumphs, token } = await getTrackedTriumphsForProfileStately ( platformMembershipId ) ;
290+ const tokenData = getSyncToken ( 'triumphs' ) ;
291+ const { triumphs, token, deletedTriumphs } = tokenData
292+ ? await syncTrackedTriumphs ( tokenData )
293+ : await getTrackedTriumphsForProfileStately ( platformMembershipId ) ;
218294 response . triumphs = triumphs ;
295+ response . deletedTriumphs = deletedTriumphs ;
219296 addSyncToken ( 'triumphs' , token ) ;
220- metrics . timing ( 'profile .triumphs.numReturned' , response . triumphs . length ) ;
221- metrics . timing ( 'profileStately .triumphs' , start ) ;
297+ metrics . timing ( ` ${ counterPrefix } .triumphs.numReturned` , response . triumphs . length ) ;
298+ metrics . timing ( ` ${ timerPrefix } .triumphs` , start ) ;
222299 } ) ( ) ,
223300 ) ;
224301 }
@@ -231,14 +308,15 @@ async function statelyProfile(
231308 promises . push (
232309 ( async ( ) => {
233310 const start = new Date ( ) ;
234- const { searches , token } = await getSearchesForProfileStately (
235- platformMembershipId ,
236- destinyVersion ,
237- ) ;
311+ const tokenData = getSyncToken ( 'searches' ) ;
312+ const { searches , token , deletedSearchHashes } = tokenData
313+ ? await syncSearches ( tokenData )
314+ : await getSearchesForProfileStately ( platformMembershipId , destinyVersion ) ;
238315 response . searches = searches ;
316+ response . deletedSearchHashes = deletedSearchHashes ;
239317 addSyncToken ( 'searches' , token ) ;
240- metrics . timing ( 'profile .searches.numReturned' , response . searches . length ) ;
241- metrics . timing ( 'profileStately .searches' , start ) ;
318+ metrics . timing ( ` ${ counterPrefix } .searches.numReturned` , response . searches . length ) ;
319+ metrics . timing ( ` ${ timerPrefix } .searches` , start ) ;
242320 } ) ( ) ,
243321 ) ;
244322 }
0 commit comments