Skip to content

Commit 3cae74f

Browse files
committed
Resolve provider usage indicator conflicts
2 parents d70120e + 131234b commit 3cae74f

203 files changed

Lines changed: 18159 additions & 9962 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,5 @@ __screenshots__/
2323
.tanstack
2424
squashfs-root/
2525
.vercel
26+
dist-electron/
27+
.electron-runtime/

apps/desktop/.gitignore

Lines changed: 0 additions & 2 deletions
This file was deleted.

apps/desktop/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@
1717
"dependencies": {
1818
"@effect/platform-node": "catalog:",
1919
"effect": "catalog:",
20-
"electron": "40.9.3",
20+
"electron": "41.5.0",
2121
"electron-updater": "^6.6.2"
2222
},
2323
"devDependencies": {
24+
"@effect/language-service": "catalog:",
25+
"@effect/vitest": "catalog:",
2426
"@t3tools/client-runtime": "workspace:*",
2527
"@t3tools/contracts": "workspace:*",
2628
"@t3tools/shared": "workspace:*",

apps/desktop/src/app/DesktopApp.ts

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
import * as Cause from "effect/Cause";
2+
import * as Data from "effect/Data";
3+
import * as Effect from "effect/Effect";
4+
import * as Option from "effect/Option";
5+
import * as Random from "effect/Random";
6+
import * as Ref from "effect/Ref";
7+
8+
import * as NetService from "@t3tools/shared/Net";
9+
import * as ElectronApp from "../electron/ElectronApp.ts";
10+
import * as ElectronDialog from "../electron/ElectronDialog.ts";
11+
import * as ElectronProtocol from "../electron/ElectronProtocol.ts";
12+
import { installDesktopIpcHandlers } from "../ipc/DesktopIpcHandlers.ts";
13+
import * as DesktopAppIdentity from "./DesktopAppIdentity.ts";
14+
import * as DesktopApplicationMenu from "../window/DesktopApplicationMenu.ts";
15+
import * as DesktopBackendManager from "../backend/DesktopBackendManager.ts";
16+
import * as DesktopEnvironment from "./DesktopEnvironment.ts";
17+
import * as DesktopLifecycle from "./DesktopLifecycle.ts";
18+
import * as DesktopObservability from "./DesktopObservability.ts";
19+
import * as DesktopServerExposure from "../backend/DesktopServerExposure.ts";
20+
import * as DesktopAppSettings from "../settings/DesktopAppSettings.ts";
21+
import * as DesktopShellEnvironment from "../shell/DesktopShellEnvironment.ts";
22+
import * as DesktopState from "./DesktopState.ts";
23+
import * as DesktopUpdates from "../updates/DesktopUpdates.ts";
24+
25+
const DEFAULT_DESKTOP_BACKEND_PORT = 3773;
26+
const MAX_TCP_PORT = 65_535;
27+
const DESKTOP_BACKEND_PORT_PROBE_HOSTS = ["127.0.0.1", "0.0.0.0", "::"] as const;
28+
29+
const makeDesktopRunId = Random.nextUUIDv4.pipe(
30+
Effect.map((value) => value.replaceAll("-", "").slice(0, 12)),
31+
);
32+
33+
class DesktopBackendPortUnavailableError extends Data.TaggedError(
34+
"DesktopBackendPortUnavailableError",
35+
)<{
36+
readonly startPort: number;
37+
readonly maxPort: number;
38+
readonly hosts: readonly string[];
39+
}> {
40+
override get message() {
41+
return `No desktop backend port is available on hosts ${this.hosts.join(", ")} between ${this.startPort} and ${this.maxPort}.`;
42+
}
43+
}
44+
45+
class DesktopDevelopmentBackendPortRequiredError extends Data.TaggedError(
46+
"DesktopDevelopmentBackendPortRequiredError",
47+
)<{}> {
48+
override get message() {
49+
return "T3CODE_PORT is required in desktop development.";
50+
}
51+
}
52+
53+
const { logInfo: logBootstrapInfo, logWarning: logBootstrapWarning } =
54+
DesktopObservability.makeComponentLogger("desktop-bootstrap");
55+
56+
const { logInfo: logStartupInfo, logError: logStartupError } =
57+
DesktopObservability.makeComponentLogger("desktop-startup");
58+
59+
const resolveDesktopBackendPort = Effect.fn("resolveDesktopBackendPort")(function* (
60+
configuredPort: Option.Option<number>,
61+
) {
62+
if (Option.isSome(configuredPort)) {
63+
return {
64+
port: configuredPort.value,
65+
selectedByScan: false,
66+
} as const;
67+
}
68+
69+
const net = yield* NetService.NetService;
70+
for (let port = DEFAULT_DESKTOP_BACKEND_PORT; port <= MAX_TCP_PORT; port += 1) {
71+
let availableOnEveryHost = true;
72+
73+
for (const host of DESKTOP_BACKEND_PORT_PROBE_HOSTS) {
74+
if (!(yield* net.canListenOnHost(port, host))) {
75+
availableOnEveryHost = false;
76+
break;
77+
}
78+
}
79+
80+
if (availableOnEveryHost) {
81+
return {
82+
port,
83+
selectedByScan: true,
84+
} as const;
85+
}
86+
}
87+
88+
return yield* new DesktopBackendPortUnavailableError({
89+
startPort: DEFAULT_DESKTOP_BACKEND_PORT,
90+
maxPort: MAX_TCP_PORT,
91+
hosts: DESKTOP_BACKEND_PORT_PROBE_HOSTS,
92+
});
93+
});
94+
95+
const handleFatalStartupError = Effect.fn("desktop.startup.handleFatalStartupError")(function* (
96+
stage: string,
97+
error: unknown,
98+
): Effect.fn.Return<
99+
void,
100+
never,
101+
| DesktopLifecycle.DesktopShutdown
102+
| DesktopState.DesktopState
103+
| ElectronApp.ElectronApp
104+
| ElectronDialog.ElectronDialog
105+
> {
106+
const shutdown = yield* DesktopLifecycle.DesktopShutdown;
107+
const state = yield* DesktopState.DesktopState;
108+
const electronApp = yield* ElectronApp.ElectronApp;
109+
const electronDialog = yield* ElectronDialog.ElectronDialog;
110+
const message = error instanceof Error ? error.message : String(error);
111+
const detail =
112+
error instanceof Error && typeof error.stack === "string" ? `\n${error.stack}` : "";
113+
yield* logStartupError("fatal startup error", {
114+
stage,
115+
message,
116+
...(detail.length > 0 ? { detail } : {}),
117+
});
118+
const wasQuitting = yield* Ref.getAndSet(state.quitting, true);
119+
if (!wasQuitting) {
120+
yield* electronDialog.showErrorBox(
121+
"T3 Code failed to start",
122+
`Stage: ${stage}\n${message}${detail}`,
123+
);
124+
}
125+
yield* shutdown.request;
126+
yield* electronApp.quit;
127+
});
128+
129+
const fatalStartupCause = <E>(stage: string, cause: Cause.Cause<E>) =>
130+
handleFatalStartupError(stage, Cause.pretty(cause)).pipe(Effect.andThen(Effect.failCause(cause)));
131+
132+
const bootstrap = Effect.gen(function* () {
133+
const backendManager = yield* DesktopBackendManager.DesktopBackendManager;
134+
const state = yield* DesktopState.DesktopState;
135+
const environment = yield* DesktopEnvironment.DesktopEnvironment;
136+
const desktopSettings = yield* DesktopAppSettings.DesktopAppSettings;
137+
const serverExposure = yield* DesktopServerExposure.DesktopServerExposure;
138+
yield* logBootstrapInfo("bootstrap start");
139+
140+
if (environment.isDevelopment && Option.isNone(environment.configuredBackendPort)) {
141+
return yield* new DesktopDevelopmentBackendPortRequiredError();
142+
}
143+
144+
const backendPortSelection = yield* resolveDesktopBackendPort(environment.configuredBackendPort);
145+
const backendPort = backendPortSelection.port;
146+
yield* logBootstrapInfo(
147+
backendPortSelection.selectedByScan
148+
? "selected backend port via sequential scan"
149+
: "using configured backend port",
150+
{
151+
port: backendPort,
152+
...(backendPortSelection.selectedByScan ? { startPort: DEFAULT_DESKTOP_BACKEND_PORT } : {}),
153+
},
154+
);
155+
156+
const settings = yield* desktopSettings.get;
157+
if (settings.serverExposureMode !== environment.defaultDesktopSettings.serverExposureMode) {
158+
yield* logBootstrapInfo("bootstrap restoring persisted server exposure mode", {
159+
mode: settings.serverExposureMode,
160+
});
161+
}
162+
const serverExposureState = yield* serverExposure.configureFromSettings({ port: backendPort });
163+
const backendConfig = yield* serverExposure.backendConfig;
164+
yield* logBootstrapInfo("bootstrap resolved backend endpoint", {
165+
baseUrl: backendConfig.httpBaseUrl.href,
166+
});
167+
if (serverExposureState.endpointUrl) {
168+
yield* logBootstrapInfo("bootstrap enabled network access", {
169+
endpointUrl: serverExposureState.endpointUrl,
170+
});
171+
} else if (settings.serverExposureMode === "network-accessible") {
172+
yield* logBootstrapWarning(
173+
"bootstrap fell back to local-only because no advertised network host was available",
174+
);
175+
}
176+
177+
yield* installDesktopIpcHandlers;
178+
yield* logBootstrapInfo("bootstrap ipc handlers registered");
179+
180+
if (!(yield* Ref.get(state.quitting))) {
181+
yield* backendManager.start;
182+
yield* logBootstrapInfo("bootstrap backend start requested");
183+
}
184+
}).pipe(Effect.withSpan("desktop.bootstrap"));
185+
186+
const startup = Effect.gen(function* () {
187+
const appIdentity = yield* DesktopAppIdentity.DesktopAppIdentity;
188+
const applicationMenu = yield* DesktopApplicationMenu.DesktopApplicationMenu;
189+
const electronApp = yield* ElectronApp.ElectronApp;
190+
const electronProtocol = yield* ElectronProtocol.ElectronProtocol;
191+
const lifecycle = yield* DesktopLifecycle.DesktopLifecycle;
192+
const shellEnvironment = yield* DesktopShellEnvironment.DesktopShellEnvironment;
193+
const desktopSettings = yield* DesktopAppSettings.DesktopAppSettings;
194+
const updates = yield* DesktopUpdates.DesktopUpdates;
195+
const environment = yield* DesktopEnvironment.DesktopEnvironment;
196+
197+
yield* shellEnvironment.installIntoProcess;
198+
const userDataPath = yield* appIdentity.resolveUserDataPath;
199+
yield* electronApp.setPath("userData", userDataPath);
200+
yield* logStartupInfo("runtime logging configured", { logDir: environment.logDir });
201+
yield* desktopSettings.load;
202+
203+
if (environment.platform === "linux") {
204+
yield* electronApp.appendCommandLineSwitch("class", environment.linuxWmClass);
205+
}
206+
207+
yield* appIdentity.configure;
208+
yield* lifecycle.register;
209+
210+
yield* electronApp.whenReady.pipe(
211+
Effect.withSpan("desktop.electron.whenReady"),
212+
Effect.catchCause((cause) => fatalStartupCause("whenReady", cause)),
213+
);
214+
yield* logStartupInfo("app ready");
215+
yield* appIdentity.configure;
216+
yield* applicationMenu.configure;
217+
yield* electronProtocol.registerDesktopFileProtocol;
218+
yield* updates.configure;
219+
yield* bootstrap.pipe(Effect.catchCause((cause) => fatalStartupCause("bootstrap", cause)));
220+
}).pipe(Effect.withSpan("desktop.startup"));
221+
222+
const scopedProgram = Effect.scoped(
223+
Effect.gen(function* () {
224+
const runId = yield* makeDesktopRunId;
225+
yield* Effect.annotateLogsScoped({ scope: "desktop", runId });
226+
yield* Effect.annotateCurrentSpan({ scope: "desktop", runId });
227+
228+
const shutdown = yield* DesktopLifecycle.DesktopShutdown;
229+
const backendManager = yield* DesktopBackendManager.DesktopBackendManager;
230+
231+
yield* Effect.addFinalizer(() =>
232+
backendManager.stop().pipe(Effect.ensuring(shutdown.markComplete)),
233+
);
234+
235+
yield* startup;
236+
yield* shutdown.awaitRequest;
237+
}),
238+
);
239+
240+
export const program = scopedProgram.pipe(Effect.withSpan("desktop.app"));

0 commit comments

Comments
 (0)