diff --git a/api/apps/index.ts b/api/apps/index.ts index f11950f..633e7fb 100644 --- a/api/apps/index.ts +++ b/api/apps/index.ts @@ -1,10 +1,12 @@ import * as Sentry from '@sentry/node'; +import { ListToken } from '@stately-cloud/client'; import { RequestHandler } from 'express'; import _ from 'lodash'; -import { getAllApps } from '../db/apps-queries.js'; +import { getAllApps as getAllAppsPostgres } from '../db/apps-queries.js'; import { pool } from '../db/index.js'; import { metrics } from '../metrics/index.js'; import { ApiApp } from '../shapes/app.js'; +import { getAllApps, updateApps } from '../stately/apps-queries.js'; /** * Express middleware that requires an API key be provided in a header @@ -38,10 +40,11 @@ export const apiKey: RequestHandler = (req, res, next) => { } }; -let apps: ApiApp[]; +let apps: ApiApp[] = []; let appsByApiKey: { [apiKey: string]: ApiApp }; let origins = new Set(); let appsInterval: NodeJS.Timeout | null = null; +let token: ListToken | undefined; export function stopAppsRefresh() { if (appsInterval) { @@ -65,28 +68,40 @@ export function isAppOrigin(origin: string) { return origins.has(origin); } -export async function refreshApps() { +export async function refreshApps(): Promise { stopAppsRefresh(); try { - const client = await pool.connect(); - try { - apps = await getAllApps(client); - appsByApiKey = _.keyBy(apps, (a) => a.dimApiKey.toLowerCase()); - origins = new Set(); - for (const app of apps) { - origins.add(app.origin); + if (apps.length === 0) { + // Start off with a copy from postgres, just in case StatelyDB is having + // problems. + await fetchAppsFromPostgres(); + digestApps(); + } + + if (!token) { + // First time, get 'em all + const [appsFromStately, newToken] = await getAllApps(); + if (appsFromStately.length > 0) { + apps = appsFromStately; + digestApps(); + token = newToken; + } + } else { + // After that, use a sync to update them + const [appsFromStately, newToken] = await updateApps(token, apps); + if (appsFromStately.length > 0) { + apps = appsFromStately; + digestApps(); + token = newToken; } - metrics.increment('apps.refresh.success.count'); - return apps; - } catch (e) { - metrics.increment('apps.refresh.error.count'); - console.error('Error refreshing apps', e); - Sentry.captureException(e); - throw e; - } finally { - client.release(); } + metrics.increment('apps.refresh.success.count'); + } catch (e) { + metrics.increment('apps.refresh.error.count'); + console.error('Error refreshing apps', e); + Sentry.captureException(e); + throw e; } finally { // Refresh again every minute or so if (!appsInterval) { @@ -94,3 +109,26 @@ export async function refreshApps() { } } } + +async function fetchAppsFromPostgres() { + const client = await pool.connect(); + try { + apps = await getAllAppsPostgres(client); + appsByApiKey = _.keyBy(apps, (a) => a.dimApiKey.toLowerCase()); + origins = new Set(); + for (const app of apps) { + origins.add(app.origin); + } + return apps; + } finally { + client.release(); + } +} + +function digestApps() { + appsByApiKey = _.keyBy(apps, (a) => a.dimApiKey.toLowerCase()); + origins = new Set(); + for (const app of apps) { + origins.add(app.origin); + } +} diff --git a/api/routes/create-app.ts b/api/routes/create-app.ts index 0b14622..2a51301 100644 --- a/api/routes/create-app.ts +++ b/api/routes/create-app.ts @@ -1,9 +1,10 @@ import asyncHandler from 'express-async-handler'; import { DatabaseError } from 'pg-protocol'; import { v4 as uuid } from 'uuid'; -import { getAppById, insertApp } from '../db/apps-queries.js'; +import { insertApp as insertAppPostgres } from '../db/apps-queries.js'; import { transaction } from '../db/index.js'; import { ApiApp, CreateAppRequest } from '../shapes/app.js'; +import { insertApp } from '../stately/apps-queries.js'; import { badRequest } from '../utils.js'; const localHosts = @@ -48,14 +49,17 @@ export const createAppHandler = asyncHandler(async (req, res) => { dimApiKey: uuid(), }; + // Put it in StatelyDB + app = await insertApp(app); + + // Also put it in Postgres, for now! await transaction(async (client) => { try { - await insertApp(client, app); + await insertAppPostgres(client, app); } catch (e) { // This is a unique constraint violation, so just get the app! if (e instanceof DatabaseError && e.code === '23505') { await client.query('ROLLBACK'); - app = (await getAppById(client, request.id))!; } else { throw e; } diff --git a/api/stately/apps-queries.ts b/api/stately/apps-queries.ts new file mode 100644 index 0000000..49bc578 --- /dev/null +++ b/api/stately/apps-queries.ts @@ -0,0 +1,126 @@ +import { keyPath, ListToken } from '@stately-cloud/client'; +import { ApiApp } from '../shapes/app.js'; +import { client } from './client.js'; +import { ApiApp as StatelyApiApp } from './generated/index.js'; + +/** + * Get all registered apps. + */ +export async function getAllApps(): Promise<[ApiApp[], ListToken]> { + let apps = client.withAllowStale(true).beginList('/apps-1'); + const allApps: ApiApp[] = []; + let token: ListToken | undefined = undefined; + + while (true) { + for await (const app of apps) { + if (client.isType(app, 'ApiApp')) { + allApps.push(convertToApiApp(app)); + } + } + + token = apps.token!; + if (token.canContinue) { + apps = client.continueList(token); + } else { + break; + } + } + + return [allApps, token] as const; +} + +/** + * Update the list of apps with changes from the server. + */ +export async function updateApps(token: ListToken, apps: ApiApp[]): Promise<[ApiApp[], ListToken]> { + const updates = client.syncList(token); + for await (const update of updates) { + switch (update.type) { + case 'changed': { + const item = update.item; + if (client.isType(item, 'ApiApp')) { + const existingIndex = apps.findIndex((app) => app.id === item.id); + if (existingIndex >= 0) { + apps[existingIndex] = convertToApiApp(item); + } else { + apps.push(convertToApiApp(item)); + } + } + break; + } + case 'deleted': + case 'updatedOutsideWindow': { + const item = update.keyPath; + // TODO: This could be easier! + const id = /^\/apps\/app-([^/]+)$/.exec(item)?.[1]; + const existingIndex = apps.findIndex((app) => app.id === id); + if (existingIndex >= 0) { + apps.splice(existingIndex, 1); + } + break; + } + case 'reset': { + apps = []; + break; + } + } + } + return [apps, updates.token!] as const; +} + +/** + * Get an app by its ID. + */ +export async function getAppById(id: string): Promise { + const result = await client.get('ApiApp', keyPathFor(id)); + if (result) { + return convertToApiApp(result); + } + return undefined; +} + +/** + * Insert a new app into the list of registered apps, or update an existing app. + */ +export async function insertApp(app: ApiApp): Promise { + let resultApp: ApiApp | undefined; + // TODO: wish I could set an if-not-exists condition here, to avoid + // accidentally updating an app. Instead I got a transaction. + const result = await client.transaction(async (txn) => { + const getResult = await txn.get('ApiApp', keyPathFor(app.id)); + if (getResult) { + resultApp = convertToApiApp(getResult); + return; + } + await txn.put(client.create('ApiApp', { ...app, partition: 1n })); + }); + + if (resultApp) { + return resultApp; + } + + if (result.committed && result.puts.length === 1) { + const put = result.puts[0]; + if (client.isType(put, 'ApiApp')) { + return convertToApiApp(put); + } + } + + throw new Error('No ApiApp in result!'); +} + +function keyPathFor(id: string) { + return keyPath`/apps-1/app-${id}`; +} + +// This mostly serves to remove the partition field, which we don't need. It +// would cause problems serializing to JSON, since it's a bigint. It'd be nice +// if I could've used a 32-bit int, but that isn't in the standard schema types... +function convertToApiApp(app: StatelyApiApp): ApiApp { + return { + id: app.id, + bungieApiKey: app.bungieApiKey, + dimApiKey: app.dimApiKey, + origin: app.origin, + }; +} diff --git a/api/stately/generated/index.js b/api/stately/generated/index.js index 8eb76f2..a38210b 100644 --- a/api/stately/generated/index.js +++ b/api/stately/generated/index.js @@ -1,4 +1,4 @@ // Code generated by Stately. DO NOT EDIT. -export * from './stately_item_types.js'; -export * from './stately_pb.js'; +export * from "./stately_pb.js"; +export * from "./stately_item_types.js"; diff --git a/api/stately/generated/stately_item_types.js b/api/stately/generated/stately_item_types.js index f3bb4e0..5624435 100644 --- a/api/stately/generated/stately_item_types.js +++ b/api/stately/generated/stately_item_types.js @@ -1,55 +1,55 @@ // Code generated by Stately. DO NOT EDIT. -import { createClient as createGenericClient } from '@stately-cloud/client'; +import { createClient as createGenericClient } from "@stately-cloud/client"; import { ApiAppSchema, + GlobalSettingsSchema, + ItemAnnotationSchema, + ItemHashTagSchema, + LoadoutSchema, + LoadoutShareSchema, + SearchSchema, + SettingsSchema, + TriumphSchema, ArtifactUnlocksSchema, CollapsedSectionSchema, CustomStatDefSchema, CustomStatWeightsEntrySchema, CustomStatsEntrySchema, - GlobalSettingsSchema, InGameLoadoutIdentifiersSchema, - ItemAnnotationSchema, - ItemHashTagSchema, LoadoutItemSchema, LoadoutParametersSchema, - LoadoutSchema, - LoadoutShareSchema, ModsByBucketEntrySchema, - SearchSchema, - SettingsSchema, SocketOverrideSchema, StatConstraintSchema, StatConstraintsEntrySchema, - TriumphSchema, -} from './stately_pb.js'; +} from "./stately_pb.js"; export const typeToSchema = { // itemTypes - ApiApp: ApiAppSchema, - GlobalSettings: GlobalSettingsSchema, - ItemAnnotation: ItemAnnotationSchema, - ItemHashTag: ItemHashTagSchema, - Loadout: LoadoutSchema, - LoadoutShare: LoadoutShareSchema, - Search: SearchSchema, - Settings: SettingsSchema, - Triumph: TriumphSchema, + "ApiApp": ApiAppSchema, + "GlobalSettings": GlobalSettingsSchema, + "ItemAnnotation": ItemAnnotationSchema, + "ItemHashTag": ItemHashTagSchema, + "Loadout": LoadoutSchema, + "LoadoutShare": LoadoutShareSchema, + "Search": SearchSchema, + "Settings": SettingsSchema, + "Triumph": TriumphSchema, // objectTypes - ArtifactUnlocks: ArtifactUnlocksSchema, - CollapsedSection: CollapsedSectionSchema, - CustomStatDef: CustomStatDefSchema, - CustomStatWeightsEntry: CustomStatWeightsEntrySchema, - CustomStatsEntry: CustomStatsEntrySchema, - InGameLoadoutIdentifiers: InGameLoadoutIdentifiersSchema, - LoadoutItem: LoadoutItemSchema, - LoadoutParameters: LoadoutParametersSchema, - ModsByBucketEntry: ModsByBucketEntrySchema, - SocketOverride: SocketOverrideSchema, - StatConstraint: StatConstraintSchema, - StatConstraintsEntry: StatConstraintsEntrySchema, + "ArtifactUnlocks": ArtifactUnlocksSchema, + "CollapsedSection": CollapsedSectionSchema, + "CustomStatDef": CustomStatDefSchema, + "CustomStatWeightsEntry": CustomStatWeightsEntrySchema, + "CustomStatsEntry": CustomStatsEntrySchema, + "InGameLoadoutIdentifiers": InGameLoadoutIdentifiersSchema, + "LoadoutItem": LoadoutItemSchema, + "LoadoutParameters": LoadoutParametersSchema, + "ModsByBucketEntry": ModsByBucketEntrySchema, + "SocketOverride": SocketOverrideSchema, + "StatConstraint": StatConstraintSchema, + "StatConstraintsEntry": StatConstraintsEntrySchema, }; export function createClient(storeId, opts) { diff --git a/api/stately/generated/stately_pb.d.ts b/api/stately/generated/stately_pb.d.ts index 2beafd2..79a21e4 100644 --- a/api/stately/generated/stately_pb.d.ts +++ b/api/stately/generated/stately_pb.d.ts @@ -33,6 +33,11 @@ export declare type ApiApp = Message<"stately.generated.ApiApp"> & { * @generated from field: string origin = 4; */ origin: string; + + /** + * @generated from field: uint64 partition = 5; + */ + partition: bigint; }; /** diff --git a/api/stately/generated/stately_pb.js b/api/stately/generated/stately_pb.js index 0bc2311..e8b15a3 100644 --- a/api/stately/generated/stately_pb.js +++ b/api/stately/generated/stately_pb.js @@ -2,239 +2,278 @@ // @generated from file stately.proto (package stately.generated, syntax proto3) /* eslint-disable */ -import { enumDesc, fileDesc, messageDesc, tsEnum } from '@bufbuild/protobuf/codegenv1'; +import { enumDesc, fileDesc, messageDesc, tsEnum } from "@bufbuild/protobuf/codegenv1"; /** * Describes the file stately.proto. */ -export const file_stately = - /*@__PURE__*/ - fileDesc( - 'Cg1zdGF0ZWx5LnByb3RvEhFzdGF0ZWx5LmdlbmVyYXRlZCJNCgZBcGlBcHASCgoCaWQYASABKAkSFAoMYnVuZ2llQXBpS2V5GAIgASgJEhEKCWRpbUFwaUtleRgDIAEoCRIOCgZvcmlnaW4YBCABKAkiQwoPQXJ0aWZhY3RVbmxvY2tzEhoKEnVubG9ja2VkSXRlbUhhc2hlcxgBIAMoBBIUCgxzZWFzb25OdW1iZXIYAiABKAQiMgoQQ29sbGFwc2VkU2VjdGlvbhILCgNrZXkYASABKAkSEQoJY29sbGFwc2VkGAIgASgIIrABCg1DdXN0b21TdGF0RGVmEhAKCHN0YXRIYXNoGAEgASgEEg0KBWxhYmVsGAIgASgJEhIKCnNob3J0TGFiZWwYAyABKAkSLgoFY2xhc3MYBCABKA4yHy5zdGF0ZWx5LmdlbmVyYXRlZC5EZXN0aW55Q2xhc3MSOgoHd2VpZ2h0cxgFIAMoCzIpLnN0YXRlbHkuZ2VuZXJhdGVkLkN1c3RvbVN0YXRXZWlnaHRzRW50cnkiOgoWQ3VzdG9tU3RhdFdlaWdodHNFbnRyeRIQCghzdGF0SGFzaBgBIAEoBBIOCgZ3ZWlnaHQYAiABKAEiWwoQQ3VzdG9tU3RhdHNFbnRyeRIyCgljbGFzc1R5cGUYASABKA4yHy5zdGF0ZWx5LmdlbmVyYXRlZC5EZXN0aW55Q2xhc3MSEwoLY3VzdG9tU3RhdHMYAiADKAQimQIKDkdsb2JhbFNldHRpbmdzEg0KBXN0YWdlGAEgASgJEhUKDWRpbUFwaUVuYWJsZWQYAiABKAgSLAokZGVzdGlueVByb2ZpbGVNaW5pbXVtUmVmcmVzaEludGVydmFsGAMgASgSEiUKHWRlc3RpbnlQcm9maWxlUmVmcmVzaEludGVydmFsGAQgASgSEhMKC2F1dG9SZWZyZXNoGAUgASgIEh8KF3JlZnJlc2hQcm9maWxlT25WaXNpYmxlGAYgASgIEigKIGRpbVByb2ZpbGVNaW5pbXVtUmVmcmVzaEludGVydmFsGAcgASgSEhcKD3Nob3dJc3N1ZUJhbm5lchgIIAEoCBITCgtsYXN0VXBkYXRlZBgJIAEoEiJRChhJbkdhbWVMb2Fkb3V0SWRlbnRpZmllcnMSEQoJY29sb3JIYXNoGAEgASgEEhAKCGljb25IYXNoGAIgASgEEhAKCG5hbWVIYXNoGAMgASgEIqMBCg5JdGVtQW5ub3RhdGlvbhIMCgRoYXNoGAEgASgEEigKA3RhZxgCIAEoDjIbLnN0YXRlbHkuZ2VuZXJhdGVkLlRhZ1ZhbHVlEg0KBW5vdGVzGAMgASgJEhEKCXByb2ZpbGVJZBgGIAEoBBIWCg5kZXN0aW55VmVyc2lvbhgHIAEoBBIKCgJpZBgEIAEoBBITCgtjcmFmdGVkRGF0ZRgFIAEoEiJ/CgtJdGVtSGFzaFRhZxIMCgRoYXNoGAEgASgEEigKA3RhZxgCIAEoDjIbLnN0YXRlbHkuZ2VuZXJhdGVkLlRhZ1ZhbHVlEg0KBW5vdGVzGAMgASgJEhEKCXByb2ZpbGVJZBgGIAEoBBIWCg5kZXN0aW55VmVyc2lvbhgHIAEoBCLbAgoHTG9hZG91dBIKCgJpZBgBIAEoDBIMCgRuYW1lGAIgASgJEg0KBW5vdGVzGAMgASgJEjIKCWNsYXNzVHlwZRgEIAEoDjIfLnN0YXRlbHkuZ2VuZXJhdGVkLkRlc3RpbnlDbGFzcxIwCghlcXVpcHBlZBgFIAMoCzIeLnN0YXRlbHkuZ2VuZXJhdGVkLkxvYWRvdXRJdGVtEjIKCnVuZXF1aXBwZWQYBiADKAsyHi5zdGF0ZWx5LmdlbmVyYXRlZC5Mb2Fkb3V0SXRlbRI4CgpwYXJhbWV0ZXJzGAcgASgLMiQuc3RhdGVseS5nZW5lcmF0ZWQuTG9hZG91dFBhcmFtZXRlcnMSEQoJY3JlYXRlZEF0GAggASgSEhUKDWxhc3RVcGRhdGVkQXQYCSABKBISFgoOZGVzdGlueVZlcnNpb24YCiABKAQSEQoJcHJvZmlsZUlkGAsgASgEIogBCgtMb2Fkb3V0SXRlbRIKCgJpZBgBIAEoBBIMCgRoYXNoGAIgASgEEg4KBmFtb3VudBgDIAEoBBI6Cg9zb2NrZXRPdmVycmlkZXMYBCADKAsyIS5zdGF0ZWx5LmdlbmVyYXRlZC5Tb2NrZXRPdmVycmlkZRITCgtjcmFmdGVkRGF0ZRgFIAEoEiKGBAoRTG9hZG91dFBhcmFtZXRlcnMSOgoPc3RhdENvbnN0cmFpbnRzGAEgAygLMiEuc3RhdGVseS5nZW5lcmF0ZWQuU3RhdENvbnN0cmFpbnQSDAoEbW9kcxgCIAMoBBIRCgljbGVhck1vZHMYAyABKAgSFAoMY2xlYXJXZWFwb25zGAQgASgIEhIKCmNsZWFyQXJtb3IYBSABKAgSOgoMbW9kc0J5QnVja2V0GAYgASgLMiQuc3RhdGVseS5nZW5lcmF0ZWQuTW9kc0J5QnVja2V0RW50cnkSOwoPYXJ0aWZhY3RVbmxvY2tzGAcgASgLMiIuc3RhdGVseS5nZW5lcmF0ZWQuQXJ0aWZhY3RVbmxvY2tzEhQKDGF1dG9TdGF0TW9kcxgIIAEoCBINCgVxdWVyeRgJIAEoCRJHChVhc3N1bWVBcm1vck1hc3RlcndvcmsYCiABKA4yKC5zdGF0ZWx5LmdlbmVyYXRlZC5Bc3N1bWVBcm1vck1hc3RlcndvcmsSFwoPZXhvdGljQXJtb3JIYXNoGAsgASgEEkYKEWluR2FtZUlkZW50aWZpZXJzGAwgASgLMisuc3RhdGVseS5nZW5lcmF0ZWQuSW5HYW1lTG9hZG91dElkZW50aWZpZXJzEiIKGmluY2x1ZGVSdW50aW1lU3RhdEJlbmVmaXRzGA0gASgIIuACCgxMb2Fkb3V0U2hhcmUSCgoCaWQYASABKAkSDAoEbmFtZRgCIAEoCRINCgVub3RlcxgDIAEoCRIyCgljbGFzc1R5cGUYBCABKA4yHy5zdGF0ZWx5LmdlbmVyYXRlZC5EZXN0aW55Q2xhc3MSMAoIZXF1aXBwZWQYBSADKAsyHi5zdGF0ZWx5LmdlbmVyYXRlZC5Mb2Fkb3V0SXRlbRIyCgp1bmVxdWlwcGVkGAYgAygLMh4uc3RhdGVseS5nZW5lcmF0ZWQuTG9hZG91dEl0ZW0SOAoKcGFyYW1ldGVycxgHIAEoCzIkLnN0YXRlbHkuZ2VuZXJhdGVkLkxvYWRvdXRQYXJhbWV0ZXJzEhEKCWNyZWF0ZWRBdBgIIAEoEhIVCg1sYXN0VXBkYXRlZEF0GAkgASgSEhYKDmRlc3RpbnlWZXJzaW9uGAogASgEEhEKCXByb2ZpbGVJZBgLIAEoBCI6ChFNb2RzQnlCdWNrZXRFbnRyeRISCgpidWNrZXRIYXNoGAEgASgEEhEKCW1vZEhhc2hlcxgCIAMoBCK0AQoGU2VhcmNoEg0KBXF1ZXJ5GAEgASgJEhIKCnVzYWdlQ291bnQYAiABKAQSDQoFc2F2ZWQYAyABKAgSEQoJbGFzdFVzYWdlGAQgASgSEisKBHR5cGUYBSABKA4yHS5zdGF0ZWx5LmdlbmVyYXRlZC5TZWFyY2hUeXBlEg0KBXFoYXNoGAYgASgMEhEKCXByb2ZpbGVJZBgHIAEoBBIWCg5kZXN0aW55VmVyc2lvbhgIIAEoBCLKDAoIU2V0dGluZ3MSEAoIbWVtYmVySWQYASABKAkSEwoLaXRlbVF1YWxpdHkYAiABKAgSFAoMc2hvd05ld0l0ZW1zGAMgASgIEjkKDmNoYXJhY3Rlck9yZGVyGAQgASgOMiEuc3RhdGVseS5nZW5lcmF0ZWQuQ2hhcmFjdGVyT3JkZXISGwoTaXRlbVNvcnRPcmRlckN1c3RvbRgFIAMoCRIZChFpdGVtU29ydFJldmVyc2FscxgGIAMoCRIPCgdjaGFyQ29sGAcgASgEEhUKDWNoYXJDb2xNb2JpbGUYCCABKAQSEAoIaXRlbVNpemUYCSABKAQSPgoRY29sbGFwc2VkU2VjdGlvbnMYCiADKAsyIy5zdGF0ZWx5LmdlbmVyYXRlZC5Db2xsYXBzZWRTZWN0aW9uEh4KFmNvbXBsZXRlZFJlY29yZHNIaWRkZW4YCyABKAgSHwoXcmVkYWN0ZWRSZWNvcmRzUmV2ZWFsZWQYDCABKAgSHwoXZmFybWluZ01ha2VSb29tRm9ySXRlbXMYDSABKAgSHAoUaW52ZW50b3J5Q2xlYXJTcGFjZXMYDiABKAQSHAoUaGlkZUNvbXBsZXRlZFJlY29yZHMYDyABKAgSGwoTY3VzdG9tQ2hhcmFjdGVyU29ydBgQIAMoCRI9ChFpbmZ1c2lvbkRpcmVjdGlvbhgRIAEoDjIiLnN0YXRlbHkuZ2VuZXJhdGVkLkluZnVzZURpcmVjdGlvbhIQCghsYW5ndWFnZRgSIAEoCRIXCg93aXNoTGlzdFNvdXJjZXMYEyADKAkSOgoMbG9QYXJhbWV0ZXJzGBQgASgLMiQuc3RhdGVseS5nZW5lcmF0ZWQuTG9hZG91dFBhcmFtZXRlcnMSSQoYbG9TdGF0Q29uc3RyYWludHNCeUNsYXNzGBUgAygLMicuc3RhdGVseS5nZW5lcmF0ZWQuU3RhdENvbnN0cmFpbnRzRW50cnkSRAoXY3VzdG9tVG90YWxTdGF0c0J5Q2xhc3MYFiADKAsyIy5zdGF0ZWx5LmdlbmVyYXRlZC5DdXN0b21TdGF0c0VudHJ5Eh8KF29yZ2FuaXplckNvbHVtbnNXZWFwb25zGBcgAygJEh0KFW9yZ2FuaXplckNvbHVtbnNBcm1vchgYIAMoCRIdChVvcmdhbml6ZXJDb2x1bW5zR2hvc3QYGSADKAkSGAoQY29tcGFyZUJhc2VTdGF0cxgaIAEoCBIYChBzaWRlY2FyQ29sbGFwc2VkGBsgASgIEhcKD3NpbmdsZUNoYXJhY3RlchgcIAEoCBIXCg9iYWRnZVBvc3RtYXN0ZXIYHSABKAgSEAoIcGVya0xpc3QYHiABKAgSMwoLbG9hZG91dFNvcnQYHyABKA4yHi5zdGF0ZWx5LmdlbmVyYXRlZC5Mb2Fkb3V0U29ydBIaChJpdGVtRmVlZEhpZGVUYWdnZWQYICABKAgSGAoQaXRlbUZlZWRFeHBhbmRlZBghIAEoCBIeChZoaWRlUHVsbEZyb21Qb3N0bWFzdGVyGCIgASgIEkQKFWRlc2NyaXB0aW9uc1RvRGlzcGxheRgjIAEoDjIlLnN0YXRlbHkuZ2VuZXJhdGVkLkRlc2NyaXB0aW9uT3B0aW9ucxIfChdjb21wYXJlV2VhcG9uTWFzdGVyd29yaxgkIAEoCBIZChFpdGVtRmVlZFdhdGVybWFyaxglIAEoBBI1CgtjdXN0b21TdGF0cxgmIAMoCzIgLnN0YXRlbHkuZ2VuZXJhdGVkLkN1c3RvbVN0YXREZWYSFgoOYXV0b0xvY2tUYWdnZWQYJyABKAgSDQoFdGhlbWUYKCABKAkSHQoVc29ydFJlY29yZFByb2dyZXNzaW9uGCkgASgIEh4KFnZlbmRvcnNIaWRlU2lsdmVySXRlbXMYKiABKAgSGwoTdmF1bHRXZWFwb25Hcm91cGluZxgrIAEoCRJNChh2YXVsdFdlYXBvbkdyb3VwaW5nU3R5bGUYLCABKA4yKy5zdGF0ZWx5LmdlbmVyYXRlZC5WYXVsdFdlYXBvbkdyb3VwaW5nU3R5bGUSNQoMaXRlbVBvcHVwVGFiGC0gASgOMh8uc3RhdGVseS5nZW5lcmF0ZWQuSXRlbVBvcHVwVGFiIjcKDlNvY2tldE92ZXJyaWRlEhMKC3NvY2tldEluZGV4GAEgASgEEhAKCGl0ZW1IYXNoGAIgASgEIkQKDlN0YXRDb25zdHJhaW50EhAKCHN0YXRIYXNoGAEgASgEEg8KB21pblRpZXIYAiABKAQSDwoHbWF4VGllchgDIAEoBCKCAQoUU3RhdENvbnN0cmFpbnRzRW50cnkSMgoJY2xhc3NUeXBlGAEgASgOMh8uc3RhdGVseS5nZW5lcmF0ZWQuRGVzdGlueUNsYXNzEjYKC2NvbnN0cmFpbnRzGAIgAygLMiEuc3RhdGVseS5nZW5lcmF0ZWQuU3RhdENvbnN0cmFpbnQiSAoHVHJpdW1waBISCgpyZWNvcmRIYXNoGAEgASgEEhEKCXByb2ZpbGVJZBgCIAEoBBIWCg5kZXN0aW55VmVyc2lvbhgHIAEoBCp7ChVBc3N1bWVBcm1vck1hc3RlcndvcmsSHgoaQXNzdW1lQXJtb3JNYXN0ZXJ3b3JrX05vbmUQABIjCh9Bc3N1bWVBcm1vck1hc3RlcndvcmtfTGVnZW5kYXJ5EAESHQoZQXNzdW1lQXJtb3JNYXN0ZXJ3b3JrX0FsbBACKooBCg5DaGFyYWN0ZXJPcmRlchIdChlDaGFyYWN0ZXJPcmRlcl9tb3N0UmVjZW50EAASJAogQ2hhcmFjdGVyT3JkZXJfbW9zdFJlY2VudFJldmVyc2UQARIYChRDaGFyYWN0ZXJPcmRlcl9maXhlZBACEhkKFUNoYXJhY3Rlck9yZGVyX2N1c3RvbRADKpYBChJEZXNjcmlwdGlvbk9wdGlvbnMSIgoeRGVzY3JpcHRpb25PcHRpb25zX1VOU1BFQ0lGSUVEEAASHQoZRGVzY3JpcHRpb25PcHRpb25zX2J1bmdpZRABEiAKHERlc2NyaXB0aW9uT3B0aW9uc19jb21tdW5pdHkQAhIbChdEZXNjcmlwdGlvbk9wdGlvbnNfYm90aBADKnMKDERlc3RpbnlDbGFzcxIWChJEZXN0aW55Q2xhc3NfVGl0YW4QABIXChNEZXN0aW55Q2xhc3NfSHVudGVyEAESGAoURGVzdGlueUNsYXNzX1dhcmxvY2sQAhIYChREZXN0aW55Q2xhc3NfVW5rbm93bhADKmgKD0luZnVzZURpcmVjdGlvbhIfChtJbmZ1c2VEaXJlY3Rpb25fVU5TUEVDSUZJRUQQABIaChZJbmZ1c2VEaXJlY3Rpb25fSW5mdXNlEAESGAoUSW5mdXNlRGlyZWN0aW9uX0Z1ZWwQAipCCgxJdGVtUG9wdXBUYWISGQoVSXRlbVBvcHVwVGFiX092ZXJ2aWV3EAASFwoTSXRlbVBvcHVwVGFiX1RyaWFnZRABKkEKC0xvYWRvdXRTb3J0EhoKFkxvYWRvdXRTb3J0X0J5RWRpdFRpbWUQABIWChJMb2Fkb3V0U29ydF9CeU5hbWUQASo5CgpTZWFyY2hUeXBlEhMKD1NlYXJjaFR5cGVfSXRlbRAAEhYKElNlYXJjaFR5cGVfTG9hZG91dBABKowBCghUYWdWYWx1ZRIYChRUYWdWYWx1ZV9VTlNQRUNJRklFRBAAEhUKEVRhZ1ZhbHVlX2Zhdm9yaXRlEAESEQoNVGFnVmFsdWVfa2VlcBACEhMKD1RhZ1ZhbHVlX2luZnVzZRADEhEKDVRhZ1ZhbHVlX2p1bmsQBBIUChBUYWdWYWx1ZV9hcmNoaXZlEAUqYwoYVmF1bHRXZWFwb25Hcm91cGluZ1N0eWxlEiIKHlZhdWx0V2VhcG9uR3JvdXBpbmdTdHlsZV9MaW5lcxAAEiMKH1ZhdWx0V2VhcG9uR3JvdXBpbmdTdHlsZV9JbmxpbmUQAWIGcHJvdG8z', - ); +export const file_stately = /*@__PURE__*/ + fileDesc("Cg1zdGF0ZWx5LnByb3RvEhFzdGF0ZWx5LmdlbmVyYXRlZCJgCgZBcGlBcHASCgoCaWQYASABKAkSFAoMYnVuZ2llQXBpS2V5GAIgASgJEhEKCWRpbUFwaUtleRgDIAEoCRIOCgZvcmlnaW4YBCABKAkSEQoJcGFydGl0aW9uGAUgASgEIkMKD0FydGlmYWN0VW5sb2NrcxIaChJ1bmxvY2tlZEl0ZW1IYXNoZXMYASADKAQSFAoMc2Vhc29uTnVtYmVyGAIgASgEIjIKEENvbGxhcHNlZFNlY3Rpb24SCwoDa2V5GAEgASgJEhEKCWNvbGxhcHNlZBgCIAEoCCKwAQoNQ3VzdG9tU3RhdERlZhIQCghzdGF0SGFzaBgBIAEoBBINCgVsYWJlbBgCIAEoCRISCgpzaG9ydExhYmVsGAMgASgJEi4KBWNsYXNzGAQgASgOMh8uc3RhdGVseS5nZW5lcmF0ZWQuRGVzdGlueUNsYXNzEjoKB3dlaWdodHMYBSADKAsyKS5zdGF0ZWx5LmdlbmVyYXRlZC5DdXN0b21TdGF0V2VpZ2h0c0VudHJ5IjoKFkN1c3RvbVN0YXRXZWlnaHRzRW50cnkSEAoIc3RhdEhhc2gYASABKAQSDgoGd2VpZ2h0GAIgASgBIlsKEEN1c3RvbVN0YXRzRW50cnkSMgoJY2xhc3NUeXBlGAEgASgOMh8uc3RhdGVseS5nZW5lcmF0ZWQuRGVzdGlueUNsYXNzEhMKC2N1c3RvbVN0YXRzGAIgAygEIpkCCg5HbG9iYWxTZXR0aW5ncxINCgVzdGFnZRgBIAEoCRIVCg1kaW1BcGlFbmFibGVkGAIgASgIEiwKJGRlc3RpbnlQcm9maWxlTWluaW11bVJlZnJlc2hJbnRlcnZhbBgDIAEoEhIlCh1kZXN0aW55UHJvZmlsZVJlZnJlc2hJbnRlcnZhbBgEIAEoEhITCgthdXRvUmVmcmVzaBgFIAEoCBIfChdyZWZyZXNoUHJvZmlsZU9uVmlzaWJsZRgGIAEoCBIoCiBkaW1Qcm9maWxlTWluaW11bVJlZnJlc2hJbnRlcnZhbBgHIAEoEhIXCg9zaG93SXNzdWVCYW5uZXIYCCABKAgSEwoLbGFzdFVwZGF0ZWQYCSABKBIiUQoYSW5HYW1lTG9hZG91dElkZW50aWZpZXJzEhEKCWNvbG9ySGFzaBgBIAEoBBIQCghpY29uSGFzaBgCIAEoBBIQCghuYW1lSGFzaBgDIAEoBCKjAQoOSXRlbUFubm90YXRpb24SDAoEaGFzaBgBIAEoBBIoCgN0YWcYAiABKA4yGy5zdGF0ZWx5LmdlbmVyYXRlZC5UYWdWYWx1ZRINCgVub3RlcxgDIAEoCRIRCglwcm9maWxlSWQYBiABKAQSFgoOZGVzdGlueVZlcnNpb24YByABKAQSCgoCaWQYBCABKAQSEwoLY3JhZnRlZERhdGUYBSABKBIifwoLSXRlbUhhc2hUYWcSDAoEaGFzaBgBIAEoBBIoCgN0YWcYAiABKA4yGy5zdGF0ZWx5LmdlbmVyYXRlZC5UYWdWYWx1ZRINCgVub3RlcxgDIAEoCRIRCglwcm9maWxlSWQYBiABKAQSFgoOZGVzdGlueVZlcnNpb24YByABKAQi2wIKB0xvYWRvdXQSCgoCaWQYASABKAwSDAoEbmFtZRgCIAEoCRINCgVub3RlcxgDIAEoCRIyCgljbGFzc1R5cGUYBCABKA4yHy5zdGF0ZWx5LmdlbmVyYXRlZC5EZXN0aW55Q2xhc3MSMAoIZXF1aXBwZWQYBSADKAsyHi5zdGF0ZWx5LmdlbmVyYXRlZC5Mb2Fkb3V0SXRlbRIyCgp1bmVxdWlwcGVkGAYgAygLMh4uc3RhdGVseS5nZW5lcmF0ZWQuTG9hZG91dEl0ZW0SOAoKcGFyYW1ldGVycxgHIAEoCzIkLnN0YXRlbHkuZ2VuZXJhdGVkLkxvYWRvdXRQYXJhbWV0ZXJzEhEKCWNyZWF0ZWRBdBgIIAEoEhIVCg1sYXN0VXBkYXRlZEF0GAkgASgSEhYKDmRlc3RpbnlWZXJzaW9uGAogASgEEhEKCXByb2ZpbGVJZBgLIAEoBCKIAQoLTG9hZG91dEl0ZW0SCgoCaWQYASABKAQSDAoEaGFzaBgCIAEoBBIOCgZhbW91bnQYAyABKAQSOgoPc29ja2V0T3ZlcnJpZGVzGAQgAygLMiEuc3RhdGVseS5nZW5lcmF0ZWQuU29ja2V0T3ZlcnJpZGUSEwoLY3JhZnRlZERhdGUYBSABKBIihgQKEUxvYWRvdXRQYXJhbWV0ZXJzEjoKD3N0YXRDb25zdHJhaW50cxgBIAMoCzIhLnN0YXRlbHkuZ2VuZXJhdGVkLlN0YXRDb25zdHJhaW50EgwKBG1vZHMYAiADKAQSEQoJY2xlYXJNb2RzGAMgASgIEhQKDGNsZWFyV2VhcG9ucxgEIAEoCBISCgpjbGVhckFybW9yGAUgASgIEjoKDG1vZHNCeUJ1Y2tldBgGIAEoCzIkLnN0YXRlbHkuZ2VuZXJhdGVkLk1vZHNCeUJ1Y2tldEVudHJ5EjsKD2FydGlmYWN0VW5sb2NrcxgHIAEoCzIiLnN0YXRlbHkuZ2VuZXJhdGVkLkFydGlmYWN0VW5sb2NrcxIUCgxhdXRvU3RhdE1vZHMYCCABKAgSDQoFcXVlcnkYCSABKAkSRwoVYXNzdW1lQXJtb3JNYXN0ZXJ3b3JrGAogASgOMiguc3RhdGVseS5nZW5lcmF0ZWQuQXNzdW1lQXJtb3JNYXN0ZXJ3b3JrEhcKD2V4b3RpY0FybW9ySGFzaBgLIAEoBBJGChFpbkdhbWVJZGVudGlmaWVycxgMIAEoCzIrLnN0YXRlbHkuZ2VuZXJhdGVkLkluR2FtZUxvYWRvdXRJZGVudGlmaWVycxIiChppbmNsdWRlUnVudGltZVN0YXRCZW5lZml0cxgNIAEoCCLgAgoMTG9hZG91dFNoYXJlEgoKAmlkGAEgASgJEgwKBG5hbWUYAiABKAkSDQoFbm90ZXMYAyABKAkSMgoJY2xhc3NUeXBlGAQgASgOMh8uc3RhdGVseS5nZW5lcmF0ZWQuRGVzdGlueUNsYXNzEjAKCGVxdWlwcGVkGAUgAygLMh4uc3RhdGVseS5nZW5lcmF0ZWQuTG9hZG91dEl0ZW0SMgoKdW5lcXVpcHBlZBgGIAMoCzIeLnN0YXRlbHkuZ2VuZXJhdGVkLkxvYWRvdXRJdGVtEjgKCnBhcmFtZXRlcnMYByABKAsyJC5zdGF0ZWx5LmdlbmVyYXRlZC5Mb2Fkb3V0UGFyYW1ldGVycxIRCgljcmVhdGVkQXQYCCABKBISFQoNbGFzdFVwZGF0ZWRBdBgJIAEoEhIWCg5kZXN0aW55VmVyc2lvbhgKIAEoBBIRCglwcm9maWxlSWQYCyABKAQiOgoRTW9kc0J5QnVja2V0RW50cnkSEgoKYnVja2V0SGFzaBgBIAEoBBIRCgltb2RIYXNoZXMYAiADKAQitAEKBlNlYXJjaBINCgVxdWVyeRgBIAEoCRISCgp1c2FnZUNvdW50GAIgASgEEg0KBXNhdmVkGAMgASgIEhEKCWxhc3RVc2FnZRgEIAEoEhIrCgR0eXBlGAUgASgOMh0uc3RhdGVseS5nZW5lcmF0ZWQuU2VhcmNoVHlwZRINCgVxaGFzaBgGIAEoDBIRCglwcm9maWxlSWQYByABKAQSFgoOZGVzdGlueVZlcnNpb24YCCABKAQiygwKCFNldHRpbmdzEhAKCG1lbWJlcklkGAEgASgJEhMKC2l0ZW1RdWFsaXR5GAIgASgIEhQKDHNob3dOZXdJdGVtcxgDIAEoCBI5Cg5jaGFyYWN0ZXJPcmRlchgEIAEoDjIhLnN0YXRlbHkuZ2VuZXJhdGVkLkNoYXJhY3Rlck9yZGVyEhsKE2l0ZW1Tb3J0T3JkZXJDdXN0b20YBSADKAkSGQoRaXRlbVNvcnRSZXZlcnNhbHMYBiADKAkSDwoHY2hhckNvbBgHIAEoBBIVCg1jaGFyQ29sTW9iaWxlGAggASgEEhAKCGl0ZW1TaXplGAkgASgEEj4KEWNvbGxhcHNlZFNlY3Rpb25zGAogAygLMiMuc3RhdGVseS5nZW5lcmF0ZWQuQ29sbGFwc2VkU2VjdGlvbhIeChZjb21wbGV0ZWRSZWNvcmRzSGlkZGVuGAsgASgIEh8KF3JlZGFjdGVkUmVjb3Jkc1JldmVhbGVkGAwgASgIEh8KF2Zhcm1pbmdNYWtlUm9vbUZvckl0ZW1zGA0gASgIEhwKFGludmVudG9yeUNsZWFyU3BhY2VzGA4gASgEEhwKFGhpZGVDb21wbGV0ZWRSZWNvcmRzGA8gASgIEhsKE2N1c3RvbUNoYXJhY3RlclNvcnQYECADKAkSPQoRaW5mdXNpb25EaXJlY3Rpb24YESABKA4yIi5zdGF0ZWx5LmdlbmVyYXRlZC5JbmZ1c2VEaXJlY3Rpb24SEAoIbGFuZ3VhZ2UYEiABKAkSFwoPd2lzaExpc3RTb3VyY2VzGBMgAygJEjoKDGxvUGFyYW1ldGVycxgUIAEoCzIkLnN0YXRlbHkuZ2VuZXJhdGVkLkxvYWRvdXRQYXJhbWV0ZXJzEkkKGGxvU3RhdENvbnN0cmFpbnRzQnlDbGFzcxgVIAMoCzInLnN0YXRlbHkuZ2VuZXJhdGVkLlN0YXRDb25zdHJhaW50c0VudHJ5EkQKF2N1c3RvbVRvdGFsU3RhdHNCeUNsYXNzGBYgAygLMiMuc3RhdGVseS5nZW5lcmF0ZWQuQ3VzdG9tU3RhdHNFbnRyeRIfChdvcmdhbml6ZXJDb2x1bW5zV2VhcG9ucxgXIAMoCRIdChVvcmdhbml6ZXJDb2x1bW5zQXJtb3IYGCADKAkSHQoVb3JnYW5pemVyQ29sdW1uc0dob3N0GBkgAygJEhgKEGNvbXBhcmVCYXNlU3RhdHMYGiABKAgSGAoQc2lkZWNhckNvbGxhcHNlZBgbIAEoCBIXCg9zaW5nbGVDaGFyYWN0ZXIYHCABKAgSFwoPYmFkZ2VQb3N0bWFzdGVyGB0gASgIEhAKCHBlcmtMaXN0GB4gASgIEjMKC2xvYWRvdXRTb3J0GB8gASgOMh4uc3RhdGVseS5nZW5lcmF0ZWQuTG9hZG91dFNvcnQSGgoSaXRlbUZlZWRIaWRlVGFnZ2VkGCAgASgIEhgKEGl0ZW1GZWVkRXhwYW5kZWQYISABKAgSHgoWaGlkZVB1bGxGcm9tUG9zdG1hc3RlchgiIAEoCBJEChVkZXNjcmlwdGlvbnNUb0Rpc3BsYXkYIyABKA4yJS5zdGF0ZWx5LmdlbmVyYXRlZC5EZXNjcmlwdGlvbk9wdGlvbnMSHwoXY29tcGFyZVdlYXBvbk1hc3RlcndvcmsYJCABKAgSGQoRaXRlbUZlZWRXYXRlcm1hcmsYJSABKAQSNQoLY3VzdG9tU3RhdHMYJiADKAsyIC5zdGF0ZWx5LmdlbmVyYXRlZC5DdXN0b21TdGF0RGVmEhYKDmF1dG9Mb2NrVGFnZ2VkGCcgASgIEg0KBXRoZW1lGCggASgJEh0KFXNvcnRSZWNvcmRQcm9ncmVzc2lvbhgpIAEoCBIeChZ2ZW5kb3JzSGlkZVNpbHZlckl0ZW1zGCogASgIEhsKE3ZhdWx0V2VhcG9uR3JvdXBpbmcYKyABKAkSTQoYdmF1bHRXZWFwb25Hcm91cGluZ1N0eWxlGCwgASgOMisuc3RhdGVseS5nZW5lcmF0ZWQuVmF1bHRXZWFwb25Hcm91cGluZ1N0eWxlEjUKDGl0ZW1Qb3B1cFRhYhgtIAEoDjIfLnN0YXRlbHkuZ2VuZXJhdGVkLkl0ZW1Qb3B1cFRhYiI3Cg5Tb2NrZXRPdmVycmlkZRITCgtzb2NrZXRJbmRleBgBIAEoBBIQCghpdGVtSGFzaBgCIAEoBCJECg5TdGF0Q29uc3RyYWludBIQCghzdGF0SGFzaBgBIAEoBBIPCgdtaW5UaWVyGAIgASgEEg8KB21heFRpZXIYAyABKAQiggEKFFN0YXRDb25zdHJhaW50c0VudHJ5EjIKCWNsYXNzVHlwZRgBIAEoDjIfLnN0YXRlbHkuZ2VuZXJhdGVkLkRlc3RpbnlDbGFzcxI2Cgtjb25zdHJhaW50cxgCIAMoCzIhLnN0YXRlbHkuZ2VuZXJhdGVkLlN0YXRDb25zdHJhaW50IkgKB1RyaXVtcGgSEgoKcmVjb3JkSGFzaBgBIAEoBBIRCglwcm9maWxlSWQYAiABKAQSFgoOZGVzdGlueVZlcnNpb24YByABKAQqewoVQXNzdW1lQXJtb3JNYXN0ZXJ3b3JrEh4KGkFzc3VtZUFybW9yTWFzdGVyd29ya19Ob25lEAASIwofQXNzdW1lQXJtb3JNYXN0ZXJ3b3JrX0xlZ2VuZGFyeRABEh0KGUFzc3VtZUFybW9yTWFzdGVyd29ya19BbGwQAiqKAQoOQ2hhcmFjdGVyT3JkZXISHQoZQ2hhcmFjdGVyT3JkZXJfbW9zdFJlY2VudBAAEiQKIENoYXJhY3Rlck9yZGVyX21vc3RSZWNlbnRSZXZlcnNlEAESGAoUQ2hhcmFjdGVyT3JkZXJfZml4ZWQQAhIZChVDaGFyYWN0ZXJPcmRlcl9jdXN0b20QAyqWAQoSRGVzY3JpcHRpb25PcHRpb25zEiIKHkRlc2NyaXB0aW9uT3B0aW9uc19VTlNQRUNJRklFRBAAEh0KGURlc2NyaXB0aW9uT3B0aW9uc19idW5naWUQARIgChxEZXNjcmlwdGlvbk9wdGlvbnNfY29tbXVuaXR5EAISGwoXRGVzY3JpcHRpb25PcHRpb25zX2JvdGgQAypzCgxEZXN0aW55Q2xhc3MSFgoSRGVzdGlueUNsYXNzX1RpdGFuEAASFwoTRGVzdGlueUNsYXNzX0h1bnRlchABEhgKFERlc3RpbnlDbGFzc19XYXJsb2NrEAISGAoURGVzdGlueUNsYXNzX1Vua25vd24QAypoCg9JbmZ1c2VEaXJlY3Rpb24SHwobSW5mdXNlRGlyZWN0aW9uX1VOU1BFQ0lGSUVEEAASGgoWSW5mdXNlRGlyZWN0aW9uX0luZnVzZRABEhgKFEluZnVzZURpcmVjdGlvbl9GdWVsEAIqQgoMSXRlbVBvcHVwVGFiEhkKFUl0ZW1Qb3B1cFRhYl9PdmVydmlldxAAEhcKE0l0ZW1Qb3B1cFRhYl9UcmlhZ2UQASpBCgtMb2Fkb3V0U29ydBIaChZMb2Fkb3V0U29ydF9CeUVkaXRUaW1lEAASFgoSTG9hZG91dFNvcnRfQnlOYW1lEAEqOQoKU2VhcmNoVHlwZRITCg9TZWFyY2hUeXBlX0l0ZW0QABIWChJTZWFyY2hUeXBlX0xvYWRvdXQQASqMAQoIVGFnVmFsdWUSGAoUVGFnVmFsdWVfVU5TUEVDSUZJRUQQABIVChFUYWdWYWx1ZV9mYXZvcml0ZRABEhEKDVRhZ1ZhbHVlX2tlZXAQAhITCg9UYWdWYWx1ZV9pbmZ1c2UQAxIRCg1UYWdWYWx1ZV9qdW5rEAQSFAoQVGFnVmFsdWVfYXJjaGl2ZRAFKmMKGFZhdWx0V2VhcG9uR3JvdXBpbmdTdHlsZRIiCh5WYXVsdFdlYXBvbkdyb3VwaW5nU3R5bGVfTGluZXMQABIjCh9WYXVsdFdlYXBvbkdyb3VwaW5nU3R5bGVfSW5saW5lEAFiBnByb3RvMw"); /** * Describes the message stately.generated.ApiApp. * Use `create(ApiAppSchema)` to create a new message. */ -export const ApiAppSchema = /*@__PURE__*/ messageDesc(file_stately, 0); +export const ApiAppSchema = /*@__PURE__*/ + messageDesc(file_stately, 0); /** * Describes the message stately.generated.ArtifactUnlocks. * Use `create(ArtifactUnlocksSchema)` to create a new message. */ -export const ArtifactUnlocksSchema = /*@__PURE__*/ messageDesc(file_stately, 1); +export const ArtifactUnlocksSchema = /*@__PURE__*/ + messageDesc(file_stately, 1); /** * Describes the message stately.generated.CollapsedSection. * Use `create(CollapsedSectionSchema)` to create a new message. */ -export const CollapsedSectionSchema = /*@__PURE__*/ messageDesc(file_stately, 2); +export const CollapsedSectionSchema = /*@__PURE__*/ + messageDesc(file_stately, 2); /** * Describes the message stately.generated.CustomStatDef. * Use `create(CustomStatDefSchema)` to create a new message. */ -export const CustomStatDefSchema = /*@__PURE__*/ messageDesc(file_stately, 3); +export const CustomStatDefSchema = /*@__PURE__*/ + messageDesc(file_stately, 3); /** * Describes the message stately.generated.CustomStatWeightsEntry. * Use `create(CustomStatWeightsEntrySchema)` to create a new message. */ -export const CustomStatWeightsEntrySchema = /*@__PURE__*/ messageDesc(file_stately, 4); +export const CustomStatWeightsEntrySchema = /*@__PURE__*/ + messageDesc(file_stately, 4); /** * Describes the message stately.generated.CustomStatsEntry. * Use `create(CustomStatsEntrySchema)` to create a new message. */ -export const CustomStatsEntrySchema = /*@__PURE__*/ messageDesc(file_stately, 5); +export const CustomStatsEntrySchema = /*@__PURE__*/ + messageDesc(file_stately, 5); /** * Describes the message stately.generated.GlobalSettings. * Use `create(GlobalSettingsSchema)` to create a new message. */ -export const GlobalSettingsSchema = /*@__PURE__*/ messageDesc(file_stately, 6); +export const GlobalSettingsSchema = /*@__PURE__*/ + messageDesc(file_stately, 6); /** * Describes the message stately.generated.InGameLoadoutIdentifiers. * Use `create(InGameLoadoutIdentifiersSchema)` to create a new message. */ -export const InGameLoadoutIdentifiersSchema = /*@__PURE__*/ messageDesc(file_stately, 7); +export const InGameLoadoutIdentifiersSchema = /*@__PURE__*/ + messageDesc(file_stately, 7); /** * Describes the message stately.generated.ItemAnnotation. * Use `create(ItemAnnotationSchema)` to create a new message. */ -export const ItemAnnotationSchema = /*@__PURE__*/ messageDesc(file_stately, 8); +export const ItemAnnotationSchema = /*@__PURE__*/ + messageDesc(file_stately, 8); /** * Describes the message stately.generated.ItemHashTag. * Use `create(ItemHashTagSchema)` to create a new message. */ -export const ItemHashTagSchema = /*@__PURE__*/ messageDesc(file_stately, 9); +export const ItemHashTagSchema = /*@__PURE__*/ + messageDesc(file_stately, 9); /** * Describes the message stately.generated.Loadout. * Use `create(LoadoutSchema)` to create a new message. */ -export const LoadoutSchema = /*@__PURE__*/ messageDesc(file_stately, 10); +export const LoadoutSchema = /*@__PURE__*/ + messageDesc(file_stately, 10); /** * Describes the message stately.generated.LoadoutItem. * Use `create(LoadoutItemSchema)` to create a new message. */ -export const LoadoutItemSchema = /*@__PURE__*/ messageDesc(file_stately, 11); +export const LoadoutItemSchema = /*@__PURE__*/ + messageDesc(file_stately, 11); /** * Describes the message stately.generated.LoadoutParameters. * Use `create(LoadoutParametersSchema)` to create a new message. */ -export const LoadoutParametersSchema = /*@__PURE__*/ messageDesc(file_stately, 12); +export const LoadoutParametersSchema = /*@__PURE__*/ + messageDesc(file_stately, 12); /** * Describes the message stately.generated.LoadoutShare. * Use `create(LoadoutShareSchema)` to create a new message. */ -export const LoadoutShareSchema = /*@__PURE__*/ messageDesc(file_stately, 13); +export const LoadoutShareSchema = /*@__PURE__*/ + messageDesc(file_stately, 13); /** * Describes the message stately.generated.ModsByBucketEntry. * Use `create(ModsByBucketEntrySchema)` to create a new message. */ -export const ModsByBucketEntrySchema = /*@__PURE__*/ messageDesc(file_stately, 14); +export const ModsByBucketEntrySchema = /*@__PURE__*/ + messageDesc(file_stately, 14); /** * Describes the message stately.generated.Search. * Use `create(SearchSchema)` to create a new message. */ -export const SearchSchema = /*@__PURE__*/ messageDesc(file_stately, 15); +export const SearchSchema = /*@__PURE__*/ + messageDesc(file_stately, 15); /** * Describes the message stately.generated.Settings. * Use `create(SettingsSchema)` to create a new message. */ -export const SettingsSchema = /*@__PURE__*/ messageDesc(file_stately, 16); +export const SettingsSchema = /*@__PURE__*/ + messageDesc(file_stately, 16); /** * Describes the message stately.generated.SocketOverride. * Use `create(SocketOverrideSchema)` to create a new message. */ -export const SocketOverrideSchema = /*@__PURE__*/ messageDesc(file_stately, 17); +export const SocketOverrideSchema = /*@__PURE__*/ + messageDesc(file_stately, 17); /** * Describes the message stately.generated.StatConstraint. * Use `create(StatConstraintSchema)` to create a new message. */ -export const StatConstraintSchema = /*@__PURE__*/ messageDesc(file_stately, 18); +export const StatConstraintSchema = /*@__PURE__*/ + messageDesc(file_stately, 18); /** * Describes the message stately.generated.StatConstraintsEntry. * Use `create(StatConstraintsEntrySchema)` to create a new message. */ -export const StatConstraintsEntrySchema = /*@__PURE__*/ messageDesc(file_stately, 19); +export const StatConstraintsEntrySchema = /*@__PURE__*/ + messageDesc(file_stately, 19); /** * Describes the message stately.generated.Triumph. * Use `create(TriumphSchema)` to create a new message. */ -export const TriumphSchema = /*@__PURE__*/ messageDesc(file_stately, 20); +export const TriumphSchema = /*@__PURE__*/ + messageDesc(file_stately, 20); /** * Describes the enum stately.generated.AssumeArmorMasterwork. */ -export const AssumeArmorMasterworkSchema = /*@__PURE__*/ enumDesc(file_stately, 0); +export const AssumeArmorMasterworkSchema = /*@__PURE__*/ + enumDesc(file_stately, 0); /** * @generated from enum stately.generated.AssumeArmorMasterwork */ -export const AssumeArmorMasterwork = /*@__PURE__*/ tsEnum(AssumeArmorMasterworkSchema); +export const AssumeArmorMasterwork = /*@__PURE__*/ + tsEnum(AssumeArmorMasterworkSchema); /** * Describes the enum stately.generated.CharacterOrder. */ -export const CharacterOrderSchema = /*@__PURE__*/ enumDesc(file_stately, 1); +export const CharacterOrderSchema = /*@__PURE__*/ + enumDesc(file_stately, 1); /** * @generated from enum stately.generated.CharacterOrder */ -export const CharacterOrder = /*@__PURE__*/ tsEnum(CharacterOrderSchema); +export const CharacterOrder = /*@__PURE__*/ + tsEnum(CharacterOrderSchema); /** * Describes the enum stately.generated.DescriptionOptions. */ -export const DescriptionOptionsSchema = /*@__PURE__*/ enumDesc(file_stately, 2); +export const DescriptionOptionsSchema = /*@__PURE__*/ + enumDesc(file_stately, 2); /** * @generated from enum stately.generated.DescriptionOptions */ -export const DescriptionOptions = /*@__PURE__*/ tsEnum(DescriptionOptionsSchema); +export const DescriptionOptions = /*@__PURE__*/ + tsEnum(DescriptionOptionsSchema); /** * Describes the enum stately.generated.DestinyClass. */ -export const DestinyClassSchema = /*@__PURE__*/ enumDesc(file_stately, 3); +export const DestinyClassSchema = /*@__PURE__*/ + enumDesc(file_stately, 3); /** * @generated from enum stately.generated.DestinyClass */ -export const DestinyClass = /*@__PURE__*/ tsEnum(DestinyClassSchema); +export const DestinyClass = /*@__PURE__*/ + tsEnum(DestinyClassSchema); /** * Describes the enum stately.generated.InfuseDirection. */ -export const InfuseDirectionSchema = /*@__PURE__*/ enumDesc(file_stately, 4); +export const InfuseDirectionSchema = /*@__PURE__*/ + enumDesc(file_stately, 4); /** * @generated from enum stately.generated.InfuseDirection */ -export const InfuseDirection = /*@__PURE__*/ tsEnum(InfuseDirectionSchema); +export const InfuseDirection = /*@__PURE__*/ + tsEnum(InfuseDirectionSchema); /** * Describes the enum stately.generated.ItemPopupTab. */ -export const ItemPopupTabSchema = /*@__PURE__*/ enumDesc(file_stately, 5); +export const ItemPopupTabSchema = /*@__PURE__*/ + enumDesc(file_stately, 5); /** * @generated from enum stately.generated.ItemPopupTab */ -export const ItemPopupTab = /*@__PURE__*/ tsEnum(ItemPopupTabSchema); +export const ItemPopupTab = /*@__PURE__*/ + tsEnum(ItemPopupTabSchema); /** * Describes the enum stately.generated.LoadoutSort. */ -export const LoadoutSortSchema = /*@__PURE__*/ enumDesc(file_stately, 6); +export const LoadoutSortSchema = /*@__PURE__*/ + enumDesc(file_stately, 6); /** * @generated from enum stately.generated.LoadoutSort */ -export const LoadoutSort = /*@__PURE__*/ tsEnum(LoadoutSortSchema); +export const LoadoutSort = /*@__PURE__*/ + tsEnum(LoadoutSortSchema); /** * Describes the enum stately.generated.SearchType. */ -export const SearchTypeSchema = /*@__PURE__*/ enumDesc(file_stately, 7); +export const SearchTypeSchema = /*@__PURE__*/ + enumDesc(file_stately, 7); /** * @generated from enum stately.generated.SearchType */ -export const SearchType = /*@__PURE__*/ tsEnum(SearchTypeSchema); +export const SearchType = /*@__PURE__*/ + tsEnum(SearchTypeSchema); /** * Describes the enum stately.generated.TagValue. */ -export const TagValueSchema = /*@__PURE__*/ enumDesc(file_stately, 8); +export const TagValueSchema = /*@__PURE__*/ + enumDesc(file_stately, 8); /** * @generated from enum stately.generated.TagValue */ -export const TagValue = /*@__PURE__*/ tsEnum(TagValueSchema); +export const TagValue = /*@__PURE__*/ + tsEnum(TagValueSchema); /** * Describes the enum stately.generated.VaultWeaponGroupingStyle. */ -export const VaultWeaponGroupingStyleSchema = /*@__PURE__*/ enumDesc(file_stately, 9); +export const VaultWeaponGroupingStyleSchema = /*@__PURE__*/ + enumDesc(file_stately, 9); /** * @generated from enum stately.generated.VaultWeaponGroupingStyle */ -export const VaultWeaponGroupingStyle = /*@__PURE__*/ tsEnum(VaultWeaponGroupingStyleSchema); +export const VaultWeaponGroupingStyle = /*@__PURE__*/ + tsEnum(VaultWeaponGroupingStyleSchema); + diff --git a/api/stately/init/global-settings.ts b/api/stately/init/global-settings.ts index c9488f8..5405121 100644 --- a/api/stately/init/global-settings.ts +++ b/api/stately/init/global-settings.ts @@ -36,4 +36,5 @@ const prodSettings = client.create('GlobalSettings', { await client.putBatch(devSettings, betaSettings, prodSettings); console.log('Global settings initialized'); +// This is due to a bug in Connect! process.exit(0); diff --git a/api/stately/schema/README.md b/api/stately/schema/README.md index d388731..9c9f84c 100644 --- a/api/stately/schema/README.md +++ b/api/stately/schema/README.md @@ -4,7 +4,7 @@ These are the objects that DIM stores in its own service - most data comes from Key paths are laid out like this: -- `/app-:id`: `ApiApp` +- `/apps/app-:id`: `ApiApp` - `/gs-:stage`: `GlobalSettings` - `/loadoutShare-:id`: `LoadoutShare` - `/member-:memberId/settings`: `Settings` @@ -20,3 +20,7 @@ The goal with this modeling is to allow for syncing all of a user's info in two - `List("/p-:profileId/d-:destinyVersion")` - get all saved data for a particular game profile + destiny version (each profile can be associated with Destiny 1 and Destiny 2) - `List("/member-:memberId")` - get settings for a whole Bungie.net account. It's a List instead of a Get so we can sync it! + +Plus some fun stuff such as: + +- `List("/apps/")` to get all registered apps, and `SyncList()` to keep them up to date. diff --git a/api/stately/schema/app.ts b/api/stately/schema/app.ts index 08fe9bf..4893516 100644 --- a/api/stately/schema/app.ts +++ b/api/stately/schema/app.ts @@ -1,4 +1,4 @@ -import { itemType, string, type } from '@stately-cloud/schema'; +import { itemType, string, type, uint } from '@stately-cloud/schema'; // A UUID stored as a string. This is inefficient, but we always use them as // strings in this API. @@ -10,7 +10,7 @@ const uuidString = type('uuidString', string, { // "apps" aren't exposed to users - they're periodically synced by server instances. export const ApiApp = itemType('ApiApp', { - keyPath: '/app-:id', + keyPath: '/apps-:partition/app-:id', fields: { /** A short ID that uniquely identifies the app. */ id: { type: string, fieldNum: 1 }, @@ -23,5 +23,7 @@ export const ApiApp = itemType('ApiApp', { }, /** The origin used to allow CORS for this app. Only requests from this origin are allowed. */ origin: { type: string, fieldNum: 4 }, + /** This isn't a "real" field but StatelyDB won't allow a group without an ID. */ + partition: { type: uint, fieldNum: 5 }, }, });