openpetswithchatandmcp/apps/desktop/src/app-state-normalization.ts
2026-06-19 07:56:56 +00:00

139 lines
4 KiB
TypeScript

import { builtInPet } from "./built-in-familiar.js";
import type { Point } from "./display.js";
import {
createDefaultAnalyticsState,
normalizeAnalytics,
type FamiliarOSActivityRecord,
type FamiliarOSAnalyticsState,
} from "./app-state-analytics.js";
import {
normalizeInstalledPetRecord,
validateInstalledPetFiles,
type FamiliarStateRecord,
} from "./app-state-familiar-records.js";
import {
createDefaultPreferences,
normalizePreferences,
type FamiliarOSPreferences,
} from "./app-state-preferences.js";
import { assertSafePetId, getInstalledPetDir } from "./familiar-paths.js";
export interface InstalledPetState {
readonly id: string;
readonly displayName: string;
readonly description?: string;
readonly builtIn: boolean;
readonly protected: boolean;
readonly installed: boolean;
readonly source?: {
readonly kind?: "catalog";
readonly catalogVersion: 2;
readonly zip: string;
readonly preview: string;
} | {
readonly kind: "codex";
readonly path: string;
};
readonly broken?: boolean;
readonly brokenReason?: string;
}
export interface FamiliarOSStateV1 {
readonly version: 1;
readonly preferences: FamiliarOSPreferences;
readonly familiars: {
readonly installed: readonly InstalledPetState[];
};
readonly defaultPet: {
readonly position?: Point;
};
readonly analytics: FamiliarOSAnalyticsState;
}
export function normalizeState(value: unknown): FamiliarOSStateV1 {
const record = isRecord(value) ? value : {};
const defaultPetRecord = isRecord(record.defaultPet) ? record.defaultPet : {};
const preferencesRecord = isRecord(record.preferences) ? record.preferences : {};
const defaultState = createDefaultState();
const position = normalizeMaybePosition(defaultPetRecord.position);
const installedPets = normalizeInstalledPets(record);
const defaultPetId = typeof preferencesRecord.defaultPetId === "string"
&& installedPets.some((familiar) => familiar.id === preferencesRecord.defaultPetId && !familiar.broken)
? preferencesRecord.defaultPetId
: builtInPet.id;
return {
version: 1,
preferences: normalizePreferences({
...defaultState.preferences,
...preferencesRecord,
defaultPetId,
}),
familiars: {
installed: installedPets,
},
defaultPet: position ? { position } : {},
analytics: normalizeAnalytics(record.analytics),
};
}
export function createDefaultState(): FamiliarOSStateV1 {
return {
version: 1,
preferences: createDefaultPreferences(),
familiars: {
installed: [builtInPet],
},
defaultPet: {},
analytics: createDefaultAnalyticsState(),
};
}
export function cloneState(state: FamiliarOSStateV1): FamiliarOSStateV1 {
return structuredClone(state) as FamiliarOSStateV1;
}
export function normalizePosition(value: Partial<Point>): Point | undefined {
if (typeof value.x !== "number" || typeof value.y !== "number") {
return undefined;
}
if (!Number.isFinite(value.x) || !Number.isFinite(value.y)) {
return undefined;
}
return {
x: Math.round(value.x),
y: Math.round(value.y),
};
}
function normalizeInstalledPets(value: Record<string, unknown>): InstalledPetState[] {
const installed = isRecord(value.familiars) && Array.isArray(value.familiars.installed)
? value.familiars.installed
: [];
const normalized = installed
.map((familiar) => normalizeInstalledPetRecord(familiar, {
builtInPetId: builtInPet.id,
assertSafePetId,
validateInstalledPetFiles: (petId) => validateInstalledPetFiles(petId, getInstalledPetDir),
}))
.filter((familiar): familiar is FamiliarStateRecord => Boolean(familiar && familiar.id !== builtInPet.id));
return [builtInPet, ...normalized];
}
function normalizeMaybePosition(value: unknown): Point | undefined {
if (!isRecord(value)) {
return undefined;
}
return normalizePosition(value);
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}
export type { FamiliarOSActivityRecord };