+12








fcd7c979df
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>
123 lines
5.0 KiB
TypeScript
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));
|