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
71 lines
2.1 KiB
TypeScript
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;
|
|
}
|