feat: initial Signage 0.1.0
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
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<string, StoredImage>;
|
||||
}
|
||||
|
||||
export function createMemorySignageStore(
|
||||
options: { readonly now?: () => Date } = {},
|
||||
): MemorySignageStore {
|
||||
const now = options.now ?? (() => new Date());
|
||||
const images = new Map<string, StoredImage>();
|
||||
const references = new Map<string, StoredImageReferences>();
|
||||
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<StoredImageReferences> {
|
||||
return Promise.resolve(references.get(imageId) ?? { slots: 0, staticHolds: 0 });
|
||||
},
|
||||
|
||||
deleteImage(imageId: string): Promise<void> {
|
||||
images.delete(imageId);
|
||||
references.delete(imageId);
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
findImage(imageId: string): Promise<StoredImage | undefined> {
|
||||
return Promise.resolve(images.get(imageId));
|
||||
},
|
||||
|
||||
findReadyImageByContentHash(contentSha256: string): Promise<StoredImage | undefined> {
|
||||
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<void> {
|
||||
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<readonly StoredImage[]> {
|
||||
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<void> {
|
||||
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();
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user