| 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,8 +1,10 @@ |
| 1 | import * as NodeOS from "node:os"; | 1 | import * as NodeOS from "node:os"; |
| 2 | 2 | ||
| 3 | import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; | ||
| 3 | import * as Context from "effect/Context"; | 4 | import * as Context from "effect/Context"; |
| 4 | import * as Effect from "effect/Effect"; | 5 | import * as Effect from "effect/Effect"; |
| 5 | import * as Layer from "effect/Layer"; | 6 | import * as Layer from "effect/Layer"; |
| 7 | import * as Schema from "effect/Schema"; | ||
| 6 | 8 | ||
| 7 | export interface DesktopNetworkInterfaceInfo { | 9 | export 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 | >; |
| 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 | export class DesktopNetworkInterfaces extends Context.Service< | 35 | export 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") {} |
| 27 | 41 | ||
| 28 | export const make = (): DesktopNetworkInterfaces["Service"] => | 42 | export 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 | }); | ||
| 32 | 51 | ||
| 33 | export const layer = Layer.succeed(DesktopNetworkInterfaces, make()); | 52 | export const layer = Layer.effect(DesktopNetworkInterfaces, make); |