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,48 @@ 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 { }
141+ }
142+ }
143+
96144// TODO: It'd be nice to pass a signal in so we can abort all the parallel fetches
97145async function statelyProfile (
98146 res : express . Response ,
99147 components : ProfileComponent [ ] ,
100148 bungieMembershipId : number ,
101149 platformMembershipId : string | undefined ,
102150 destinyVersion : DestinyVersion ,
151+ incomingSyncTokens : { [ component : string ] : Buffer } = { } ,
103152) {
104- const response : ProfileResponse = { } ;
153+ const response : ProfileResponse = {
154+ sync : Boolean ( incomingSyncTokens ) ,
155+ } ;
156+ const timerPrefix = response . sync ? 'profileSync' : 'profileStately' ;
157+ const counterPrefix = response . sync ? 'sync' : 'stately' ;
105158 const syncTokens : { [ component : string ] : string } = { } ;
106159 const addSyncToken = ( name : string , token : ListToken ) => {
107160 if ( token . canSync ) {
108161 syncTokens [ name ] = Buffer . from ( token . tokenData ) . toString ( 'base64' ) ;
109162 }
110163 } ;
164+ const getSyncToken = ( name : string ) => {
165+ const tokenData = incomingSyncTokens . settings ;
166+ if ( incomingSyncTokens && ! tokenData ) {
167+ throw new Error ( `Missing sync token: ${ name } ` ) ;
168+ }
169+ return tokenData ;
170+ } ;
111171
112172 // We'll accumulate promises and await them all at the end
113173 const promises : Promise < void > [ ] = [ ] ;
@@ -116,11 +176,13 @@ async function statelyProfile(
116176 promises . push (
117177 ( async ( ) => {
118178 const start = new Date ( ) ;
119- const { settings : storedSettings , token : settingsToken } =
120- await querySettings ( bungieMembershipId ) ;
179+ const tokenData = getSyncToken ( 'settings' ) ;
180+ const { settings : storedSettings , token : settingsToken } = tokenData
181+ ? await syncSettings ( tokenData )
182+ : await querySettings ( bungieMembershipId ) ;
121183 response . settings = storedSettings ;
122184 addSyncToken ( 'settings' , settingsToken ) ;
123- metrics . timing ( 'profileStately .settings' , start ) ;
185+ metrics . timing ( ` ${ timerPrefix } .settings` , start ) ;
124186 } ) ( ) ,
125187 ) ;
126188 }
@@ -133,17 +195,20 @@ async function statelyProfile(
133195 )
134196 ) {
135197 const start = new Date ( ) ;
136- const { profile : profileResponse , token : profileToken } = await getProfile (
137- platformMembershipId ,
138- destinyVersion ,
139- ) ;
140- metrics . timing ( 'profileStately .allComponents' , start ) ;
198+ const tokenData = getSyncToken ( 'profile' ) ;
199+ const { profile : profileResponse , token : profileToken } = tokenData
200+ ? await syncProfile ( tokenData )
201+ : await getProfile ( platformMembershipId , destinyVersion ) ;
202+ metrics . timing ( ` ${ timerPrefix } .allComponents` , start ) ;
141203 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 ) ;
204+ metrics . timing ( `${ counterPrefix } .loadouts.numReturned` , profileResponse . loadouts ?. length ?? 0 ) ;
205+ metrics . timing ( `${ counterPrefix } .tags.numReturned` , profileResponse . tags ?. length ?? 0 ) ;
206+ metrics . timing (
207+ `${ counterPrefix } .hashtags.numReturned` ,
208+ profileResponse . itemHashTags ?. length ?? 0 ,
209+ ) ;
210+ metrics . timing ( `${ counterPrefix } .triumphs.numReturned` , profileResponse . triumphs ?. length ?? 0 ) ;
211+ metrics . timing ( `${ counterPrefix } .searches.numReturned` , profileResponse . searches ?. length ?? 0 ) ;
147212 addSyncToken ( 'profile' , profileToken ) ;
148213 response . syncToken = serializeSyncToken ( syncTokens ) ;
149214 return { ...response , ...profileResponse } ;
@@ -157,14 +222,15 @@ async function statelyProfile(
157222 promises . push (
158223 ( async ( ) => {
159224 const start = new Date ( ) ;
160- const { loadouts , token } = await getLoadoutsForProfileStately (
161- platformMembershipId ,
162- destinyVersion ,
163- ) ;
225+ const tokenData = getSyncToken ( 'loadouts' ) ;
226+ const { loadouts , token , deletedLoadoutIds } = tokenData
227+ ? await syncLoadouts ( tokenData )
228+ : await getLoadoutsForProfileStately ( platformMembershipId , destinyVersion ) ;
164229 response . loadouts = loadouts ;
230+ response . deletedLoadoutIds = deletedLoadoutIds ;
165231 addSyncToken ( 'loadouts' , token ) ;
166- metrics . timing ( 'profile .loadouts.numReturned' , response . loadouts . length ) ;
167- metrics . timing ( 'profileStately .loadouts' , start ) ;
232+ metrics . timing ( ` ${ counterPrefix } .loadouts.numReturned` , response . loadouts . length ) ;
233+ metrics . timing ( ` ${ timerPrefix } .loadouts` , start ) ;
168234 } ) ( ) ,
169235 ) ;
170236 }
@@ -177,11 +243,12 @@ async function statelyProfile(
177243 promises . push (
178244 ( async ( ) => {
179245 const start = new Date ( ) ;
180- const { tags , token } = await getItemAnnotationsForProfileStately (
181- platformMembershipId ,
182- destinyVersion ,
183- ) ;
246+ const tokenData = getSyncToken ( 'tags' ) ;
247+ const { tags , token , deletedTagsIds } = tokenData
248+ ? await syncItemAnnotations ( tokenData )
249+ : await getItemAnnotationsForProfileStately ( platformMembershipId , destinyVersion ) ;
184250 response . tags = tags ;
251+ response . deletedTagsIds = deletedTagsIds ;
185252 addSyncToken ( 'tags' , token ) ;
186253 metrics . timing ( 'profile.tags.numReturned' , response . tags . length ) ;
187254 metrics . timing ( 'profileStately.tags' , start ) ;
@@ -197,8 +264,12 @@ async function statelyProfile(
197264 promises . push (
198265 ( async ( ) => {
199266 const start = new Date ( ) ;
200- const { hashTags, token } = await getItemHashTagsForProfileStately ( platformMembershipId ) ;
267+ const tokenData = getSyncToken ( 'hashtags' ) ;
268+ const { hashTags, token, deletedItemHashTagHashes } = tokenData
269+ ? await syncItemHashTags ( tokenData )
270+ : await getItemHashTagsForProfileStately ( platformMembershipId ) ;
201271 response . itemHashTags = hashTags ;
272+ response . deletedItemHashTagHashes = deletedItemHashTagHashes ;
202273 addSyncToken ( 'hashtags' , token ) ;
203274 metrics . timing ( 'profile.hashtags.numReturned' , response . itemHashTags . length ) ;
204275 metrics . timing ( 'profileStately.hashtags' , start ) ;
@@ -214,8 +285,12 @@ async function statelyProfile(
214285 promises . push (
215286 ( async ( ) => {
216287 const start = new Date ( ) ;
217- const { triumphs, token } = await getTrackedTriumphsForProfileStately ( platformMembershipId ) ;
288+ const tokenData = getSyncToken ( 'triumphs' ) ;
289+ const { triumphs, token, deletedTriumphs } = tokenData
290+ ? await syncTrackedTriumphs ( tokenData )
291+ : await getTrackedTriumphsForProfileStately ( platformMembershipId ) ;
218292 response . triumphs = triumphs ;
293+ response . deletedTriumphs = deletedTriumphs ;
219294 addSyncToken ( 'triumphs' , token ) ;
220295 metrics . timing ( 'profile.triumphs.numReturned' , response . triumphs . length ) ;
221296 metrics . timing ( 'profileStately.triumphs' , start ) ;
@@ -231,11 +306,12 @@ async function statelyProfile(
231306 promises . push (
232307 ( async ( ) => {
233308 const start = new Date ( ) ;
234- const { searches , token } = await getSearchesForProfileStately (
235- platformMembershipId ,
236- destinyVersion ,
237- ) ;
309+ const tokenData = getSyncToken ( 'searches' ) ;
310+ const { searches , token , deletedSearchHashes } = tokenData
311+ ? await syncSearches ( tokenData )
312+ : await getSearchesForProfileStately ( platformMembershipId , destinyVersion ) ;
238313 response . searches = searches ;
314+ response . deletedSearchHashes = deletedSearchHashes ;
239315 addSyncToken ( 'searches' , token ) ;
240316 metrics . timing ( 'profile.searches.numReturned' , response . searches . length ) ;
241317 metrics . timing ( 'profileStately.searches' , start ) ;
0 commit comments