Files
homarr/packages/api/src/router/custom-widget/preview-sessions.ts
T
+12 fcd7c979df feat!: release v2 (#6545)
Co-authored-by: Artur <145776561+arturict@users.noreply.github.com>
Co-authored-by: arturict <arturict@users.noreply.github.com>
Co-authored-by: Thomas <thomas@ajnart.fr>
Co-authored-by: Afonso Jorge Ramos <afonsojorgeramos@gmail.com>
Co-authored-by: Danura30082 <danura30082+github@gmail.com>
Co-authored-by: Kushie <122986913+kushiemoon-dev@users.noreply.github.com>
Co-authored-by: ajnart <thomas@ajnart.fr>
Co-authored-by: ajnart <thomas@ajnart.dev>
Co-authored-by: chenjun <chenjun@foxmail.com>
Co-authored-by: Wikibear <wikibear@users.noreply.github.com>
Co-authored-by: Bergasha <bergasha@gmail.com>
Co-authored-by: leafyelin <43306196+leafyelin@users.noreply.github.com>
Co-authored-by: Lucas Araujo <lucas.lra@gmail.com>
Co-authored-by: GreenMonito <86085570+GreenMonito@users.noreply.github.com>
Co-authored-by: Nerdy-Technician <roffo@nerdy-technician.social>
Co-authored-by: Jun <34545896+jcstrong@users.noreply.github.com>
Co-authored-by: Edmond Abraham <54119952+gitedmond@users.noreply.github.com>
Co-authored-by: thrawnx <tight.bulb9384@fastmail.com>
Co-authored-by: Chris <58918656+nordada@users.noreply.github.com>
Co-authored-by: yi111 <153097222+Yi-111-a@users.noreply.github.com>
Co-authored-by: Yanis <naamaneyanis@gmail.com>
Co-authored-by: Wikibear <me@sej.de>
Co-authored-by: alessiottavo <93441886+alessiottavo@users.noreply.github.com>
2026-10-02 21:00:14 +02:00

123 lines
5.0 KiB
TypeScript

import { createId } from "@homarr/common";
import { decryptSecret, encryptSecret } from "@homarr/common/server";
import { createRedisClient } from "@homarr/core/infrastructure/redis";
import type { RedisClient } from "@homarr/core/infrastructure/redis";
import { CustomWidgetPreviewSessionService } from "@homarr/custom-widgets/server";
import type {
CreatePreviewSessionInput,
CustomWidgetPreviewJournalEntry,
CustomWidgetPreviewSession,
PreviewSessionStore,
} from "@homarr/custom-widgets/server";
import { toTrpcError } from "./domain-error";
const SESSION_PREFIX = "custom-widget:preview-session:";
const JOURNAL_PREFIX = "custom-widget:preview-journal:";
const EVIDENCE_PREFIX = "custom-widget:preview-evidence:";
const COMPARE_AND_SWAP_SESSION_SCRIPT = `
local current = redis.call('GET', KEYS[1])
if not current then return 0 end
local ok, parsed = pcall(cjson.decode, current)
if not ok or type(parsed) ~= 'table' or tonumber(parsed.revision) ~= tonumber(ARGV[1]) then return 0 end
redis.call('SET', KEYS[1], ARGV[2], 'PX', ARGV[3])
return 1
`;
class RedisPreviewSessionStore implements PreviewSessionStore {
public constructor(private readonly redis: RedisClient) {}
public async saveSession(id: string, value: unknown, ttlMs: number) {
await this.redis.set(`${SESSION_PREFIX}${id}`, JSON.stringify(value), "PX", ttlMs);
}
public getSession(id: string) {
return this.redis.get(`${SESSION_PREFIX}${id}`);
}
public async compareAndSwapSession(id: string, expectedRevision: number, value: unknown, ttlMs: number) {
return (
Number(
await this.redis.eval(
COMPARE_AND_SWAP_SESSION_SCRIPT,
1,
`${SESSION_PREFIX}${id}`,
expectedRevision,
JSON.stringify(value),
ttlMs,
),
) === 1
);
}
public async deleteSession(id: string) {
await this.redis.del(`${SESSION_PREFIX}${id}`);
}
public async appendJournal(id: string, value: unknown, maxEntries: number, ttlMs: number) {
const key = `${JOURNAL_PREFIX}${id}`;
await this.redis
.multi()
.lpush(key, JSON.stringify(value))
.ltrim(key, 0, maxEntries - 1)
.pexpire(key, ttlMs)
.exec();
}
public getJournal(id: string, maxEntries: number) {
return this.redis.lrange(`${JOURNAL_PREFIX}${id}`, 0, maxEntries - 1);
}
public async saveEvidence(id: string, key: string, value: unknown, ttlMs: number) {
const evidenceKey = `${EVIDENCE_PREFIX}${id}`;
await this.redis.multi().hset(evidenceKey, key, JSON.stringify(value)).pexpire(evidenceKey, ttlMs).exec();
}
public getEvidence(id: string) {
return this.redis.hvals(`${EVIDENCE_PREFIX}${id}`);
}
}
let service: CustomWidgetPreviewSessionService | undefined;
function getService() {
if (service) return service;
const useLocal = process.env.CI !== undefined || process.env.NODE_ENV === "test";
service = new CustomWidgetPreviewSessionService({
createId,
encrypt: encryptSecret,
decrypt: (value) => decryptSecret(value as `${string}.${string}`),
store: useLocal ? undefined : new RedisPreviewSessionStore(createRedisClient()),
});
return service;
}
async function call<T>(operation: () => Promise<T>): Promise<T> {
try {
return await operation();
} catch (error) {
toTrpcError(error);
}
}
export type { CreatePreviewSessionInput, CustomWidgetPreviewJournalEntry, CustomWidgetPreviewSession };
export const createPreviewSession = (input: CreatePreviewSessionInput) => call(() => getService().create(input));
export const getPreviewSession = (id: string, userId: string) => call(() => getService().get(id, userId));
export const setPreviewSessionLiveActions = (id: string, userId: string, enabled: boolean) =>
call(() => getService().setLiveActions(id, userId, enabled));
export const revisePreviewSessionTemplate = (id: string, userId: string, template: string, expectedRevision?: number) =>
call(() => getService().reviseTemplate(id, userId, template, expectedRevision));
export const appendPreviewJournal = (
session: CustomWidgetPreviewSession,
entry: Omit<CustomWidgetPreviewJournalEntry, "id" | "sessionRevision" | "timestamp">,
) => call(() => getService().appendJournal(session, { ...entry, sessionRevision: session.revision }));
export const getPreviewJournal = (id: string, userId: string) => call(() => getService().getJournal(id, userId));
export const getPreviewEvidence = (id: string, userId: string) => call(() => getService().getEvidence(id, userId));
export const getPreviewSessionSecrets = (session: CustomWidgetPreviewSession, sourceId: string) =>
getService().getSecrets(session, sourceId);
export const setPreviewSessionSecrets = (id: string, userId: string, secrets: CreatePreviewSessionInput["secrets"]) =>
call(() => getService().setSecrets(id, userId, secrets));
export const configurePreviewSessionSource = (
id: string,
userId: string,
sourceId: string,
source: CreatePreviewSessionInput["sources"][string],
secrets: CreatePreviewSessionInput["secrets"],
) => call(() => getService().configureSource(id, userId, sourceId, source, secrets));