feat: initial Signage 0.1.0

This commit is contained in:
ekklesia
2026-08-28 16:41:54 -05:00
parent 20756f11af
commit 92bef4c7aa
107 changed files with 24397 additions and 0 deletions
+99
View File
@@ -0,0 +1,99 @@
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<void> {
await database.delete(viewers).where(eq(viewers.id, viewerId));
},
async findViewer(viewerId: string): Promise<StoredViewer | undefined> {
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<StoredViewer | undefined> {
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<void> {
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<readonly StoredViewer[]> {
const rows = await database.select().from(viewers).orderBy(asc(viewers.name));
return rows.map(toViewer);
},
async updateViewer(viewerId: string, changes: ViewerChanges): Promise<void> {
const values: Record<string, unknown> = { 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));
},
};
}