import { type NewStoredImage, type SignageStore, SignageStoreConflict, type StoredImage, type StoredImageReferences, } from "../../src/server/store/types.js"; import { createMemoryPlaybackStore, type MemoryPlaybackStore } from "./memory-playback.js"; import { createMemoryTimelineStore, type MemoryTimelineStore } from "./memory-timelines.js"; import { createMemoryViewerStore, type MemoryViewerStore } from "./memory-viewers.js"; /** * An in-memory implementation of Signage's data port. * * This is what makes Signage's *decisions* testable without PostgreSQL: deduplication, no-op * detection, reference-aware deletion, provider resolution, and playback reconciliation are all * behaviour, and behaviour proven against a real database for every case would be slow, flaky, and * therefore rare. The Drizzle implementation is proven separately, against a live disposable * database, where what is being proven is the row mapping rather than the decision. * * It enforces the same uniqueness guarantees the database does, so a test cannot pass here and fail * there. */ export interface MemorySignageStore extends SignageStore, MemoryTimelineStore, MemoryPlaybackStore, MemoryViewerStore { /** Test-only: seed a reference so reference-aware deletion can be exercised. */ addImageReference(imageId: string, kind: "slot" | "static-hold"): void; readonly images: Map; } export function createMemorySignageStore( options: { readonly now?: () => Date } = {}, ): MemorySignageStore { const now = options.now ?? (() => new Date()); const images = new Map(); const references = new Map(); const timelineStore = createMemoryTimelineStore(options); const playbackStore = createMemoryPlaybackStore(options); const viewerStore = createMemoryViewerStore(options); return { ...timelineStore, ...playbackStore, ...viewerStore, addImageReference(imageId: string, kind: "slot" | "static-hold"): void { const current = references.get(imageId) ?? { slots: 0, staticHolds: 0 }; references.set( imageId, kind === "slot" ? { slots: current.slots + 1, staticHolds: current.staticHolds } : { slots: current.slots, staticHolds: current.staticHolds + 1 }, ); }, countImageReferences(imageId: string): Promise { return Promise.resolve(references.get(imageId) ?? { slots: 0, staticHolds: 0 }); }, deleteImage(imageId: string): Promise { images.delete(imageId); references.delete(imageId); return Promise.resolve(); }, findImage(imageId: string): Promise { return Promise.resolve(images.get(imageId)); }, findReadyImageByContentHash(contentSha256: string): Promise { for (const image of images.values()) { if (image.contentSha256 === contentSha256 && image.status === "ready") { return Promise.resolve(image); } } return Promise.resolve(undefined); }, images, insertImage(image: NewStoredImage): Promise { for (const existing of images.values()) { if (existing.contentSha256 === image.contentSha256) { // Exactly what the unique index does, so a race is exercised here and not only in // production. return Promise.reject(new SignageStoreConflict("image.contentSha256")); } } images.set(image.id, { ...image, createdAt: now() }); return Promise.resolve(); }, listImages(): Promise { return Promise.resolve( [...images.values()].sort( (left, right) => right.createdAt.getTime() - left.createdAt.getTime(), ), ); }, updateImage( imageId: string, changes: { readonly displayName?: string; readonly expiresAt?: Date | null }, ): Promise { const existing = images.get(imageId); if (!existing) { return Promise.resolve(); } images.set(imageId, { ...existing, ...(changes.displayName === undefined ? {} : { displayName: changes.displayName }), ...(changes.expiresAt === undefined ? {} : { expiresAt: changes.expiresAt }), }); return Promise.resolve(); }, }; }