| author | |
| committer | |
| log | ce0c20b840f9e9d49532c3480113e1e54201fab0 |
| tree | d12d229d3930d05a542b99955d013b8eacf7ad17 |
| parent | fccecd8749056d4f811962a1523229500e7a4e72 |
| signature | Signed by PGP key B5690EEEBB952194 |
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 @@ |
| 1 | import { assert, describe, it } from "@effect/vitest"; | |
| 2 | import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; | |
| 3 | import * as Cause from "effect/Cause"; | |
| 4 | import * as Effect from "effect/Effect"; | |
| 5 | import * as Layer from "effect/Layer"; | |
| 6 | import { beforeEach, vi } from "vite-plus/test"; | |
| 7 | ||
| 8 | const { networkInterfacesMock } = vi.hoisted(() => ({ | |
| 9 | networkInterfacesMock: vi.fn(), | |
| 10 | })); | |
| 11 | ||
| 12 | vi.mock("node:os", () => ({ | |
| 13 | networkInterfaces: networkInterfacesMock, | |
| 14 | })); | |
| 15 | ||
| 16 | import * as DesktopNetworkInterfaces from "./DesktopNetworkInterfaces.ts"; | |
| 17 | ||
| 18 | const TestLayer = DesktopNetworkInterfaces.layer.pipe( | |
| 19 | Layer.provide(Layer.succeed(HostProcessPlatform, "linux")), | |
| 20 | ); | |
| 21 | ||
| 22 | describe("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 | 1 | import * as NodeOS from "node:os"; |
| 2 | 2 | |
| 3 | import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; | |
| 3 | 4 | import * as Context from "effect/Context"; |
| 4 | 5 | import * as Effect from "effect/Effect"; |
| 5 | 6 | import * as Layer from "effect/Layer"; |
| 7 | import * as Schema from "effect/Schema"; | |
| 6 | 8 | |
| 7 | 9 | export interface DesktopNetworkInterfaceInfo { |
| 8 | 10 | readonly address: string; |
| ... | ... | @@ -18,6 +20,18 @@ export type NetworkInterfaces = Readonly< |
| 18 | 20 | Record<string, readonly DesktopNetworkInterfaceInfo[] | undefined> |
| 19 | 21 | >; |
| 20 | 22 | |
| 23 | export 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 | ||
| 21 | 35 | export class DesktopNetworkInterfaces extends Context.Service< |
| 22 | 36 | DesktopNetworkInterfaces, |
| 23 | 37 | { |
| ... | ... | @@ -25,9 +39,14 @@ export class DesktopNetworkInterfaces extends Context.Service< |
| 25 | 39 | } |
| 26 | 40 | >()("@t3tools/desktop/backend/DesktopNetworkInterfaces") {} |
| 27 | 41 | |
| 28 | export const make = (): DesktopNetworkInterfaces["Service"] => | |
| 29 | DesktopNetworkInterfaces.of({ | |
| 30 | read: Effect.sync(() => NodeOS.networkInterfaces()), | |
| 42 | export const make = Effect.gen(function* () { | |
| 43 | const platform = yield* HostProcessPlatform; | |
| 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 | }); | |
| 32 | 51 | |
| 33 | export const layer = Layer.succeed(DesktopNetworkInterfaces, make()); | |
| 52 | export const layer = Layer.effect(DesktopNetworkInterfaces, make); |