Skip to content

Commit c818e7d

Browse files
authored
Turn on settings automigration for everyone (#295)
* Turn on settings automigration for everyone * Cleanup * Fix type * Prefer postgres over stately? * Typing * Really prefer postgres * Don't worry about missing tokens * More test * Fix import * More robust token handling
1 parent dba63d9 commit c818e7d

7 files changed

Lines changed: 83 additions & 56 deletions

File tree

api/db/settings-queries.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export async function getSettings(
99
bungieMembershipId: number,
1010
): Promise<{ settings: Partial<Settings>; deleted: boolean; lastModifiedAt: number } | undefined> {
1111
const results = await client.query<{
12-
settings: Settings;
12+
settings: Partial<Settings>;
1313
deleted_at: Date | null;
1414
last_updated_at: Date;
1515
}>({

api/routes/import.ts

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { uniqBy } from 'es-toolkit';
22
import { isEmpty } from 'es-toolkit/compat';
33
import asyncHandler from 'express-async-handler';
4+
import { transaction } from '../db/index.js';
5+
import { replaceSettings } from '../db/settings-queries.js';
46
import { ExportResponse } from '../shapes/export.js';
57
import { DestinyVersion } from '../shapes/general.js';
68
import { ImportResponse } from '../shapes/import.js';
@@ -15,7 +17,6 @@ import { importTags } from '../stately/item-annotations-queries.js';
1517
import { importHashTags } from '../stately/item-hash-tags-queries.js';
1618
import { importLoadouts } from '../stately/loadouts-queries.js';
1719
import { importSearches } from '../stately/searches-queries.js';
18-
import { convertToStatelyItem } from '../stately/settings-queries.js';
1920
import { batches } from '../stately/stately-utils.js';
2021
import { importTriumphs } from '../stately/triumphs-queries.js';
2122
import { badRequest, delay, subtractObject } from '../utils.js';
@@ -119,11 +120,6 @@ export async function statelyImport(
119120
let numTriumphs = 0;
120121
await deleteAllDataForUser(bungieMembershipId, platformMembershipIds);
121122

122-
const settingsItem = convertToStatelyItem(
123-
{ ...defaultSettings, ...settings },
124-
bungieMembershipId,
125-
);
126-
127123
// The export will have duplicates because import saved to each profile
128124
// instead of the one that was exported.
129125
itemHashTags = uniqBy(itemHashTags, (a) => a.hash);
@@ -152,10 +148,9 @@ export async function statelyImport(
152148
items.push(...importSearches(platformMembershipId, searches));
153149
}
154150

155-
// Put the settings in first since it's in a different group
156-
await client.put({
157-
item: settingsItem,
158-
});
151+
// Settings live in Postgres now
152+
await transaction(async (client) => replaceSettings(client, bungieMembershipId, settings));
153+
159154
// OK now put them in as fast as we can
160155
for (const batch of batches(items)) {
161156
// We shouldn't have any existing items...

api/routes/profile.ts

Lines changed: 20 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,10 @@ function extractSyncToken(syncTokenParam: string | undefined) {
147147
const tokenMap = JSON.parse(syncTokenParam) as { [component: string]: string | number };
148148
return Object.entries(tokenMap).reduce<{ [component: string]: Buffer | number }>(
149149
(acc, [component, token]) => {
150-
acc[component] = typeof token === 'string' ? Buffer.from(token, 'base64') : token;
150+
acc[component] =
151+
typeof token === 'string' && !/^\d+$/.exec(token)
152+
? Buffer.from(token, 'base64')
153+
: Number(token);
151154
return acc;
152155
},
153156
{},
@@ -172,7 +175,7 @@ async function statelyProfile(
172175
};
173176
const timerPrefix = response.sync ? 'profileSync' : 'profileStately';
174177
const counterPrefix = response.sync ? 'sync' : 'stately';
175-
const syncTokens: { [component: string]: string } = {};
178+
const syncTokens: { [component: string]: string | number } = {};
176179
const addSyncToken = (
177180
name: string,
178181
token: ListToken | { canSync: boolean; tokenData: number },
@@ -181,14 +184,14 @@ async function statelyProfile(
181184
syncTokens[name] =
182185
token.tokenData instanceof Uint8Array
183186
? Buffer.from(token.tokenData).toString('base64')
184-
: token.tokenData.toString();
187+
: token.tokenData;
185188
}
186189
};
187190
const getSyncToken = <T extends number | Buffer>(name: string) => {
188191
const tokenData = incomingSyncTokens?.[name];
189-
if (incomingSyncTokens && !tokenData) {
190-
throw new Error(`Missing sync token: ${name}`);
191-
}
192+
// if (incomingSyncTokens && !tokenData) {
193+
// throw new Error(`Missing sync token: ${name}`);
194+
// }
192195
return tokenData as T | undefined;
193196
};
194197

@@ -202,29 +205,22 @@ async function statelyProfile(
202205
// Load settings from Stately. If they're there, you're done. Otherwise load from Postgres.
203206
const start = new Date();
204207

205-
const statelySettings = await querySettings(bungieMembershipId);
206-
if (!statelySettings.settings) {
207-
const now = Date.now();
208-
const pgSettings = await readTransaction(async (pgClient) =>
209-
getSettings(pgClient, bungieMembershipId),
210-
);
211-
if (pgSettings) {
212-
const tokenData = getSyncToken<number>('s');
213-
if (tokenData === undefined || pgSettings.lastModifiedAt > tokenData) {
214-
response.settings = { ...defaultSettings, ...pgSettings.settings };
215-
}
216-
} else {
217-
response.settings = defaultSettings;
208+
const now = Date.now();
209+
// TODO: Should add the token to the query to avoid fetching if unchanged
210+
const pgSettings = await readTransaction(async (pgClient) =>
211+
getSettings(pgClient, bungieMembershipId),
212+
);
213+
if (pgSettings) {
214+
const tokenData = getSyncToken<number>('s');
215+
if (tokenData === undefined || pgSettings.lastModifiedAt > tokenData) {
216+
response.settings = { ...defaultSettings, ...pgSettings.settings };
218217
}
219218
addSyncToken('s', { canSync: true, tokenData: pgSettings?.lastModifiedAt ?? now });
220219
} else {
221220
const tokenData = getSyncToken<Buffer>('settings');
222221
const { settings: storedSettings, token: settingsToken } = tokenData
223222
? await syncSettings(tokenData)
224-
: {
225-
settings: statelySettings.settings ?? defaultSettings,
226-
token: statelySettings.token,
227-
};
223+
: await querySettings(bungieMembershipId);
228224
response.settings = storedSettings;
229225
addSyncToken('settings', settingsToken);
230226
}
@@ -335,6 +331,6 @@ async function statelyProfile(
335331
return response;
336332
}
337333

338-
function serializeSyncToken(syncTokens: { [component: string]: string }) {
334+
function serializeSyncToken(syncTokens: { [component: string]: string | number }) {
339335
return JSON.stringify(syncTokens);
340336
}

api/routes/update.ts

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@ import { captureMessage } from '@sentry/node';
22
import { chunk, groupBy, partition, sortBy } from 'es-toolkit';
33
import express from 'express';
44
import asyncHandler from 'express-async-handler';
5-
import { transaction } from '../db/index.js';
5+
import { readTransaction, transaction } from '../db/index.js';
66
import { backfillMigrationState } from '../db/migration-state-queries.js';
7-
import { replaceSettings, setSetting as setSettingInPostgres } from '../db/settings-queries.js';
7+
import {
8+
getSettings,
9+
replaceSettings,
10+
setSetting as setSettingInPostgres,
11+
} from '../db/settings-queries.js';
812
import { metrics } from '../metrics/index.js';
913
import { ApiApp } from '../shapes/app.js';
1014
import { DestinyVersion } from '../shapes/general.js';
@@ -43,11 +47,7 @@ import {
4347
UpdateSearch,
4448
updateSearches,
4549
} from '../stately/searches-queries.js';
46-
import {
47-
deleteSettings as deleteSettingsInStately,
48-
getSettingsForUpdate,
49-
setSetting as setSettingInStately,
50-
} from '../stately/settings-queries.js';
50+
import { getSettingsForUpdate, keyFor } from '../stately/settings-queries.js';
5151
import { trackUntrackTriumphs } from '../stately/triumphs-queries.js';
5252
import {
5353
badRequest,
@@ -313,9 +313,16 @@ async function statelyUpdate(
313313
mergedSettings = { ...mergedSettings, ...update.payload };
314314
}
315315

316-
if (bungieMembershipId === 7094) {
316+
// TODO: Remove the check for settings in Postgres once we're fully migrated off Stately
317+
const pgSettings = await readTransaction((client) =>
318+
getSettings(client, bungieMembershipId),
319+
);
320+
if (pgSettings) {
321+
await transaction(async (pgClient) => {
322+
await setSettingInPostgres(pgClient, bungieMembershipId, mergedSettings);
323+
});
324+
} else {
317325
const statelySettings = await getSettingsForUpdate(txn, bungieMembershipId);
318-
319326
if (statelySettings) {
320327
mergedSettings = { ...statelySettings, ...mergedSettings };
321328
await transaction(async (pgClient) => {
@@ -325,14 +332,12 @@ async function statelyUpdate(
325332
subtractObject(mergedSettings, defaultSettings),
326333
);
327334
});
328-
await deleteSettingsInStately(bungieMembershipId);
335+
await txn.del(keyFor(bungieMembershipId));
329336
} else {
330337
await transaction(async (pgClient) => {
331338
await setSettingInPostgres(pgClient, bungieMembershipId, mergedSettings);
332339
});
333340
}
334-
} else {
335-
await setSettingInStately(txn, bungieMembershipId, mergedSettings);
336341
}
337342
break;
338343
}

api/server.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,11 +210,40 @@ describe('profile', () => {
210210

211211
expect(profileSyncResponse.syncToken).toBeDefined();
212212
expect(profileSyncResponse.syncToken).not.toBe(profileResponse.syncToken);
213+
expect(profileSyncResponse.syncToken).toContain('"s":');
213214
expect(profileSyncResponse.sync).toBe(true);
214215
expect(profileSyncResponse.settings?.showNewItems).toBe(true);
215216
expect(profileSyncResponse.tags?.length).toBe(1);
216217
expect(profileSyncResponse.tags?.[0].id).toBe('1234');
217218
expect(profileSyncResponse.deletedLoadoutIds?.length).toBe(1);
219+
220+
const request2: ProfileUpdateRequest = {
221+
platformMembershipId,
222+
destinyVersion: 2,
223+
updates: [
224+
{
225+
action: 'setting',
226+
payload: {
227+
compareBaseStats: true,
228+
},
229+
},
230+
],
231+
};
232+
await postRequestAuthed('/profile', request2).expect(200);
233+
234+
const profileSyncResponse2 = (await getRequestAuthed(
235+
`/profile?components=settings,loadouts,tags,triumphs,searches,hashtags&platformMembershipId=${platformMembershipId}&sync=${encodeURIComponent(profileSyncResponse.syncToken!)}`,
236+
)
237+
.expect(200)
238+
.json()) as ProfileResponse;
239+
240+
expect(profileSyncResponse2.syncToken).toBeDefined();
241+
expect(profileSyncResponse2.syncToken).not.toBe(profileSyncResponse.syncToken);
242+
expect(profileSyncResponse2.syncToken).toContain('"s":');
243+
expect(profileSyncResponse2.sync).toBe(true);
244+
expect(profileSyncResponse2.settings).toBeDefined();
245+
expect(profileSyncResponse.settings?.compareBaseStats).toBe(true);
246+
expect(profileSyncResponse2.settings?.showNewItems).toBe(true);
218247
});
219248

220249
it('can retrieve only settings, without needing a platform membership ID', async () => {

api/stately/bulk-queries.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { DeleteAllResponse } from '../shapes/delete-all.js';
66
import { ExportResponse } from '../shapes/export.js';
77
import { DestinyVersion } from '../shapes/general.js';
88
import { ProfileResponse } from '../shapes/profile.js';
9-
import { defaultSettings } from '../shapes/settings.js';
9+
import { defaultSettings, Settings } from '../shapes/settings.js';
1010
import { delay, subtractObject } from '../utils.js';
1111
import { client } from './client.js';
1212
import { AnyItem } from './generated/index.js';
@@ -115,12 +115,14 @@ export async function exportDataForUser(
115115
bungieMembershipId: number,
116116
platformMembershipIds: string[],
117117
): Promise<ExportResponse> {
118-
let settings = await getSettings(bungieMembershipId);
119-
if (!settings) {
120-
const pgSettings = await readTransaction((client) =>
121-
getSettingsFromPostgres(client, bungieMembershipId),
122-
);
123-
settings = { ...defaultSettings, ...pgSettings?.settings };
118+
let settings: Settings;
119+
const pgSettings = await readTransaction((client) =>
120+
getSettingsFromPostgres(client, bungieMembershipId),
121+
);
122+
if (pgSettings) {
123+
settings = { ...defaultSettings, ...pgSettings.settings };
124+
} else {
125+
settings = (await getSettings(bungieMembershipId)) ?? defaultSettings;
124126
}
125127

126128
const responses = await Promise.all(platformMembershipIds.map((p) => exportDataForProfile(p)));

api/utils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { camelCase, mapKeys } from 'es-toolkit';
1+
import { camelCase, isEqual, mapKeys } from 'es-toolkit';
22
import { Response } from 'express';
33

44
/**
@@ -141,7 +141,7 @@ export function subtractObject<T extends object>(obj: Partial<T>, defaults: T):
141141
const result: Partial<T> = {};
142142
if (obj) {
143143
for (const key in defaults) {
144-
if (obj[key] !== undefined && obj[key] !== defaults[key]) {
144+
if (obj[key] !== undefined && !isEqual(obj[key], defaults[key])) {
145145
result[key] = obj[key];
146146
}
147147
}

0 commit comments

Comments
 (0)