// Domain DTOs shared by the plugin server and the UI — Schemas are the source of truth, // the derived types keep the original domain names so the pure core reads unchanged. // // Trap, measured on this API (rom-manager's note only covers half of it): `optionalKey` // rejects a present-but-`undefined` value and 400s at the RESPONSE encoder — but // `Schema.optional` is not the fix, because the HttpApi encoder serialises `undefined` as // `null`, and `string | undefined` then refuses that null when the CLIENT decodes it. The // failure is silent in the SSE feed (invalid frames are skipped) and loud in the typed // client. So: every "may be absent" field here is `Schema.NullOr`, and the server always // sends an explicit value. JSON-native, symmetric, no undefined anywhere on the wire. import { Schema } from "effect"; /** Where the export was found — drives the console's "exporter connected?" banner. */ export const Location = Schema.Struct({ /** The resolved source dir (the ingest inbox, or a Playnite data dir), or null. */ dir: Schema.NullOr(Schema.String), /** Every dir considered, in probe order (the UI's diagnostics view). */ candidates: Schema.Array(Schema.String), /** The resolved export file, or null if the exporter hasn't written one yet. */ file: Schema.NullOr(Schema.String), /** mtime (epoch ms) of the export file, or null. */ mtime: Schema.NullOr(Schema.Number), /** True when the file came from `/ingest/playnite` (the reliable lane). */ viaIngest: Schema.Boolean, }); export type Location = typeof Location.Type; /** A game the compute rejected, with a human reason (Library page, CLI). */ export const SkippedGame = Schema.Struct({ id: Schema.String, title: Schema.String, reason: Schema.String, }); export type SkippedGame = typeof SkippedGame.Type; /** A summary of one desired-state compute (Overview + Library pages). */ export const SyncReport = Schema.Struct({ /** ISO timestamp the exporter stamped on the source export. */ generatedAt: Schema.String, /** Schema version of the export that produced this report. */ schemaVersion: Schema.Number, /** Games in the export before filtering. */ considered: Schema.Number, /** Entries in the reconcile payload. */ included: Schema.Number, skipped: Schema.Array(SkippedGame), /** Dropped by an explicit per-game override (the Library page's toggle). */ excluded: Schema.Array( Schema.Struct({ id: Schema.String, title: Schema.String }), ), warnings: Schema.Array(Schema.String), /** Included entries per Playnite source ("Steam", "GOG", "(none)", …). */ perSource: Schema.Record(Schema.String, Schema.Number), }); export type SyncReport = typeof SyncReport.Type; export const LastSync = Schema.Struct({ fingerprint: Schema.String, count: Schema.Number, at: Schema.Number, }); export type LastSync = typeof LastSync.Type; /** The engine status the Overview page renders and the SSE feed streams. */ export const EngineStatus = Schema.Struct({ platform: Schema.String, location: Location, /** Last read/decode error (missing export, corruption, too-new schema), else null. */ exportError: Schema.NullOr(Schema.String), /** Playnite's own version + mode, as stamped on the last readable export. */ playnite: Schema.NullOr( Schema.Struct({ version: Schema.NullOr(Schema.String), mode: Schema.NullOr(Schema.String), }), ), artMode: Schema.String, syncing: Schema.Boolean, lastSync: Schema.NullOr(LastSync), lastReport: Schema.NullOr(SyncReport), paths: Schema.Struct({ dir: Schema.String, config: Schema.String, cache: Schema.String, ingest: Schema.String, }), }); export type EngineStatus = typeof EngineStatus.Type;