querysub 0.605.0 → 0.607.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,76 @@
1
+ import { SocketFunction } from "socket-function/SocketFunction";
2
+ import { listServerBuckets, clearServerWriteStats, activateServerBucket, getServerActiveBucket } from "sliftutils/storage/remoteStorage/createArchives";
3
+ import type { ServerBucketInfo, ActiveBucketInfo } from "sliftutils/storage/remoteStorage/storageServerState";
4
+ import { parseStorageUrl, authenticateStorage } from "sliftutils/storage/remoteStorage/ArchivesRemote";
5
+ import { RemoteStorageController, STORAGE_NOT_AUTHENTICATED } from "sliftutils/storage/remoteStorage/storageController";
6
+ import type { AccessTotals, AccessSummaryState } from "sliftutils/storage/remoteStorage/accessStats";
7
+ import type { SummaryEntry } from "sliftutils/treeSummary";
8
+ import { getSyncedController } from "../../../library-components/SyncedController";
9
+ import { assertIsManagementUser } from "../../../diagnostics/managementPages";
10
+ import { STORAGE_ACCOUNT } from "../../../-a-archives/archives2";
11
+
12
+ // The registered controller and its synced wrapper have to keep a stable identity, so this file is a full-refresh boundary rather than a hot-reload one (matching SyncedController itself).
13
+ module.hotreload = false;
14
+
15
+ /** The access-stats endpoints have no client wrapper in sliftutils (a pinned dependency), so we reconnect and authenticate the same way its own callServer does. */
16
+ async function callStorageServer<T>(url: string, run: (controller: typeof RemoteStorageController.nodes[string]) => Promise<T>): Promise<T> {
17
+ SocketFunction.ENABLE_CLIENT_MODE = true;
18
+ let parsed = parseStorageUrl(url);
19
+ let nodeId = SocketFunction.connect({ address: parsed.address, port: parsed.port });
20
+ let controller = RemoteStorageController.nodes[nodeId];
21
+ try {
22
+ return await run(controller);
23
+ } catch (e) {
24
+ if (!String((e as Error).stack ?? e).includes(STORAGE_NOT_AUTHENTICATED)) throw e;
25
+ await authenticateStorage({ address: parsed.address, port: parsed.port, nodeId });
26
+ return await run(controller);
27
+ }
28
+ }
29
+
30
+ /** Every storage call is proxied through here: they authenticate with this machine's identity CA, which the browser has no access to. */
31
+ class StoragePageControllerBase {
32
+ /** One call per server, so each caches and refreshes on its own. */
33
+ public async getServerBuckets(url: string): Promise<ServerBucketInfo[]> {
34
+ return await listServerBuckets({ url, account: STORAGE_ACCOUNT });
35
+ }
36
+ public async activateBucket(url: string, bucketName: string): Promise<ActiveBucketInfo | string> {
37
+ return await activateServerBucket({ url, account: STORAGE_ACCOUNT, bucketName });
38
+ }
39
+ public async getActiveBucket(url: string, bucketName: string): Promise<ActiveBucketInfo | string> {
40
+ return await getServerActiveBucket({ url, account: STORAGE_ACCOUNT, bucketName });
41
+ }
42
+ public async clearWriteStats(url: string): Promise<{ clearedBuckets: number }> {
43
+ return await clearServerWriteStats({ url, account: STORAGE_ACCOUNT });
44
+ }
45
+ /** Per-operation totals since startup (or the last reset), aggregated across every bucket in the account. */
46
+ public async getAccessStats(url: string): Promise<AccessTotals> {
47
+ return await callStorageServer(url, controller => controller.getAccessStats({ account: STORAGE_ACCOUNT }));
48
+ }
49
+ /** The path breakdown of one operation. weightBySize orders it (and the totals) by bytes rather than call count. */
50
+ public async getAccessSummaries(url: string, operation: string, maxCount: number, weightBySize: boolean): Promise<SummaryEntry<AccessSummaryState>[]> {
51
+ return await callStorageServer(url, controller => controller.getAccessSummaries({ account: STORAGE_ACCOUNT, operation, maxCount, weightBySize }));
52
+ }
53
+ }
54
+
55
+ export const StoragePageController = SocketFunction.register(
56
+ "StoragePageController-4f1c93ab-77e2-4d15-9a30-1c6b8f5d2e04",
57
+ new StoragePageControllerBase(),
58
+ () => ({
59
+ getServerBuckets: {},
60
+ activateBucket: {},
61
+ getActiveBucket: {},
62
+ clearWriteStats: {},
63
+ getAccessStats: {},
64
+ getAccessSummaries: {},
65
+ }),
66
+ () => ({
67
+ hooks: [assertIsManagementUser],
68
+ }),
69
+ {
70
+ }
71
+ );
72
+
73
+ export const StorageSynced = getSyncedController(StoragePageController, {
74
+ reads: {},
75
+ writes: {},
76
+ });
@@ -0,0 +1,73 @@
1
+ import type { ServerBucketInfo } from "sliftutils/storage/remoteStorage/storageServerState";
2
+ import { getLiveServiceParameters, getMachineTargets, applyCommandTemplate, ServiceParameters, ServiceConfig, DEFAULT_OVERLAP_TIME } from "../../machineSchema";
3
+ import { STORAGE_COMMAND_PREFIX } from "../../serviceCategories";
4
+
5
+ module.hotreload = true;
6
+
7
+ const URL_ARG_REGEX = /--url\s+(\S+)/g;
8
+
9
+ /** The release a storage service is in the middle of. Read straight from the service configs, so it resolves without contacting any storage server - which is exactly when it matters most, since a deploy is the usual reason the servers don't answer. */
10
+ export type StorageService = {
11
+ serviceKey: string;
12
+ serviceTitle: string;
13
+ releaseTime?: number;
14
+ overlapTime: number;
15
+ hasOldParameters: boolean;
16
+ };
17
+
18
+ export type StorageServer = StorageService & {
19
+ url: string;
20
+ };
21
+
22
+ export type StorageServerBuckets = StorageServer & {
23
+ /** Still waiting on this server's own endpoint - each server loads independently */
24
+ loading?: boolean;
25
+ buckets?: ServerBucketInfo[];
26
+ /** This server could not be read at all. Kept per server rather than thrown, so one bad server costs its own row and nothing else. */
27
+ error?: string;
28
+ };
29
+
30
+ function getStorageUrls(parameters: ServiceParameters): string[] {
31
+ if (!parameters.command.trimStart().startsWith(STORAGE_COMMAND_PREFIX)) return [];
32
+ let urls: string[] = [];
33
+ for (let target of getMachineTargets(parameters)) {
34
+ let command = applyCommandTemplate(parameters.command, target.variables);
35
+ for (let match of command.matchAll(URL_ARG_REGEX)) {
36
+ urls.push(match[1]);
37
+ }
38
+ }
39
+ return urls;
40
+ }
41
+
42
+ /** The storage servers a set of service configs runs, worked out on the client - it already has the configs, so asking the server to re-derive them would just make the whole page wait on one uncacheable call. */
43
+ export function getStorageServers(configs: ServiceConfig[]): StorageServer[] {
44
+ let serviceByUrl = new Map<string, StorageServer>();
45
+ for (let config of configs) {
46
+ let allParameters = [getLiveServiceParameters(config)];
47
+ for (let parameters of allParameters) {
48
+ if (!parameters) continue;
49
+ for (let url of getStorageUrls(parameters)) {
50
+ if (serviceByUrl.has(url)) continue;
51
+ serviceByUrl.set(url, {
52
+ url,
53
+ serviceKey: parameters.key,
54
+ serviceTitle: config.info.title,
55
+ releaseTime: config.parameters.releaseTime,
56
+ // The overlap always comes from the newest parameters, even though it governs how long the old instances outlive the release
57
+ overlapTime: config.parameters.overlapTime ?? DEFAULT_OVERLAP_TIME,
58
+ hasOldParameters: !!config.oldParameters,
59
+ });
60
+ }
61
+ }
62
+ }
63
+ return [...serviceByUrl.values()];
64
+ }
65
+
66
+ export function getStorageServices(servers: StorageServer[]): StorageService[] {
67
+ let byKey = new Map<string, StorageService>();
68
+ for (let server of servers) {
69
+ if (byKey.has(server.serviceKey)) continue;
70
+ byKey.set(server.serviceKey, server);
71
+ }
72
+ return [...byKey.values()];
73
+ }
@@ -29,6 +29,16 @@ export const SERVICE_FOLDER = `${SERVICE_FOLDER_NAME}/`;
29
29
  export const MACHINE_RESYNC_INTERVAL = timeInMinute * 15;
30
30
  export const SERVICE_NODE_FILE_NAME = "serviceNodeId.txt";
31
31
 
32
+
33
+ export type MachineConfig = {
34
+ machineId: string;
35
+ disabled: boolean;
36
+ };
37
+ // We want all of these to have very high availability. Even if they're wrong, it's better to have an old service deployed or even view information on an old service, or even set information only in back plays rather than have the server be completely down.
38
+ export const machineInfos = archiveJSONT<MachineInfo>(() => getArchives2("machines/machine-heartbeats/"), { fallbacks: true });
39
+ export const serviceConfigs = archiveJSONT<ServiceConfig>(() => getArchives2("machines/service-configs/"), { fallbacks: true });
40
+ export const machineConfigs = archiveJSONT<MachineConfig>(() => getArchives2("machines/machine-configs/"), { fallbacks: true });
41
+
32
42
  export type MachineInfo = {
33
43
  machineId: string;
34
44
 
@@ -182,13 +192,6 @@ function normalizeServiceConfig(config: ServiceConfig): ServiceConfig {
182
192
  return config;
183
193
  }
184
194
 
185
- export type MachineConfig = {
186
- machineId: string;
187
- disabled: boolean;
188
- };
189
- export const machineInfos = archiveJSONT<MachineInfo>(() => getArchives2("machines/machine-heartbeats/"));
190
- export const serviceConfigs = archiveJSONT<ServiceConfig>(() => getArchives2("machines/service-configs/"));
191
- export const machineConfigs = archiveJSONT<MachineConfig>(() => getArchives2("machines/machine-configs/"));
192
195
 
193
196
  export type LaunchRecord = {
194
197
  serviceId: string;
@@ -276,7 +279,7 @@ export class MachineServiceControllerBase {
276
279
 
277
280
  public async getMachineList() {
278
281
  return [...new Set([
279
- ...(await machineInfos.keys({ fallbacks: true })),
282
+ ...(await machineInfos.keys()),
280
283
  ])];
281
284
  }
282
285
  public async deleteMachineIds(machineIds: string[]) {
@@ -303,7 +306,7 @@ export class MachineServiceControllerBase {
303
306
  }
304
307
  public async getServiceList(): Promise<string[]> {
305
308
  return [...new Set([
306
- ...(await serviceConfigs.keys({ fallbacks: true })),
309
+ ...(await serviceConfigs.keys()),
307
310
  ])];
308
311
  }
309
312
  public async getServiceConfig(serviceId: string): Promise<ServiceConfig | undefined> {
@@ -312,7 +315,7 @@ export class MachineServiceControllerBase {
312
315
  }
313
316
 
314
317
  public async getMachineConfigList() {
315
- return await machineConfigs.values({ fallbacks: true });
318
+ return await machineConfigs.values();
316
319
  }
317
320
  public async getMachineConfig(machineId: string): Promise<MachineConfig | undefined> {
318
321
  return await machineConfigs.get(machineId);
@@ -378,7 +381,7 @@ export class MachineServiceControllerBase {
378
381
  // NOTE: This is correct. This is obviously correct. For some reason, the AI wrote some really big, complicated thing here. I don't know why. This is the only way it should be done, obviously.
379
382
  config.oldParameters = getLiveServiceParameters(serviceConfig);
380
383
 
381
- await serviceConfigs.set(serviceId, config, { fallbacks: true });
384
+ await serviceConfigs.set(serviceId, config);
382
385
  // Only notify we were or are deployed. If it's not deployed, this will be ignored anyways.
383
386
  if (config.parameters.deploy || serviceConfig.parameters.deploy) {
384
387
  for (let machineId of getConfigMachineIds(config)) {
@@ -24,13 +24,16 @@ export class UsageBar extends qreact.Component<{
24
24
  label: string;
25
25
  value: number;
26
26
  max: number;
27
+ /** Appended after each formatted number, so byte bars can read "5.2GB" instead of a bare "5.2G". */
28
+ unit?: string;
27
29
  /** Fraction of max at which the bar fill turns yellow. */
28
30
  warningThreshold?: number;
29
31
  /** Fraction of max at which the bar fill turns red and flashes. */
30
32
  errorThreshold?: number;
31
33
  }> {
32
34
  render() {
33
- let { label, value, max, warningThreshold, errorThreshold } = this.props;
35
+ let { label, value, max, unit, warningThreshold, errorThreshold } = this.props;
36
+ let suffix = unit || "";
34
37
  let fraction = max && value / max || 0;
35
38
  let isError = errorThreshold !== undefined && fraction >= errorThreshold;
36
39
  let isWarning = !isError && warningThreshold !== undefined && fraction >= warningThreshold;
@@ -42,7 +45,7 @@ export class UsageBar extends qreact.Component<{
42
45
  + (isError && (" " + FLASH_CLASS_NAME) || "")
43
46
  } />
44
47
  <div className={css.relative}>
45
- {label} ({formatNumber(value)} / {formatNumber(max)})
48
+ {label} ({formatNumber(value)}{suffix} / {formatNumber(max)}{suffix})
46
49
  </div>
47
50
  {isError && <style>{`
48
51
  @keyframes ${FLASH_CLASS_NAME}-anim {