perf: optimize widget integration requests

This commit is contained in:
ajnart committed 2026-08-14 11:32:11 +02:00
1 parent f65969053a
commit dcef36ffa7
14 files changed
+517 -48

No files matched your search

@@ -11,11 +11,14 @@ import { filteringStatusSchema, statsResponseSchema, statusResponseSchema } from
export class AdGuardHomeIntegration extends Integration implements DnsHoleSummaryIntegration {
public async getSummaryAsync(): Promise<DnsHoleSummary> {
const statsResponse = await fetchWithTrustedCertificatesAsync(this.url("/control/stats"), {
headers: {
Authorization: `Basic ${this.getAuthorizationHeaderValue()}`,
},
});
const headers = {
Authorization: `Basic ${this.getAuthorizationHeaderValue()}`,
};
const [statsResponse, statusResponse, filteringStatusResponse] = await Promise.all([
fetchWithTrustedCertificatesAsync(this.url("/control/stats"), { headers }),
fetchWithTrustedCertificatesAsync(this.url("/control/status"), { headers }),
fetchWithTrustedCertificatesAsync(this.url("/control/filtering/status"), { headers }),
]);
if (!statsResponse.ok) {
throw new Error(
@@ -23,24 +26,12 @@ export class AdGuardHomeIntegration extends Integration implements DnsHoleSummar
);
}
const statusResponse = await fetchWithTrustedCertificatesAsync(this.url("/control/status"), {
headers: {
Authorization: `Basic ${this.getAuthorizationHeaderValue()}`,
},
});
if (!statusResponse.ok) {
throw new Error(
`Failed to fetch status for ${this.integration.name} (${this.integration.id}): ${statusResponse.statusText}`,
);
}
const filteringStatusResponse = await fetchWithTrustedCertificatesAsync(this.url("/control/filtering/status"), {
headers: {
Authorization: `Basic ${this.getAuthorizationHeaderValue()}`,
},
});
if (!filteringStatusResponse.ok) {
throw new Error(
`Failed to fetch filtering status for ${this.integration.name} (${this.integration.id}): ${filteringStatusResponse.statusText}`,
@@ -0,0 +1,95 @@
// @vitest-environment node
import { Response } from "undici";
import { beforeEach, describe, expect, test, vi } from "vitest";
vi.hoisted(() => {
process.env.SKIP_ENV_VALIDATION = "true";
process.env.SECRET_ENCRYPTION_KEY = "ff3f4f7ce30e870c9630de9e5d244ffa81101a24ed0dfe5f064beb53a7e684f1";
});
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import { AdGuardHomeIntegration } from "../adguard-home-integration";
vi.mock("@homarr/core/infrastructure/http", () => ({ fetchWithTrustedCertificatesAsync: vi.fn() }));
const mockFetch = vi.mocked(fetchWithTrustedCertificatesAsync);
const integration = new AdGuardHomeIntegration({
id: "adguard-1",
name: "AdGuard Home",
url: "https://adguard.example.com",
externalUrl: null,
decryptedSecrets: [
{ kind: "username", value: "admin" },
{ kind: "password", value: "secret" },
],
});
describe("AdGuardHomeIntegration", () => {
beforeEach(() => vi.clearAllMocks());
test("starts all summary requests before awaiting any response", async () => {
const requestGates = new Map<string, ReturnType<typeof Promise.withResolvers<Response>>>();
mockFetch.mockImplementation((url) => {
const path = new URL(url.toString()).pathname;
const gate = Promise.withResolvers<Response>();
requestGates.set(path, gate);
return gate.promise;
});
const resultPromise = integration.getSummaryAsync();
await vi.waitFor(() => {
expect([...requestGates.keys()]).toEqual(["/control/stats", "/control/status", "/control/filtering/status"]);
});
requestGates.get("/control/stats")?.resolve(jsonResponse(stats));
requestGates.get("/control/status")?.resolve(jsonResponse(status));
requestGates.get("/control/filtering/status")?.resolve(jsonResponse(filteringStatus));
await expect(resultPromise).resolves.toMatchObject({
status: "enabled",
adsBlockedToday: 20,
dnsQueriesToday: 100,
domainsBeingBlocked: 12,
});
});
});
const jsonResponse = (body: unknown) => new Response(JSON.stringify(body), { status: 200 });
const stats = {
time_units: "hours",
top_queried_domains: [],
top_clients: [],
top_blocked_domains: [],
dns_queries: [40, 60],
blocked_filtering: [5, 15],
replaced_safebrowsing: [],
replaced_parental: [],
num_dns_queries: 100,
num_blocked_filtering: 20,
num_replaced_safebrowsing: 0,
num_replaced_safesearch: 0,
num_replaced_parental: 0,
avg_processing_time: 0.01,
};
const status = {
version: "1.0.0",
language: "en",
dns_addresses: ["0.0.0.0"],
dns_port: 53,
http_port: 3000,
protection_enabled: true,
dhcp_available: false,
running: true,
};
const filteringStatus = {
filters: [
{ url: "https://example.com/filter.txt", name: "Enabled", id: 1, rules_count: 12, enabled: true },
{ url: "https://example.com/disabled.txt", name: "Disabled", id: 2, rules_count: 50, enabled: false },
],
};
@@ -26,13 +26,16 @@ export class DashDotIntegration extends Integration implements ISystemHealthMoni
}
public async getSystemInfoAsync(): Promise<SystemHealthMonitoring> {
const info = await this.getInfoAsync();
const cpuLoad = await this.getCurrentCpuLoadAsync();
const memoryLoad = await this.getCurrentMemoryLoadAsync();
const storageLoad = await this.getCurrentStorageLoadAsync();
const networkLoad = await this.getCurrentNetworkLoadAsync();
const gpuLoad = await this.getCurrentGpuLoadAsync();
const [info, cpuLoad, memoryLoad, storageLoad, networkLoad, gpuLoad] = await Promise.all([
this.getInfoAsync(),
this.getCurrentCpuLoadAsync(),
this.getCurrentMemoryLoadAsync(),
this.getCurrentStorageLoadAsync(),
this.getCurrentNetworkLoadAsync(),
this.getCurrentGpuLoadAsync(),
]);
// Read history after the CPU request has persisted its newest sample.
const channel = this.getChannel();
const history = await channel.getSliceUntilTimeAsync(dayjs().subtract(15, "minutes").toDate());
@@ -0,0 +1,114 @@
// @vitest-environment node
import { Response } from "undici";
import { beforeEach, describe, expect, test, vi } from "vitest";
vi.hoisted(() => {
process.env.SKIP_ENV_VALIDATION = "true";
process.env.SECRET_ENCRYPTION_KEY = "ff3f4f7ce30e870c9630de9e5d244ffa81101a24ed0dfe5f064beb53a7e684f1";
});
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import { createChannelEventHistoryOld } from "../../../../redis/src/lib/channel";
import { DashDotIntegration } from "../dashdot-integration";
vi.mock("@homarr/redis", () => ({}));
vi.mock("@homarr/core/infrastructure/http", () => ({ fetchWithTrustedCertificatesAsync: vi.fn() }));
vi.mock("../../../../redis/src/lib/channel", () => ({ createChannelEventHistoryOld: vi.fn() }));
const mockFetch = vi.mocked(fetchWithTrustedCertificatesAsync);
const mockCreateHistory = vi.mocked(createChannelEventHistoryOld);
const channel = {
pushAsync: vi.fn(),
getSliceUntilTimeAsync: vi.fn(),
};
const integration = new DashDotIntegration({
id: "dashdot-1",
name: "DashDot",
url: "https://dashdot.example.com",
externalUrl: null,
decryptedSecrets: [],
});
describe("DashDotIntegration", () => {
beforeEach(() => {
vi.clearAllMocks();
mockCreateHistory.mockReturnValue(channel as never);
});
test("starts independent requests together and reads history after the CPU sample is persisted", async () => {
const requestGates = new Map<string, ReturnType<typeof Promise.withResolvers<Response>>>();
const persistenceGate = Promise.withResolvers<void>();
const order: string[] = [];
mockFetch.mockImplementation((url) => {
const path = new URL(url.toString()).pathname;
const gate = Promise.withResolvers<Response>();
requestGates.set(path, gate);
return gate.promise;
});
channel.pushAsync.mockImplementation(async () => {
order.push("push-start");
await persistenceGate.promise;
order.push("push-end");
});
channel.getSliceUntilTimeAsync.mockImplementation(async () => {
order.push("history");
return [cpuLoad];
});
const resultPromise = integration.getSystemInfoAsync();
await vi.waitFor(() => {
expect([...requestGates.keys()]).toEqual([
"/info",
"/load/cpu",
"/load/ram",
"/load/storage",
"/load/network",
"/load/gpu",
]);
});
requestGates.get("/info")?.resolve(jsonResponse(serverInfo));
requestGates.get("/load/cpu")?.resolve(jsonResponse(cpuLoad));
requestGates.get("/load/ram")?.resolve(jsonResponse({ load: 4_000 }));
requestGates.get("/load/storage")?.resolve(jsonResponse([250]));
requestGates.get("/load/network")?.resolve(jsonResponse({ up: 20, down: 10 }));
requestGates.get("/load/gpu")?.resolve(jsonResponse({ layout: [{ load: 30, memory: 40 }] }));
await vi.waitFor(() => expect(channel.pushAsync).toHaveBeenCalledOnce());
expect(channel.getSliceUntilTimeAsync).not.toHaveBeenCalled();
persistenceGate.resolve();
const result = await resultPromise;
expect(order).toEqual(["push-start", "push-end", "history"]);
expect(result).toMatchObject({
cpuUtilization: 15,
memUsedInBytes: 4_000,
memAvailableInBytes: 12_000,
network: { up: 20, down: 10 },
fileSystem: [{ used: "250", available: "750", percentage: 25 }],
gpu: [{ memoryUtilization: 40, processorUtilization: 30 }],
});
});
});
const jsonResponse = (body: unknown) => new Response(JSON.stringify(body), { status: 200 });
const cpuLoad = [
{ load: 10, temp: 40 },
{ load: 20, temp: 50 },
];
const serverInfo = {
os: { distro: "Linux", kernel: "6.0", release: "1", uptime: 60 },
cpu: { brand: "Intel", model: "Test CPU" },
ram: { size: 16_000 },
storage: [{ size: 1_000, disks: [{ device: "sda", brand: "Test", type: "SSD" }] }],
gpu: { layout: [{ brand: "Test GPU" }] },
};
@@ -23,9 +23,11 @@ export class NzbGetIntegration extends Integration implements IDownloadClientInt
public async getClientJobsAndStatusAsync(input: { limit: number }): Promise<DownloadClientJobsAndStatus> {
const type = "usenet";
const queue = await this.nzbGetApiCallAsync("listgroups");
const history = await this.nzbGetApiCallAsync("history");
const nzbGetStatus = await this.nzbGetApiCallAsync("status");
const [queue, history, nzbGetStatus] = await Promise.all([
this.nzbGetApiCallAsync("listgroups"),
this.nzbGetApiCallAsync("history"),
this.nzbGetApiCallAsync("status"),
]);
const status: DownloadClientStatus = {
paused: nzbGetStatus.DownloadPaused,
rates: { down: nzbGetStatus.DownloadRate },
@@ -0,0 +1,75 @@
// @vitest-environment node
import { Response } from "undici";
import { beforeEach, describe, expect, test, vi } from "vitest";
vi.hoisted(() => {
process.env.SKIP_ENV_VALIDATION = "true";
process.env.SECRET_ENCRYPTION_KEY = "ff3f4f7ce30e870c9630de9e5d244ffa81101a24ed0dfe5f064beb53a7e684f1";
});
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import type { IntegrationSecret } from "../../../base/types";
import { NzbGetIntegration } from "../nzbget-integration";
vi.mock("@homarr/core/infrastructure/http", () => ({
fetchWithTrustedCertificatesAsync: vi.fn(),
}));
const mockFetch = vi.mocked(fetchWithTrustedCertificatesAsync);
const secrets: IntegrationSecret[] = [
{ kind: "username", value: "test-user" },
{ kind: "password", value: "test-password" },
];
const createIntegration = () =>
new NzbGetIntegration({
id: "test-nzbget",
name: "Test NZBGet",
url: "https://nzbget.example.com",
externalUrl: null,
decryptedSecrets: secrets,
});
describe("NzbGetIntegration.getClientJobsAndStatusAsync", () => {
beforeEach(() => {
mockFetch.mockReset();
});
test("starts queue, history, and status requests together", async () => {
const releaseRequests = Promise.withResolvers<void>();
const startedMethods: string[] = [];
const results = {
listgroups: [],
history: [],
status: { DownloadPaused: false, DownloadRate: 0 },
} as const;
mockFetch.mockImplementation(async (_url, init) => {
const method = (JSON.parse(String(init?.body)) as { method: keyof typeof results }).method;
startedMethods.push(method);
await releaseRequests.promise;
return new Response(JSON.stringify({ result: results[method] }), {
status: 200,
headers: { "content-type": "application/json" },
});
});
const resultPromise = createIntegration().getClientJobsAndStatusAsync({ limit: 10 });
try {
await vi.waitFor(() => {
expect(startedMethods).toEqual(expect.arrayContaining(["listgroups", "history", "status"]));
expect(startedMethods).toHaveLength(3);
});
} finally {
releaseRequests.resolve();
}
await expect(resultPromise).resolves.toStrictEqual({
status: { paused: false, rates: { down: 0 }, types: ["usenet"] },
items: [],
});
});
});
@@ -25,12 +25,16 @@ export class SabnzbdIntegration extends Integration implements IDownloadClientIn
public async getClientJobsAndStatusAsync(input: { limit: number }): Promise<DownloadClientJobsAndStatus> {
const type = "usenet";
const { queue } = await queueSchema.parseAsync(
await this.sabNzbApiCallAsync("queue", { limit: input.limit.toString() }),
);
const { history } = await historySchema.parseAsync(
await this.sabNzbApiCallAsync("history", { limit: input.limit.toString() }),
);
const [queueResult, historyResult] = await Promise.all([
this.sabNzbApiCallAsync("queue", { limit: input.limit.toString() }).then((result) =>
queueSchema.parseAsync(result),
),
this.sabNzbApiCallAsync("history", { limit: input.limit.toString() }).then((result) =>
historySchema.parseAsync(result),
),
]);
const { queue } = queueResult;
const { history } = historyResult;
const status: DownloadClientStatus = {
paused: queue.paused,
rates: { down: Math.floor(Number(queue.kbpersec) * 1024) }, //Actually rounded kiBps ()
@@ -0,0 +1,71 @@
// @vitest-environment node
import { Response } from "undici";
import { beforeEach, describe, expect, test, vi } from "vitest";
vi.hoisted(() => {
process.env.SKIP_ENV_VALIDATION = "true";
process.env.SECRET_ENCRYPTION_KEY = "ff3f4f7ce30e870c9630de9e5d244ffa81101a24ed0dfe5f064beb53a7e684f1";
});
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import type { IntegrationSecret } from "../../../base/types";
import { SabnzbdIntegration } from "../sabnzbd-integration";
vi.mock("@homarr/core/infrastructure/http", () => ({
fetchWithTrustedCertificatesAsync: vi.fn(),
}));
const mockFetch = vi.mocked(fetchWithTrustedCertificatesAsync);
const secrets: IntegrationSecret[] = [{ kind: "apiKey", value: "test-api-key" }];
const createIntegration = () =>
new SabnzbdIntegration({
id: "test-sabnzbd",
name: "Test SABnzbd",
url: "https://sabnzbd.example.com",
externalUrl: null,
decryptedSecrets: secrets,
});
describe("SabnzbdIntegration.getClientJobsAndStatusAsync", () => {
beforeEach(() => {
mockFetch.mockReset();
});
test("starts queue and history requests together", async () => {
const releaseRequests = Promise.withResolvers<void>();
const startedModes: string[] = [];
const results = {
queue: { queue: { paused: false, kbpersec: "0", slots: [] } },
history: { history: { slots: [] } },
} as const;
mockFetch.mockImplementation(async (url) => {
const mode = new URL(String(url)).searchParams.get("mode") as keyof typeof results;
startedModes.push(mode);
await releaseRequests.promise;
return new Response(JSON.stringify(results[mode]), {
status: 200,
headers: { "content-type": "application/json" },
});
});
const resultPromise = createIntegration().getClientJobsAndStatusAsync({ limit: 10 });
try {
await vi.waitFor(() => {
expect(startedModes).toEqual(expect.arrayContaining(["queue", "history"]));
expect(startedModes).toHaveLength(2);
});
} finally {
releaseRequests.resolve();
}
await expect(resultPromise).resolves.toStrictEqual({
status: { paused: false, rates: { down: 0 }, types: ["usenet"] },
items: [],
});
});
});
@@ -1,5 +1,12 @@
// @vitest-environment node
import { beforeEach, describe, expect, test, vi } from "vitest";
vi.hoisted(() => {
process.env.SKIP_ENV_VALIDATION = "true";
process.env.SECRET_ENCRYPTION_KEY = "ff3f4f7ce30e870c9630de9e5d244ffa81101a24ed0dfe5f064beb53a7e684f1";
});
import { IntegrationRequestError } from "../../base/errors/http/integration-request-error";
import type { IntegrationSecret } from "../../base/types";
@@ -8,7 +15,7 @@ const ws = vi.hoisted(() => {
const constructedUrls: string[] = [];
// A failure with a `code` emits a coded TLS-style error; without one it mimics a missing endpoint (HTTP 404).
const failures: { path: string; code?: string }[] = [];
let responder: (method: string, params: unknown[]) => unknown = () => undefined;
let responder: (method: string, params: unknown[]) => unknown | Promise<unknown> = () => undefined;
type Listener = (...args: unknown[]) => void;
@@ -58,12 +65,15 @@ const ws = vi.hoisted(() => {
return;
}
const result = responder(payload.method as string, (payload.params as unknown[] | undefined) ?? []);
const envelope =
payload.jsonrpc === "2.0"
? { jsonrpc: "2.0", id: payload.id, result }
: { msg: "result", id: payload.id, result };
this.respond(JSON.stringify(envelope));
void Promise.resolve(responder(payload.method as string, (payload.params as unknown[] | undefined) ?? [])).then(
(result) => {
const envelope =
payload.jsonrpc === "2.0"
? { jsonrpc: "2.0", id: payload.id, result }
: { msg: "result", id: payload.id, result };
this.respond(JSON.stringify(envelope));
},
);
}
close() {
@@ -85,7 +95,7 @@ const ws = vi.hoisted(() => {
sentPayloads,
constructedUrls,
failures,
setResponder: (fn: (method: string, params: unknown[]) => unknown) => {
setResponder: (fn: (method: string, params: unknown[]) => unknown | Promise<unknown>) => {
responder = fn;
},
};
@@ -210,6 +220,34 @@ describe("TrueNasIntegration", () => {
});
});
test("starts independent system requests together while preserving the network interface dependency", async () => {
const gates = new Map<string, ReturnType<typeof Promise.withResolvers<unknown>>>();
ws.setResponder((method) => {
if (method.startsWith("auth.")) return true;
const gate = Promise.withResolvers<unknown>();
gates.set(method, gate);
return gate.promise;
});
const integration = createIntegration(credentials);
const resultPromise = integration.getSystemInfoAsync();
await vi.waitFor(() => {
expect([...gates.keys()]).toEqual(["system.info", "reporting.get_data", "pool.query", "interface.query"]);
});
expect(gates.has("reporting.netdata_get_data")).toBe(false);
gates.get("interface.query")?.resolve(happyResponder("interface.query"));
await vi.waitFor(() => expect(gates.has("reporting.netdata_get_data")).toBe(true));
for (const [method, gate] of gates) gate.resolve(happyResponder(method));
await expect(resultPromise).resolves.toMatchObject({
version: "TrueNAS-SCALE-24.10",
network: { up: 20_000, down: 10_000 },
});
});
test("authenticates with an API key when one is configured", async () => {
const integration = createIntegration([{ kind: "apiKey", value: "api-key-123" }]);
@@ -24,15 +24,16 @@ export class TrueNasIntegration extends Integration implements ISystemHealthMoni
}
public async getSystemInfoAsync(): Promise<SystemHealthMonitoring> {
const systemInformation = await this.getSystemInformationAsync();
const reporting = await this.getReportingAsync();
const [systemInformation, reporting, datasets, netdata] = await Promise.all([
this.getSystemInformationAsync(),
this.getReportingAsync(),
this.getPoolsAsync(),
this.getReportingNetdataAsync(),
]);
const cpuData = this.extractLatestReportingData(reporting, "cpu");
const cpuTempData = this.extractLatestReportingData(reporting, "cputemp");
const memoryData = this.extractLatestReportingData(reporting, "memory");
const datasets = await this.getPoolsAsync();
const netdata = await this.getReportingNetdataAsync();
const upload = this.extractNetworkTrafficData(netdata, 2); // Index 2 is "sent"
const download = this.extractNetworkTrafficData(netdata, 1); // Index 1 is "received"
@@ -1,3 +1,5 @@
// @vitest-environment node
import { Request, Response } from "undici";
import { beforeEach, describe, expect, test, vi } from "vitest";
@@ -76,6 +78,7 @@ describe("UmamiIntegration", () => {
expect(requestedUrls).toHaveLength(1);
expect(requestedUrls[0]?.path).toContain("/metrics");
expect(requestedUrls[0]?.params.get("type")).toBe("event");
expect(requestedUrls[0]?.params.get("limit")).toBe("5000");
expect(requestedUrls.some((requested) => requested.path.endsWith("/events"))).toBe(false);
});
@@ -29,6 +29,7 @@ import {
// Umami event type ID for custom events (type 1 = page view, type 2 = custom event)
const UMAMI_CUSTOM_EVENT_TYPE = 2;
const UMAMI_EVENT_NAME_LIMIT = 5_000;
const UMAMI_EVENT_PAGE_SIZE = 5_000;
const UMAMI_EVENT_PAGE_MAX = 20;
@@ -184,7 +185,14 @@ export class UmamiIntegration extends Integration {
const authHeaders = await this.getAuthHeadersAsync();
const endAt = Date.now();
const startAt = dayjs().subtract(30, "day").valueOf();
const metrics = await this.getWebsiteMetricsAsync(websiteId, startAt, endAt, "event", authHeaders);
const metrics = await this.getWebsiteMetricsAsync(
websiteId,
startAt,
endAt,
"event",
authHeaders,
UMAMI_EVENT_NAME_LIMIT,
);
const names = new Set<string>();
for (const metric of metrics) {
if (metric.x) names.add(metric.x);
@@ -0,0 +1,60 @@
import { beforeEach, describe, expect, test, vi } from "vitest";
import type { MediaRequestStats } from "@homarr/integrations/types";
import { mediaRequestStatsRequestHandler } from "./media-request-stats";
const mocks = vi.hoisted(() => ({
createIntegrationAsync: vi.fn(),
}));
vi.mock("@homarr/integrations", () => ({
createIntegrationAsync: mocks.createIntegrationAsync,
}));
const integration = {
id: "media-request-integration",
kind: "overseerr",
} as Parameters<typeof mediaRequestStatsRequestHandler.handler>[0];
describe("mediaRequestStatsRequestHandler", () => {
beforeEach(() => {
mediaRequestStatsRequestHandler.invalidateCache();
mocks.createIntegrationAsync.mockReset();
});
test("starts independent upstream requests together", async () => {
const requestsStarted = Promise.withResolvers<void>();
const expected = {
stats: {
total: 8,
movie: 5,
tv: 3,
pending: 1,
approved: 2,
declined: 1,
processing: 1,
available: 3,
},
users: [{ id: 1, displayName: "User", avatar: "/avatar.png", requestCount: 4, link: "/users/1" }],
} satisfies MediaRequestStats;
const getStatsAsync = vi.fn(async () => {
await requestsStarted.promise;
return expected.stats;
});
const getUsersAsync = vi.fn(async () => {
await requestsStarted.promise;
return expected.users;
});
mocks.createIntegrationAsync.mockResolvedValue({ getStatsAsync, getUsersAsync });
const resultPromise = mediaRequestStatsRequestHandler.handler(integration, {}).getDataAsync();
await vi.waitFor(() => {
expect(getStatsAsync).toHaveBeenCalledOnce();
expect(getUsersAsync).toHaveBeenCalledOnce();
});
requestsStarted.resolve();
await expect(resultPromise).resolves.toMatchObject({ data: expected });
});
});
@@ -11,9 +11,13 @@ export const mediaRequestStatsRequestHandler = createIntegrationRequestHandler<
>({
async requestAsync(integration, _input) {
const integrationInstance = await createIntegrationAsync(integration);
const [stats, users] = await Promise.all([
integrationInstance.getStatsAsync(),
integrationInstance.getUsersAsync(),
]);
return {
stats: await integrationInstance.getStatsAsync(),
users: await integrationInstance.getUsersAsync(),
stats,
users,
};
},
cacheTtlMs: 60_000,