import type { ModuleDatabase } from "@ekklesia/module-sdk"; import { asc, eq } from "drizzle-orm"; import { viewers } from "../../schema/tables.js"; import type { NewStoredViewer, SignageViewerStore, StoredViewer, ViewerChanges } from "./types.js"; /** The viewer half of Signage's production store. Deliberately dull: heartbeat coalescing, health * derivation, and every other decision live above it. */ function toViewer(row: typeof viewers.$inferSelect): StoredViewer { return { cachedAssetCount: row.cachedAssetCount, channelId: row.channelId, createdAt: row.createdAt, deviceId: row.deviceId, id: row.id, lastError: row.lastErrorCode === null || row.lastErrorAt === null ? null : { at: row.lastErrorAt, code: row.lastErrorCode, detail: row.lastErrorDetail }, lastSeenAt: row.lastSeenAt, name: row.name, offlineReady: row.offlineReady, reportedContributionId: row.reportedContributionId, reportedPlaybackRevision: row.reportedPlaybackRevision, reportedProviderId: row.reportedProviderId, reportedSelectionId: row.reportedSelectionId, reportedTimelineId: row.reportedTimelineId, viewerKey: row.viewerKey, }; } export function createViewerStore(database: ModuleDatabase): SignageViewerStore { return { async deleteViewer(viewerId: string): Promise { await database.delete(viewers).where(eq(viewers.id, viewerId)); }, async findViewer(viewerId: string): Promise { const rows = await database.select().from(viewers).where(eq(viewers.id, viewerId)); const row = rows[0]; return row ? toViewer(row) : undefined; }, async findViewerByKey(viewerKey: string): Promise { const rows = await database.select().from(viewers).where(eq(viewers.viewerKey, viewerKey)); const row = rows[0]; return row ? toViewer(row) : undefined; }, async insertViewer(viewer: NewStoredViewer): Promise { await database.insert(viewers).values({ cachedAssetCount: viewer.cachedAssetCount, channelId: viewer.channelId, deviceId: viewer.deviceId, id: viewer.id, name: viewer.name, offlineReady: viewer.offlineReady, reportedContributionId: viewer.reportedContributionId, reportedPlaybackRevision: viewer.reportedPlaybackRevision, reportedProviderId: viewer.reportedProviderId, reportedSelectionId: viewer.reportedSelectionId, reportedTimelineId: viewer.reportedTimelineId, viewerKey: viewer.viewerKey, }); }, async listViewers(): Promise { const rows = await database.select().from(viewers).orderBy(asc(viewers.name)); return rows.map(toViewer); }, async updateViewer(viewerId: string, changes: ViewerChanges): Promise { const values: Record = { updatedAt: new Date() }; for (const key of [ "cachedAssetCount", "channelId", "deviceId", "lastSeenAt", "name", "offlineReady", "reportedContributionId", "reportedPlaybackRevision", "reportedProviderId", "reportedSelectionId", "reportedTimelineId", ] as const) { if (changes[key] !== undefined) { values[key] = changes[key]; } } if (changes.lastError !== undefined) { values.lastErrorAt = changes.lastError?.at ?? null; values.lastErrorCode = changes.lastError?.code ?? null; values.lastErrorDetail = changes.lastError?.detail ?? null; } await database.update(viewers).set(values).where(eq(viewers.id, viewerId)); }, }; }