fix(widgets): preserve data on integration failures (#6484)
This commit is contained in:
1 parent
906c9f11fa
commit
2885f7a297
4 files changed
+119
-4
No files matched your search
@@ -19,8 +19,15 @@ export async function settleIntegrationQueries<TIntegration extends IntegrationL
|
||||
options?: Options<TIntegration, TResult>,
|
||||
): Promise<TResult[]> {
|
||||
const settled = await Promise.allSettled(integrations.map(async (integration) => fn(integration)));
|
||||
return settled.flatMap((result, index) => {
|
||||
if (result.status === "fulfilled") return [result.value];
|
||||
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(
|
||||
@@ -29,7 +36,18 @@ export async function settleIntegrationQueries<TIntegration extends IntegrationL
|
||||
{ cause: result.reason },
|
||||
),
|
||||
);
|
||||
if (options?.fallback && integration) return [options.fallback(integration, result.reason)];
|
||||
return [];
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
|
||||
import { settleIntegrationQueries } from "../settle-integrations";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
logger: { warn: vi.fn() },
|
||||
}));
|
||||
|
||||
vi.mock("@homarr/core/infrastructure/logs", () => ({
|
||||
createLogger: () => mocks.logger,
|
||||
}));
|
||||
|
||||
vi.mock("@homarr/core/infrastructure/logs/error", () => ({
|
||||
ErrorWithMetadata: Error,
|
||||
}));
|
||||
|
||||
const integrations = [
|
||||
{ id: "first", name: "First", kind: "plex" },
|
||||
{ id: "second", name: "Second", kind: "jellyfin" },
|
||||
];
|
||||
|
||||
describe("settleIntegrationQueries", () => {
|
||||
test("returns an empty result when no integrations are configured", async () => {
|
||||
await expect(settleIntegrationQueries([], vi.fn())).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
test("keeps successful integrations when another integration fails", async () => {
|
||||
const error = new Error("Plex unavailable");
|
||||
|
||||
await expect(
|
||||
settleIntegrationQueries(integrations, (integration) =>
|
||||
integration.id === "first" ? Promise.reject(error) : Promise.resolve(integration.name),
|
||||
),
|
||||
).resolves.toEqual(["Second"]);
|
||||
});
|
||||
|
||||
test("rejects when every integration fails", async () => {
|
||||
const error = new Error("Integration unavailable");
|
||||
|
||||
await expect(settleIntegrationQueries(integrations, () => Promise.reject(error))).rejects.toBe(error);
|
||||
});
|
||||
|
||||
test("uses configured fallbacks when every integration fails", async () => {
|
||||
await expect(
|
||||
settleIntegrationQueries(integrations, () => Promise.reject(new Error("Integration unavailable")), {
|
||||
fallback: (integration) => integration.name,
|
||||
}),
|
||||
).resolves.toEqual(["First", "Second"]);
|
||||
});
|
||||
});
|
||||
@@ -13,4 +13,6 @@ export const mediaReleaseRequestHandler = createIntegrationRequestHandler<
|
||||
const integrationInstance = await createIntegrationAsync(integration);
|
||||
return await integrationInstance.getMediaReleasesAsync();
|
||||
},
|
||||
cacheTtlMs: 5 * 60 * 1000,
|
||||
fallbackToStaleOnError: true,
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { MediaRelease } from "@homarr/integrations/types";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
|
||||
import { mediaReleaseRequestHandler } from "../media-release";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
createIntegrationAsync: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@homarr/integrations", () => ({
|
||||
createIntegrationAsync: mocks.createIntegrationAsync,
|
||||
}));
|
||||
|
||||
const integration = {
|
||||
id: "plex-integration",
|
||||
kind: "plex",
|
||||
} as Parameters<typeof mediaReleaseRequestHandler.handler>[0];
|
||||
|
||||
describe("mediaReleaseRequestHandler", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-07-28T00:00:00Z"));
|
||||
mediaReleaseRequestHandler.invalidateCache();
|
||||
mocks.createIntegrationAsync.mockReset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
test("keeps the last successful releases when a refresh fails", async () => {
|
||||
const releases = [{ id: "release-1" }] as MediaRelease[];
|
||||
const getMediaReleasesAsync = vi.fn().mockResolvedValueOnce(releases).mockRejectedValueOnce(new Error("Plex down"));
|
||||
mocks.createIntegrationAsync.mockResolvedValue({ getMediaReleasesAsync });
|
||||
|
||||
const handler = mediaReleaseRequestHandler.handler(integration, {});
|
||||
const initial = await handler.getDataAsync();
|
||||
|
||||
vi.advanceTimersByTime(5 * 60 * 1000 + 1);
|
||||
const fallback = await handler.getDataAsync();
|
||||
|
||||
expect(fallback).toEqual(initial);
|
||||
expect(getMediaReleasesAsync).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user