Skip to content

Commit 4edff75

Browse files
committed
Use sync tokens if they're passed in
1 parent f3f2a83 commit 4edff75

File tree

9 files changed

+376
-61
lines changed

9 files changed

+376
-61
lines changed

api/routes/profile.ts

Lines changed: 121 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as Sentry from '@sentry/node';
12
import { ListToken } from '@stately-cloud/client';
23
import express from 'express';
34
import asyncHandler from 'express-async-handler';
@@ -6,13 +7,28 @@ import { ApiApp } from '../shapes/app.js';
67
import { DestinyVersion } from '../shapes/general.js';
78
import { ProfileResponse } from '../shapes/profile.js';
89
import { 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';
1632
import { badRequest, checkPlatformMembershipId, isValidPlatformMembershipId } from '../utils.js';
1733

1834
type 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
97145
async 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);

api/stately/bulk-queries.ts

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { convertItemHashTag, keyFor as hashTagKeyFor } from './item-hash-tags-qu
1212
import { convertLoadoutFromStately, keyFor as loadoutKeyFor } from './loadouts-queries.js';
1313
import { convertSearchFromStately, keyFor as searchKeyFor } from './searches-queries.js';
1414
import { deleteSettings, getSettings } from './settings-queries.js';
15-
import { batches } from './stately-utils.js';
15+
import { batches, parseKeyPath } from './stately-utils.js';
1616
import { keyFor as triumphKeyFor } from './triumphs-queries.js';
1717

1818
/**
@@ -212,3 +212,69 @@ export async function getProfile(
212212

213213
return { profile: response, token: iter.token! };
214214
}
215+
216+
export async function syncProfile(
217+
tokenData: Buffer,
218+
): Promise<{ profile: ProfileResponse; token: ListToken }> {
219+
const response: ProfileResponse = {
220+
sync: true,
221+
};
222+
223+
// Now get all the data under the profile in one listing.
224+
const iter = client.syncList(tokenData);
225+
for await (const change of iter) {
226+
switch (change.type) {
227+
case 'reset': {
228+
response.sync = false;
229+
break;
230+
}
231+
case 'changed': {
232+
const item = change.item;
233+
if (client.isType(item, 'Triumph')) {
234+
(response.triumphs ??= []).push(item.recordHash);
235+
} else if (client.isType(item, 'ItemAnnotation')) {
236+
(response.tags ??= []).push(convertItemAnnotation(item));
237+
} else if (client.isType(item, 'ItemHashTag')) {
238+
(response.itemHashTags ??= []).push(convertItemHashTag(item));
239+
} else if (client.isType(item, 'Loadout')) {
240+
(response.loadouts ??= []).push(convertLoadoutFromStately(item));
241+
} else if (client.isType(item, 'Search')) {
242+
(response.searches ??= []).push(convertSearchFromStately(item));
243+
}
244+
break;
245+
}
246+
case 'deleted': {
247+
const keyPath = parseKeyPath(change.keyPath);
248+
if (keyPath[0].ns === 'p') {
249+
const lastPart = keyPath.at(-1)!;
250+
const idStr = lastPart.id;
251+
const type = lastPart.ns;
252+
switch (type) {
253+
case 'triumph': {
254+
(response.deletedTriumphs ??= []).push(Number(idStr));
255+
break;
256+
}
257+
case 'ia': {
258+
(response.deletedTagsIds ??= []).push(idStr);
259+
break;
260+
}
261+
case 'ia-hash': {
262+
(response.deletedItemHashTagHashes ??= []).push(Number(idStr));
263+
break;
264+
}
265+
case 'loadout': {
266+
(response.deletedLoadoutIds ??= []).push(idStr);
267+
break;
268+
}
269+
case 'search': {
270+
(response.deletedSearchHashes ??= []).push(idStr);
271+
break;
272+
}
273+
}
274+
}
275+
}
276+
}
277+
}
278+
279+
return { profile: response, token: iter.token! };
280+
}

0 commit comments

Comments
 (0)