Files
homarr/packages/api/src/settle-integrations.ts
T
Thomas Camlong 018f98e318 perf: implement Phase 1 homepage diet via Next.js Cache Components
P1-0: V8 heap cap (HOMARR_MAX_OLD_SPACE_SIZE, default 512MB)
P1-1: Module graph cuts
  - Dynamic integration imports (55 static → lazy loaders)
  - Conditional DB driver loading (only configured driver loaded)
  - Replace jsdom with linkedom for SVG sanitization
  - Delete eager widget barrel (55 import * as → 0)
  - Extract createRetryableLoader to @homarr/common
P1-2: Enable cacheComponents + restructure root layout
  - Suspense boundaries around session-dependent providers
  - ColorSchemeScript replaces server-side cookie reads
  - Board/manage/auth layouts wrapped in Suspense
P1-3: Cache read paths
  - Cache tag registry + invalidation helpers
  - Cache invalidation wired into all mutation routers
  - Translation deep-merge memoized per locale
P1-4: Prerendered shells + loading.tsx files
P1-5: Shared integration data cache (60s TTL, stale-while-revalidate)
P1-6: Redis-backed Next.js cache handler (@homarr/next-cache-handler)
P1-7: Redis optional (memory channel fallback), TTL on pubSub:last:*,
      Redis clients consolidated from 6 to 2
2026-08-07 18:59:47 +02:00

71 lines
2.1 KiB
TypeScript

import { createLogger } from "@homarr/core/infrastructure/logs";
import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error";
import { getCachedIntegrationData } from "./integration-data-cache";
const logger = createLogger({ module: "settleIntegrations" });
interface IntegrationLike {
id: string;
name: string;
kind: string;
}
interface Options<TIntegration extends IntegrationLike, TResult> {
fallback?: (integration: TIntegration, error: unknown) => TResult;
/** Widget procedure key, e.g. `downloads:getJobsAndStatuses:{"limitPerIntegration":50}` */
queryKey?: string;
}
export const integrationQueryKey = (widget: string, procedure: string, params?: unknown) => {
if (params === undefined) return `${widget}:${procedure}`;
return `${widget}:${procedure}:${JSON.stringify(params)}`;
};
export async function settleIntegrationQueries<TIntegration extends IntegrationLike, TResult>(
integrations: TIntegration[],
fn: (integration: TIntegration) => Promise<TResult>,
options?: Options<TIntegration, TResult>,
): Promise<TResult[]> {
const queryKey = options?.queryKey;
const runQuery = async (integration: TIntegration) => {
if (queryKey) {
return getCachedIntegrationData(integration.id, queryKey, () => fn(integration));
}
return fn(integration);
};
const settled = await Promise.allSettled(integrations.map(runQuery));
const results: TResult[] = [];
const errors: unknown[] = [];
settled.forEach((result, index) => {
if (result.status === "fulfilled") {
results.push(result.value);
return;
}
const integration = integrations[index];
logger.warn(
new ErrorWithMetadata(
"Integration query failed",
{ integrationId: integration?.id, integrationKind: integration?.kind },
{ cause: result.reason },
),
);
if (options?.fallback && integration) {
results.push(options.fallback(integration, result.reason));
return;
}
errors.push(result.reason);
});
if (results.length === 0 && errors.length > 0) {
throw errors[0];
}
return results;
}