1/**
2 * Removes `projectSettingsOverrides` entries
3 * whose project no longer exists.
4 *
5 * `ProjectService.delete` clears a project's entry as part of deleting the
6 * project, but that only covers deletions a server new enough to run that
7 * code actually processes. A project deleted by an older server, or one
8 * whose cleanup write failed, leaves its override behind:
9 * `ProviderInstanceRegistryHydration` folds every project's override in
10 * unconditionally, so a machine-disabled provider instance the override once
11 * turned on stays probed and alive forever, with no UI left to turn it off.
12 *
13 * This sweep runs once at boot and removes overrides left over from projects
14 * that no longer exist. The settings watcher's normal reconcile picks up
15 * whatever it clears, so the registry does not need its own live-project
16 * check on every settings change.
17 *
18 * @module provider/ProjectSettingsOverrideSweep
19 */
20import { type ProjectId, type ServerSettings } from "@t3tools/contracts";
21import * as Effect from "effect/Effect";
22
23import * as ProjectStore from "../orchestration-v2/ProjectStore.ts";
24import * as Settings from "../serverSettings.ts";
25
26/** Pure so the orphan rule can be tested without layers. */
27export const findOrphanedProjectIds = (
28 settings: Pick<ServerSettings, "projectSettingsOverrides">,
29 liveProjectIds: ReadonlySet<ProjectId>,
30): ReadonlyArray<ProjectId> =>
31 Object.keys(settings.projectSettingsOverrides)
32 .map((projectId) => projectId as ProjectId)
33 .filter((projectId) => !liveProjectIds.has(projectId));
34
35/**
36 * Best-effort: a failed read or write here should not block server startup,
37 * and a sweep that does not run to completion is simply retried on the next
38 * boot.
39 */
40export const sweep = Effect.gen(function* () {
41 const serverSettings = yield* Settings.ServerSettingsService;
42 const projectStore = yield* ProjectStore.ProjectStoreV2;
43
44 const settings = yield* serverSettings.getSettings;
45 const liveShells = yield* projectStore.listShells();
46 const liveProjectIds = new Set(liveShells.map((shell) => shell.id));
47
48 const orphanedProjectIds = findOrphanedProjectIds(settings, liveProjectIds);
49 if (orphanedProjectIds.length === 0) return;
50
51 yield* serverSettings.updateSettings({
52 projectSettingsOverrides: Object.fromEntries(
53 orphanedProjectIds.map((projectId) => [projectId, null]),
54 ),
55 });
56}).pipe(
57 Effect.asVoid,
58 Effect.catchCause((cause) =>
59 Effect.logWarning("Failed to sweep orphaned project settings overrides.", cause),
60 ),
61);