1import type { EnvironmentServerConfig } from "@t3tools/client-runtime/connection";
2import { runAtomCommand } from "@t3tools/client-runtime/state/runtime";
3import { AuthPreviewOperateScope, type EnvironmentId } from "@t3tools/contracts";
4import type { ClientSettings } from "@t3tools/contracts/settings";
5import { Atom } from "effect/reactivity";
6
7import {
8 getClientSettings,
9 getClientSettingsHydratedSnapshot,
10 subscribeClientSettings,
11 subscribeClientSettingsHydration,
12} from "~/hooks/useSettings";
13import { appAtomRegistry } from "~/rpc/atomRegistry";
14import { environmentPresentations } from "~/state/presentation";
15import { previewEnvironment } from "~/state/preview";
16import { environmentSession, sessionHasScope } from "~/state/session";
17
18export interface ProfileReport {
19 readonly profiles: ClientSettings["browserProfiles"];
20 readonly defaultProfileId: ClientSettings["browserDefaultProfileId"];
21 readonly recordingFrameRate: ClientSettings["browserRecordingFrameRate"];
22}
23
24/** Server-browser environments this client may operate, with the config each currently runs. */
25const reportTargetsAtom = Atom.make((get) => {
26 const targets = new Map<EnvironmentId, EnvironmentServerConfig>();
27 for (const [environmentId, config] of get(environmentPresentations.serverConfigsAtom)) {
28 if (!config.environment.capabilities.serverBrowser) continue;
29 const session = get(environmentSession.sessionStateAtom(environmentId));
30 if (!sessionHasScope(session, AuthPreviewOperateScope)) continue;
31 targets.set(environmentId, config);
32 }
33 return targets;
34}).pipe(Atom.withLabel("browser-profile-report-targets"));
35
36/**
37 * Keeps every operable server browser on this client's profiles. An environment is reported
38 * again when its server config changes (a restarted server has forgotten them), when it becomes
39 * operable, and when the profile settings change.
40 */
41export function startBrowserProfileReporting(): () => void {
42 const reported: ReportedProfiles = new Map();
43 const report = () => {
44 if (!getClientSettingsHydratedSnapshot()) return;
45 const settings = getClientSettings();
46 const input: ProfileReport = {
47 profiles: settings.browserProfiles,
48 defaultProfileId: settings.browserDefaultProfileId,
49 recordingFrameRate: settings.browserRecordingFrameRate,
50 };
51 const targets = appAtomRegistry.get(reportTargetsAtom);
52 for (const environmentId of takeDueProfileReports(reported, targets, input)) {
53 void runAtomCommand(
54 appAtomRegistry,
55 previewEnvironment.reportProfiles,
56 { environmentId, input },
57 { reportFailure: false },
58 );
59 }
60 };
61 const unsubscribes = [
62 appAtomRegistry.subscribe(reportTargetsAtom, report),
63 subscribeClientSettings(report),
64 subscribeClientSettingsHydration(report),
65 ];
66 report();
67 return () => {
68 for (const unsubscribe of unsubscribes) unsubscribe();
69 };
70}
71
72export type ReportedProfiles = Map<
73 EnvironmentId,
74 { readonly config: EnvironmentServerConfig; readonly input: ProfileReport }
75>;
76
77/**
78 * Returns the targets that need `input` reported and records them in `reported`. An
79 * environment that dropped out of `targets` is forgotten, so it is reported again once it
80 * becomes operable.
81 */
82export function takeDueProfileReports(
83 reported: ReportedProfiles,
84 targets: ReadonlyMap<EnvironmentId, EnvironmentServerConfig>,
85 input: ProfileReport,
86): EnvironmentId[] {
87 for (const environmentId of reported.keys()) {
88 if (!targets.has(environmentId)) reported.delete(environmentId);
89 }
90 const due: EnvironmentId[] = [];
91 for (const [environmentId, config] of targets) {
92 const last = reported.get(environmentId);
93 if (last && last.config === config && sameReportInput(last.input, input)) continue;
94 reported.set(environmentId, { config, input });
95 due.push(environmentId);
96 }
97 return due;
98}
99
100function sameReportInput(left: ProfileReport, right: ProfileReport): boolean {
101 return (
102 left.profiles === right.profiles &&
103 left.defaultProfileId === right.defaultProfileId &&
104 left.recordingFrameRate === right.recordingFrameRate
105 );
106}