authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 11:09:02-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 11:09:02-07:00
logce0c20b840f9e9d49532c3480113e1e54201fab0
treed12d229d3930d05a542b99955d013b8eacf7ad17
parentfccecd8749056d4f811962a1523229500e7a4e72
signature Signed by PGP key B5690EEEBB952194

[codex] Structure desktop network interface failures (#3313)

Co-authored-by: codex <codex@users.noreply.github.com>

2 files changed, 88 insertions(+), 4 deletions(-)

apps/desktop/src/backend/DesktopNetworkInterfaces.test.ts created+65
...@@ -0,0 +1,65 @@
1import { assert, describe, it } from "@effect/vitest";
2import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
3import * as Cause from "effect/Cause";
4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6import { beforeEach, vi } from "vite-plus/test";
7
8const { networkInterfacesMock } = vi.hoisted(() => ({
9 networkInterfacesMock: vi.fn(),
10}));
11
12vi.mock("node:os", () => ({
13 networkInterfaces: networkInterfacesMock,
14}));
15
16import * as DesktopNetworkInterfaces from "./DesktopNetworkInterfaces.ts";
17
18const TestLayer = DesktopNetworkInterfaces.layer.pipe(
19 Layer.provide(Layer.succeed(HostProcessPlatform, "linux")),
20);
21
22describe("DesktopNetworkInterfaces", () => {
23 beforeEach(() => {
24 networkInterfacesMock.mockReset();
25 });
26
27 it.effect("reads network interfaces through the service", () => {
28 const interfaces = {
29 en0: [
30 {
31 address: "192.168.1.10",
32 family: "IPv4",
33 internal: false,
34 },
35 ],
36 };
37 networkInterfacesMock.mockReturnValueOnce(interfaces);
38
39 return Effect.gen(function* () {
40 const service = yield* DesktopNetworkInterfaces.DesktopNetworkInterfaces;
41 assert.strictEqual(yield* service.read, interfaces);
42 }).pipe(Effect.provide(TestLayer));
43 });
44
45 it.effect("preserves network interface read failures as structured defects", () => {
46 const cause = new Error("network interface probe failed");
47 networkInterfacesMock.mockImplementationOnce(() => {
48 throw cause;
49 });
50
51 return Effect.gen(function* () {
52 const service = yield* DesktopNetworkInterfaces.DesktopNetworkInterfaces;
53 const exit = yield* Effect.exit(service.read);
54
55 assert.equal(exit._tag, "Failure");
56 if (exit._tag === "Failure") {
57 const error = Cause.squash(exit.cause);
58 assert.instanceOf(error, DesktopNetworkInterfaces.DesktopNetworkInterfacesReadError);
59 assert.equal(error.platform, "linux");
60 assert.strictEqual(error.cause, cause);
61 assert.equal(error.message, "Failed to read desktop network interfaces on linux.");
62 }
63 }).pipe(Effect.provide(TestLayer));
64 });
65});
apps/desktop/src/backend/DesktopNetworkInterfaces.ts+23-4
...@@ -1,8 +1,10 @@...@@ -1,8 +1,10 @@
1import * as NodeOS from "node:os";1import * as NodeOS from "node:os";
22
3import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
3import * as Context from "effect/Context";4import * as Context from "effect/Context";
4import * as Effect from "effect/Effect";5import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";6import * as Layer from "effect/Layer";
7import * as Schema from "effect/Schema";
68
7export interface DesktopNetworkInterfaceInfo {9export interface DesktopNetworkInterfaceInfo {
8 readonly address: string;10 readonly address: string;
...@@ -18,6 +20,18 @@ export type NetworkInterfaces = Readonly<...@@ -18,6 +20,18 @@ export type NetworkInterfaces = Readonly<
18 Record<string, readonly DesktopNetworkInterfaceInfo[] | undefined>20 Record<string, readonly DesktopNetworkInterfaceInfo[] | undefined>
19>;21>;
2022
23export class DesktopNetworkInterfacesReadError extends Schema.TaggedErrorClass<DesktopNetworkInterfacesReadError>()(
24 "DesktopNetworkInterfacesReadError",
25 {
26 platform: Schema.String,
27 cause: Schema.Defect(),
28 },
29) {
30 override get message(): string {
31 return `Failed to read desktop network interfaces on ${this.platform}.`;
32 }
33}
34
21export class DesktopNetworkInterfaces extends Context.Service<35export class DesktopNetworkInterfaces extends Context.Service<
22 DesktopNetworkInterfaces,36 DesktopNetworkInterfaces,
23 {37 {
...@@ -25,9 +39,14 @@ export class DesktopNetworkInterfaces extends Context.Service<...@@ -25,9 +39,14 @@ export class DesktopNetworkInterfaces extends Context.Service<
25 }39 }
26>()("@t3tools/desktop/backend/DesktopNetworkInterfaces") {}40>()("@t3tools/desktop/backend/DesktopNetworkInterfaces") {}
2741
28export const make = (): DesktopNetworkInterfaces["Service"] =>42export const make = Effect.gen(function* () {
29 DesktopNetworkInterfaces.of({43 const platform = yield* HostProcessPlatform;
30 read: Effect.sync(() => NodeOS.networkInterfaces()),44 return DesktopNetworkInterfaces.of({
45 read: Effect.try({
46 try: () => NodeOS.networkInterfaces(),
47 catch: (cause) => new DesktopNetworkInterfacesReadError({ platform, cause }),
48 }).pipe(Effect.orDie),
31 });49 });
50});
3251
33export const layer = Layer.succeed(DesktopNetworkInterfaces, make());52export const layer = Layer.effect(DesktopNetworkInterfaces, make);