| author | |
| committer | |
| log | 2b8e012924063e185786c8ff120b6fd2886b3aa7 |
| tree | f6dc9f4632ba1dea180406ee0468ca8486f6329d |
| parent | 49c23221d80c68114fe4d88e92b05fa1129de0d3 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: codex <codex@users.noreply.github.com>2 files changed, 107 insertions(+), 19 deletions(-)
apps/desktop/src/backend/DesktopServerExposure.test.ts+65-1| ... | ... | @@ -91,6 +91,7 @@ function makeLayer(input: { |
| 91 | 91 | readonly networkInterfaces?: DesktopNetworkInterfaces.NetworkInterfaces; |
| 92 | 92 | readonly env?: Record<string, string | undefined>; |
| 93 | 93 | readonly spawnerLayer?: Layer.Layer<ChildProcessSpawner.ChildProcessSpawner>; |
| 94 | readonly desktopSettingsLayer?: Layer.Layer<DesktopAppSettings.DesktopAppSettings>; | |
| 94 | 95 | }) { |
| 95 | 96 | const env = { T3CODE_HOME: input.baseDir, ...input.env }; |
| 96 | 97 | const environmentLayer = makeEnvironmentLayer(input.baseDir, env); |
| ... | ... | @@ -99,7 +100,7 @@ function makeLayer(input: { |
| 99 | 100 | }); |
| 100 | 101 | |
| 101 | 102 | return DesktopServerExposure.layer.pipe( |
| 102 | Layer.provideMerge(DesktopAppSettings.layer), | |
| 103 | Layer.provideMerge(input.desktopSettingsLayer ?? DesktopAppSettings.layer), | |
| 103 | 104 | Layer.provideMerge(NodeFileSystem.layer), |
| 104 | 105 | Layer.provideMerge(NodeHttpClient.layerUndici), |
| 105 | 106 | Layer.provideMerge(input.spawnerLayer ?? mockSpawnerLayer()), |
| ... | ... | @@ -122,6 +123,7 @@ const withHarness = <A, E, R>( |
| 122 | 123 | >, |
| 123 | 124 | env: Record<string, string | undefined> = {}, |
| 124 | 125 | spawnerLayer?: Layer.Layer<ChildProcessSpawner.ChildProcessSpawner>, |
| 126 | desktopSettingsLayer?: Layer.Layer<DesktopAppSettings.DesktopAppSettings>, | |
| 125 | 127 | ) => |
| 126 | 128 | Effect.gen(function* () { |
| 127 | 129 | const fileSystem = yield* FileSystem.FileSystem; |
| ... | ... | @@ -135,6 +137,7 @@ const withHarness = <A, E, R>( |
| 135 | 137 | networkInterfaces, |
| 136 | 138 | env, |
| 137 | 139 | ...(spawnerLayer ? { spawnerLayer } : {}), |
| 140 | ...(desktopSettingsLayer ? { desktopSettingsLayer } : {}), | |
| 138 | 141 | }), |
| 139 | 142 | ), |
| 140 | 143 | ); |
| ... | ... | @@ -237,6 +240,67 @@ describe("DesktopServerExposure", () => { |
| 237 | 240 | ), |
| 238 | 241 | ); |
| 239 | 242 | |
| 243 | it.effect("preserves persistence request context and the settings failure chain", () => { | |
| 244 | const diskFailure = new Error("disk exploded"); | |
| 245 | const settingsFailure = new DesktopAppSettings.DesktopSettingsWriteError({ | |
| 246 | operation: "replace-settings-file", | |
| 247 | path: "/tmp/desktop-settings.json", | |
| 248 | cause: diskFailure, | |
| 249 | }); | |
| 250 | const settingsLayer = Layer.succeed(DesktopAppSettings.DesktopAppSettings, { | |
| 251 | get: Effect.succeed(DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS), | |
| 252 | load: Effect.succeed(DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS), | |
| 253 | setServerExposureMode: () => Effect.fail(settingsFailure), | |
| 254 | setTailscaleServe: () => Effect.fail(settingsFailure), | |
| 255 | setUpdateChannel: () => Effect.die("unexpected update channel change"), | |
| 256 | } satisfies DesktopAppSettings.DesktopAppSettings["Service"]); | |
| 257 | ||
| 258 | return withHarness( | |
| 259 | lanNetworkInterfaces, | |
| 260 | Effect.gen(function* () { | |
| 261 | const serverExposure = yield* DesktopServerExposure.DesktopServerExposure; | |
| 262 | yield* serverExposure.configureFromSettings({ port: 4173 }); | |
| 263 | ||
| 264 | const modeError = yield* serverExposure.setMode("network-accessible").pipe(Effect.flip); | |
| 265 | assert.instanceOf( | |
| 266 | modeError, | |
| 267 | DesktopServerExposure.DesktopServerExposureModePersistenceError, | |
| 268 | ); | |
| 269 | assert.isTrue(DesktopServerExposure.isDesktopServerExposureSetModeError(modeError)); | |
| 270 | assert.isTrue(DesktopServerExposure.isDesktopServerExposureError(modeError)); | |
| 271 | assert.equal(modeError.mode, "network-accessible"); | |
| 272 | assert.strictEqual(modeError.cause, settingsFailure); | |
| 273 | assert.strictEqual(modeError.cause.cause, diskFailure); | |
| 274 | assert.equal( | |
| 275 | modeError.message, | |
| 276 | "Failed to persist desktop server exposure mode network-accessible.", | |
| 277 | ); | |
| 278 | assert.notInclude(modeError.message, diskFailure.message); | |
| 279 | ||
| 280 | const tailscaleError = yield* serverExposure | |
| 281 | .setTailscaleServeEnabled({ enabled: true, port: 8443 }) | |
| 282 | .pipe(Effect.flip); | |
| 283 | assert.instanceOf( | |
| 284 | tailscaleError, | |
| 285 | DesktopServerExposure.DesktopTailscaleServePersistenceError, | |
| 286 | ); | |
| 287 | assert.isTrue(DesktopServerExposure.isDesktopServerExposureError(tailscaleError)); | |
| 288 | assert.equal(tailscaleError.enabled, true); | |
| 289 | assert.equal(tailscaleError.port, 8443); | |
| 290 | assert.strictEqual(tailscaleError.cause, settingsFailure); | |
| 291 | assert.strictEqual(tailscaleError.cause.cause, diskFailure); | |
| 292 | assert.equal( | |
| 293 | tailscaleError.message, | |
| 294 | "Failed to persist desktop Tailscale Serve settings (enabled: true, port: 8443).", | |
| 295 | ); | |
| 296 | assert.notInclude(tailscaleError.message, diskFailure.message); | |
| 297 | }), | |
| 298 | {}, | |
| 299 | undefined, | |
| 300 | settingsLayer, | |
| 301 | ); | |
| 302 | }); | |
| 303 | ||
| 240 | 304 | it.effect("resolves advertised endpoints from the scoped runtime state", () => |
| 241 | 305 | withHarness( |
| 242 | 306 | { ...lanNetworkInterfaces, ...tailnetNetworkInterfaces }, |
apps/desktop/src/backend/DesktopServerExposure.ts+42-18| ... | ... | @@ -2,11 +2,12 @@ import { |
| 2 | 2 | createAdvertisedEndpoint, |
| 3 | 3 | type CreateAdvertisedEndpointInput, |
| 4 | 4 | } from "@t3tools/shared/advertisedEndpoint"; |
| 5 | import type { | |
| 6 | AdvertisedEndpoint, | |
| 7 | AdvertisedEndpointProvider, | |
| 8 | DesktopServerExposureMode, | |
| 9 | DesktopServerExposureState, | |
| 5 | import { | |
| 6 | DesktopServerExposureModeSchema, | |
| 7 | type AdvertisedEndpoint, | |
| 8 | type AdvertisedEndpointProvider, | |
| 9 | type DesktopServerExposureMode, | |
| 10 | type DesktopServerExposureState, | |
| 10 | 11 | } from "@t3tools/contracts"; |
| 11 | 12 | import { readTailscaleStatus } from "@t3tools/tailscale"; |
| 12 | 13 | import * as Context from "effect/Context"; |
| ... | ... | @@ -213,23 +214,45 @@ export class DesktopServerExposureNoNetworkAddressError extends Schema.TaggedErr |
| 213 | 214 | } |
| 214 | 215 | } |
| 215 | 216 | |
| 216 | export class DesktopServerExposurePersistenceError extends Schema.TaggedErrorClass<DesktopServerExposurePersistenceError>()( | |
| 217 | "DesktopServerExposurePersistenceError", | |
| 217 | export class DesktopServerExposureModePersistenceError extends Schema.TaggedErrorClass<DesktopServerExposureModePersistenceError>()( | |
| 218 | "DesktopServerExposureModePersistenceError", | |
| 218 | 219 | { |
| 219 | operation: Schema.Literals(["server-exposure-mode", "tailscale-serve"]), | |
| 220 | mode: DesktopServerExposureModeSchema, | |
| 220 | 221 | cause: Schema.instanceOf(DesktopAppSettings.DesktopSettingsWriteError), |
| 221 | 222 | }, |
| 222 | 223 | ) { |
| 223 | 224 | override get message(): string { |
| 224 | return `Failed to persist desktop ${this.operation} settings.`; | |
| 225 | return `Failed to persist desktop server exposure mode ${this.mode}.`; | |
| 225 | 226 | } |
| 226 | 227 | } |
| 227 | 228 | |
| 228 | export type DesktopServerExposureSetModeError = | |
| 229 | | DesktopServerExposureNoNetworkAddressError | |
| 230 | | DesktopServerExposurePersistenceError; | |
| 229 | export class DesktopTailscaleServePersistenceError extends Schema.TaggedErrorClass<DesktopTailscaleServePersistenceError>()( | |
| 230 | "DesktopTailscaleServePersistenceError", | |
| 231 | { | |
| 232 | enabled: Schema.Boolean, | |
| 233 | port: Schema.NullOr(Schema.Number), | |
| 234 | cause: Schema.instanceOf(DesktopAppSettings.DesktopSettingsWriteError), | |
| 235 | }, | |
| 236 | ) { | |
| 237 | override get message(): string { | |
| 238 | return `Failed to persist desktop Tailscale Serve settings (enabled: ${this.enabled}, port: ${this.port ?? "unchanged"}).`; | |
| 239 | } | |
| 240 | } | |
| 231 | 241 | |
| 232 | export type DesktopServerExposureError = DesktopServerExposureSetModeError; | |
| 242 | export const DesktopServerExposureSetModeError = Schema.Union([ | |
| 243 | DesktopServerExposureNoNetworkAddressError, | |
| 244 | DesktopServerExposureModePersistenceError, | |
| 245 | ]); | |
| 246 | export type DesktopServerExposureSetModeError = typeof DesktopServerExposureSetModeError.Type; | |
| 247 | export const isDesktopServerExposureSetModeError = Schema.is(DesktopServerExposureSetModeError); | |
| 248 | ||
| 249 | export const DesktopServerExposureError = Schema.Union([ | |
| 250 | DesktopServerExposureNoNetworkAddressError, | |
| 251 | DesktopServerExposureModePersistenceError, | |
| 252 | DesktopTailscaleServePersistenceError, | |
| 253 | ]); | |
| 254 | export type DesktopServerExposureError = typeof DesktopServerExposureError.Type; | |
| 255 | export const isDesktopServerExposureError = Schema.is(DesktopServerExposureError); | |
| 233 | 256 | |
| 234 | 257 | export interface DesktopServerExposureBackendConfig { |
| 235 | 258 | readonly port: number; |
| ... | ... | @@ -258,7 +281,7 @@ export class DesktopServerExposure extends Context.Service< |
| 258 | 281 | readonly setTailscaleServeEnabled: (input: { |
| 259 | 282 | readonly enabled: boolean; |
| 260 | 283 | readonly port?: number; |
| 261 | }) => Effect.Effect<DesktopServerExposureChange, DesktopServerExposurePersistenceError>; | |
| 284 | }) => Effect.Effect<DesktopServerExposureChange, DesktopTailscaleServePersistenceError>; | |
| 262 | 285 | readonly getAdvertisedEndpoints: Effect.Effect<readonly AdvertisedEndpoint[]>; |
| 263 | 286 | } |
| 264 | 287 | >()("@t3tools/desktop/backend/DesktopServerExposure") {} |
| ... | ... | @@ -449,8 +472,8 @@ export const make = Effect.gen(function* () { |
| 449 | 472 | const change = yield* desktopSettings.setServerExposureMode(mode).pipe( |
| 450 | 473 | Effect.mapError( |
| 451 | 474 | (cause) => |
| 452 | new DesktopServerExposurePersistenceError({ | |
| 453 | operation: "server-exposure-mode", | |
| 475 | new DesktopServerExposureModePersistenceError({ | |
| 476 | mode, | |
| 454 | 477 | cause, |
| 455 | 478 | }), |
| 456 | 479 | ), |
| ... | ... | @@ -477,8 +500,9 @@ export const make = Effect.gen(function* () { |
| 477 | 500 | .pipe( |
| 478 | 501 | Effect.mapError( |
| 479 | 502 | (cause) => |
| 480 | new DesktopServerExposurePersistenceError({ | |
| 481 | operation: "tailscale-serve", | |
| 503 | new DesktopTailscaleServePersistenceError({ | |
| 504 | enabled: input.enabled, | |
| 505 | port: input.port ?? null, | |
| 482 | 506 | cause, |
| 483 | 507 | }), |
| 484 | 508 | ), |