Unified · Split
| @@ -1,6 +1,7 @@ | |||
| 1 | import { assert, describe, it } from "@effect/vitest"; | 1 | import { assert, describe, it } from "@effect/vitest"; |
| 2 | import * as Effect from "effect/Effect"; | 2 | import * as Effect from "effect/Effect"; |
| 3 | import * as Layer from "effect/Layer"; | 3 | import * as Layer from "effect/Layer"; |
| 4 | import * as Option from "effect/Option"; | ||
| 4 | import { HttpClient } from "effect/unstable/http"; | 5 | import { HttpClient } from "effect/unstable/http"; |
| 5 | import { ChildProcessSpawner } from "effect/unstable/process"; | 6 | import { ChildProcessSpawner } from "effect/unstable/process"; |
| 6 | 7 | ||
| @@ -34,8 +35,8 @@ describe("tailscale endpoint provider", () => { · 27 unmodified lines | |||
| 34 | const dnsName = yield* parseTailscaleMagicDnsName( | 35 | const dnsName = yield* parseTailscaleMagicDnsName( |
| 35 | `{"Self":{"DNSName":"desktop.tail.ts.net."}}`, | 36 | `{"Self":{"DNSName":"desktop.tail.ts.net."}}`, |
| 36 | ); | 37 | ); |
| 37 | assert.equal(dnsName, "desktop.tail.ts.net"); | 38 | assert.deepEqual(dnsName, Option.some("desktop.tail.ts.net")); |
| 38 | assert.equal(yield* parseTailscaleMagicDnsName("{}"), null); | 39 | assert.deepEqual(yield* parseTailscaleMagicDnsName("{}"), Option.none()); |
| 39 | const malformed = yield* Effect.result(parseTailscaleMagicDnsName("not-json")); | 40 | const malformed = yield* Effect.result(parseTailscaleMagicDnsName("not-json")); |
| 40 | assert.isTrue(malformed._tag === "Failure"); | 41 | assert.isTrue(malformed._tag === "Failure"); |
| 41 | }), | 42 | }), |
| @@ -61,17 +61,17 @@ function resolveTailscaleIpAdvertisedEndpoints(input: { · 60 unmodified lines | |||
| 61 | const resolveTailscaleMagicDnsAdvertisedEndpoint = Effect.fn( | 61 | const resolveTailscaleMagicDnsAdvertisedEndpoint = Effect.fn( |
| 62 | "resolveTailscaleMagicDnsAdvertisedEndpoint", | 62 | "resolveTailscaleMagicDnsAdvertisedEndpoint", |
| 63 | )(function* (input: { | 63 | )(function* (input: { |
| 64 | readonly dnsName: string | null; | 64 | readonly dnsName: Option.Option<string>; |
| 65 | readonly serveEnabled: boolean; | 65 | readonly serveEnabled: boolean; |
| 66 | readonly servePort?: number; | 66 | readonly servePort?: number; |
| 67 | readonly probe?: (baseUrl: string) => Effect.Effect<boolean, never, HttpClient.HttpClient>; | 67 | readonly probe?: (baseUrl: string) => Effect.Effect<boolean, never, HttpClient.HttpClient>; |
| 68 | }): Effect.fn.Return<Option.Option<AdvertisedEndpoint>, never, HttpClient.HttpClient> { | 68 | }): Effect.fn.Return<Option.Option<AdvertisedEndpoint>, never, HttpClient.HttpClient> { |
| 69 | if (!input.dnsName) { | 69 | if (Option.isNone(input.dnsName)) { |
| 70 | return Option.none(); | 70 | return Option.none(); |
| 71 | } | 71 | } |
| 72 | 72 | ||
| 73 | const httpBaseUrl = buildTailscaleHttpsBaseUrl({ | 73 | const httpBaseUrl = buildTailscaleHttpsBaseUrl({ |
| 74 | magicDnsName: input.dnsName, | 74 | magicDnsName: input.dnsName.value, |
| 75 | ...(input.servePort === undefined ? {} : { servePort: input.servePort }), | 75 | ...(input.servePort === undefined ? {} : { servePort: input.servePort }), |
| 76 | }); | 76 | }); |
| 77 | const probe = | 77 | const probe = |
| @@ -116,13 +116,13 @@ export const resolveTailscaleAdvertisedEndpoints = Effect.fn("resolveTailscaleAd · 38 unmodified lines | |||
| 116 | input.statusJson === undefined | 116 | input.statusJson === undefined |
| 117 | ? yield* readTailscaleStatus.pipe( | 117 | ? yield* readTailscaleStatus.pipe( |
| 118 | Effect.map((status) => status.magicDnsName), | 118 | Effect.map((status) => status.magicDnsName), |
| 119 | Effect.catch(() => Effect.succeed(null)), | 119 | Effect.catch(() => Effect.succeed(Option.none())), |
| 120 | ) | 120 | ) |
| 121 | : input.statusJson | 121 | : input.statusJson |
| 122 | ? yield* parseTailscaleMagicDnsName(input.statusJson).pipe( | 122 | ? yield* parseTailscaleMagicDnsName(input.statusJson).pipe( |
| 123 | Effect.catch(() => Effect.succeed(null)), | 123 | Effect.catch(() => Effect.succeed(Option.none())), |
| 124 | ) | 124 | ) |
| 125 | : null; | 125 | : Option.none(); |
| 126 | const magicDnsEndpoint = yield* resolveTailscaleMagicDnsAdvertisedEndpoint({ | 126 | const magicDnsEndpoint = yield* resolveTailscaleMagicDnsAdvertisedEndpoint({ |
| 127 | dnsName, | 127 | dnsName, |
| 128 | serveEnabled: input.serveEnabled === true, | 128 | serveEnabled: input.serveEnabled === true, |
| @@ -1,8 +1,12 @@ | |||
| 1 | import { assert, describe, it } from "@effect/vitest"; | 1 | import { assert, describe, it } from "@effect/vitest"; |
| 2 | import * as Duration from "effect/Duration"; | ||
| 2 | import * as Effect from "effect/Effect"; | 3 | import * as Effect from "effect/Effect"; |
| 4 | import * as Fiber from "effect/Fiber"; | ||
| 3 | import * as Layer from "effect/Layer"; | 5 | import * as Layer from "effect/Layer"; |
| 6 | import * as Option from "effect/Option"; | ||
| 4 | import * as Sink from "effect/Sink"; | 7 | import * as Sink from "effect/Sink"; |
| 5 | import * as Stream from "effect/Stream"; | 8 | import * as Stream from "effect/Stream"; |
| 9 | import * as TestClock from "effect/testing/TestClock"; | ||
| 6 | import { ChildProcessSpawner } from "effect/unstable/process"; | 10 | import { ChildProcessSpawner } from "effect/unstable/process"; |
| 7 | 11 | ||
| 8 | import { | 12 | import { |
| @@ -13,6 +17,7 @@ import { · 4 unmodified lines | |||
| 13 | parseTailscaleMagicDnsName, | 17 | parseTailscaleMagicDnsName, |
| 14 | parseTailscaleStatus, | 18 | parseTailscaleStatus, |
| 15 | readTailscaleStatus, | 19 | readTailscaleStatus, |
| 20 | TAILSCALE_STATUS_TIMEOUT, | ||
| 16 | } from "./tailscale.ts"; | 21 | } from "./tailscale.ts"; |
| 17 | 22 | ||
| 18 | const encoder = new TextEncoder(); | 23 | const encoder = new TextEncoder(); |
| @@ -35,22 +40,37 @@ function mockHandle(result: { stdout?: string; stderr?: string; code?: number }) · 16 unmodified lines | |||
| 35 | }); | 40 | }); |
| 36 | } | 41 | } |
| 37 | 42 | ||
| 43 | function mockHangingHandle() { | ||
| 44 | return ChildProcessSpawner.makeHandle({ | ||
| 45 | pid: ChildProcessSpawner.ProcessId(1), | ||
| 46 | exitCode: Effect.never, | ||
| 47 | isRunning: Effect.succeed(true), | ||
| 48 | kill: () => Effect.void, | ||
| 49 | unref: Effect.succeed(Effect.void), | ||
| 50 | stdin: Sink.drain, | ||
| 51 | stdout: Stream.empty, | ||
| 52 | stderr: Stream.empty, | ||
| 53 | all: Stream.empty, | ||
| 54 | getInputFd: () => Sink.drain, | ||
| 55 | getOutputFd: () => Stream.empty, | ||
| 56 | }); | ||
| 57 | } | ||
| 58 | |||
| 38 | function mockSpawnerLayer( | 59 | function mockSpawnerLayer( |
| 39 | handler: ( | 60 | handler: ( |
| 40 | command: string, | 61 | command: string, |
| 41 | args: ReadonlyArray<string>, | 62 | args: ReadonlyArray<string>, |
| 42 | ) => { stdout?: string; stderr?: string; code?: number }, | 63 | ) => { stdout?: string; stderr?: string; code?: number }, |
| 43 | ) { | 64 | ) { |
| 44 | return Layer.succeed( | 65 | return Layer.mock(ChildProcessSpawner.ChildProcessSpawner, { |
| 45 | ChildProcessSpawner.ChildProcessSpawner, | 66 | spawn: (command) => { |
| 46 | ChildProcessSpawner.make((command) => { | ||
| 47 | const childProcess = command as unknown as { | 67 | const childProcess = command as unknown as { |
| 48 | readonly command: string; | 68 | readonly command: string; |
| 49 | readonly args: ReadonlyArray<string>; | 69 | readonly args: ReadonlyArray<string>; |
| 50 | }; | 70 | }; |
| 51 | return Effect.succeed(mockHandle(handler(childProcess.command, childProcess.args))); | 71 | return Effect.succeed(mockHandle(handler(childProcess.command, childProcess.args))); |
| 52 | }), | 72 | }, |
| 53 | ); | 73 | }); |
| 54 | } | 74 | } |
| 55 | 75 | ||
| 56 | describe("tailscale", () => { | 76 | describe("tailscale", () => { |
| @@ -66,8 +86,8 @@ describe("tailscale", () => { · 9 unmodified lines | |||
| 66 | it.effect("parses MagicDNS names from tailscale status", () => | 86 | it.effect("parses MagicDNS names from tailscale status", () => |
| 67 | Effect.gen(function* () { | 87 | Effect.gen(function* () { |
| 68 | const dnsName = yield* parseTailscaleMagicDnsName(tailscaleStatusJson); | 88 | const dnsName = yield* parseTailscaleMagicDnsName(tailscaleStatusJson); |
| 69 | assert.equal(dnsName, "desktop.tail.ts.net"); | 89 | assert.deepEqual(dnsName, Option.some("desktop.tail.ts.net")); |
| 70 | assert.equal(yield* parseTailscaleMagicDnsName("{}"), null); | 90 | assert.deepEqual(yield* parseTailscaleMagicDnsName("{}"), Option.none()); |
| 71 | }), | 91 | }), |
| 72 | ); | 92 | ); |
| 73 | 93 | ||
| @@ -75,7 +95,7 @@ describe("tailscale", () => { · 1 unmodified line | |||
| 75 | Effect.gen(function* () { | 95 | Effect.gen(function* () { |
| 76 | const status = yield* parseTailscaleStatus(tailscaleStatusJson); | 96 | const status = yield* parseTailscaleStatus(tailscaleStatusJson); |
| 77 | assert.deepEqual(status, { | 97 | assert.deepEqual(status, { |
| 78 | magicDnsName: "desktop.tail.ts.net", | 98 | magicDnsName: Option.some("desktop.tail.ts.net"), |
| 79 | tailnetIpv4Addresses: ["100.100.100.100"], | 99 | tailnetIpv4Addresses: ["100.100.100.100"], |
| 80 | }); | 100 | }); |
| 81 | }), | 101 | }), |
| @@ -106,12 +126,31 @@ describe("tailscale", () => { · 24 unmodified lines | |||
| 106 | return Effect.gen(function* () { | 126 | return Effect.gen(function* () { |
| 107 | const status = yield* readTailscaleStatus.pipe(Effect.provide(layer)); | 127 | const status = yield* readTailscaleStatus.pipe(Effect.provide(layer)); |
| 108 | assert.deepEqual(status, { | 128 | assert.deepEqual(status, { |
| 109 | magicDnsName: "desktop.tail.ts.net", | 129 | magicDnsName: Option.some("desktop.tail.ts.net"), |
| 110 | tailnetIpv4Addresses: ["100.90.1.2"], | 130 | tailnetIpv4Addresses: ["100.90.1.2"], |
| 111 | }); | 131 | }); |
| 112 | }); | 132 | }); |
| 113 | }); | 133 | }); |
| 114 | 134 | ||
| 135 | it.effect("times out tailscale status with TestClock", () => { | ||
| 136 | const layer = Layer.mock(ChildProcessSpawner.ChildProcessSpawner, { | ||
| 137 | spawn: () => Effect.succeed(mockHangingHandle()), | ||
| 138 | }); | ||
| 139 | |||
| 140 | return Effect.gen(function* () { | ||
| 141 | const fiber = yield* readTailscaleStatus.pipe(Effect.provide(layer), Effect.result, Effect.fork); | ||
| 142 | yield* Effect.yieldNow; | ||
| 143 | yield* TestClock.adjust(Duration.sum(TAILSCALE_STATUS_TIMEOUT, Duration.millis(1))); | ||
| 144 | const result = yield* Fiber.join(fiber); | ||
| 145 | |||
| 146 | assert.equal(result._tag, "Failure"); | ||
| 147 | if (result._tag === "Failure") { | ||
| 148 | assert.equal(result.failure._tag, "TailscaleCommandError"); | ||
| 149 | assert.equal(result.failure.message, "Tailscale status timed out."); | ||
| 150 | } | ||
| 151 | }).pipe(Effect.provide(TestClock.layer())); | ||
| 152 | }); | ||
| 153 | |||
| 115 | it.effect("configures tailscale serve through the process spawner service", () => { | 154 | it.effect("configures tailscale serve through the process spawner service", () => { |
| 116 | const layer = mockSpawnerLayer((command, args) => { | 155 | const layer = mockSpawnerLayer((command, args) => { |
| 117 | assert.equal(command, "tailscale"); | 156 | assert.equal(command, "tailscale"); |
| @@ -1,4 +1,4 @@ | |||
| 1 | import * as Data from "effect/Data"; | 1 | import * as Duration from "effect/Duration"; |
| 2 | import * as Effect from "effect/Effect"; | 2 | import * as Effect from "effect/Effect"; |
| 3 | import * as Option from "effect/Option"; | 3 | import * as Option from "effect/Option"; |
| 4 | import * as Schema from "effect/Schema"; | 4 | import * as Schema from "effect/Schema"; |
| @@ -7,24 +7,33 @@ import { HttpClient, HttpClientRequest } from "effect/unstable/http"; · 2 unmodified lines | |||
| 7 | import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; | 7 | import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; |
| 8 | 8 | ||
| 9 | export const DEFAULT_TAILSCALE_SERVE_PORT = 443; | 9 | export const DEFAULT_TAILSCALE_SERVE_PORT = 443; |
| 10 | export const TAILSCALE_STATUS_TIMEOUT_MS = 1_500; | 10 | export const TAILSCALE_STATUS_TIMEOUT = Duration.millis(1_500); |
| 11 | export const TAILSCALE_SERVE_TIMEOUT_MS = 10_000; | 11 | export const TAILSCALE_SERVE_TIMEOUT = Duration.seconds(10); |
| 12 | export const TAILSCALE_PROBE_TIMEOUT_MS = 2_500; | 12 | export const TAILSCALE_PROBE_TIMEOUT = Duration.millis(2_500); |
| 13 | 13 | ||
| 14 | export class TailscaleCommandError extends Data.TaggedError("TailscaleCommandError")<{ | 14 | export class TailscaleCommandError extends Schema.TaggedErrorClass<TailscaleCommandError>()( |
| 15 | readonly command: readonly string[]; | 15 | "TailscaleCommandError", |
| 16 | readonly message: string; | 16 | { |
| 17 | readonly exitCode: number | null; | 17 | command: Schema.Array(Schema.String), |
| 18 | readonly stderr: string; | 18 | message: Schema.String, |
| 19 | }> {} | 19 | exitCode: Schema.Union([Schema.Number, Schema.Null]), |
| 20 | stderr: Schema.String, | ||
| 21 | }, | ||
| 22 | ) {} | ||
| 20 | 23 | ||
| 21 | export class TailscaleStatusParseError extends Data.TaggedError("TailscaleStatusParseError")<{ | 24 | export class TailscaleStatusParseError extends Schema.TaggedErrorClass<TailscaleStatusParseError>()( |
| 22 | readonly cause: unknown; | 25 | "TailscaleStatusParseError", |
| 23 | }> {} | 26 | { |
| 27 | cause: Schema.Defect, | ||
| 28 | }, | ||
| 29 | ) {} | ||
| 24 | 30 | ||
| 25 | export class TailscaleUnavailableError extends Data.TaggedError("TailscaleUnavailableError")<{ | 31 | export class TailscaleUnavailableError extends Schema.TaggedErrorClass<TailscaleUnavailableError>()( |
| 26 | readonly reason: string; | 32 | "TailscaleUnavailableError", |
| 27 | }> {} | 33 | { |
| 34 | reason: Schema.String, | ||
| 35 | }, | ||
| 36 | ) {} | ||
| 28 | 37 | ||
| 29 | const TailscaleStatusSelf = Schema.Struct({ | 38 | const TailscaleStatusSelf = Schema.Struct({ |
| 30 | DNSName: Schema.optional(Schema.Unknown), | 39 | DNSName: Schema.optional(Schema.Unknown), |
| @@ -39,7 +48,7 @@ export type TailscaleStatusSelf = typeof TailscaleStatusSelf.Type; · 8 unmodified lines | |||
| 39 | export type TailscaleStatusJson = typeof TailscaleStatusJson.Type; | 48 | export type TailscaleStatusJson = typeof TailscaleStatusJson.Type; |
| 40 | 49 | ||
| 41 | export interface TailscaleStatus { | 50 | export interface TailscaleStatus { |
| 42 | readonly magicDnsName: string | null; | 51 | readonly magicDnsName: Option.Option<string>; |
| 43 | readonly tailnetIpv4Addresses: readonly string[]; | 52 | readonly tailnetIpv4Addresses: readonly string[]; |
| 44 | } | 53 | } |
| 45 | 54 | ||
| @@ -69,19 +78,19 @@ const tailscaleCommandError = ( · 23 unmodified lines | |||
| 69 | 78 | ||
| 70 | const decodeTailscaleStatusJson = Schema.decodeEffect(Schema.fromJsonString(TailscaleStatusJson)); | 79 | const decodeTailscaleStatusJson = Schema.decodeEffect(Schema.fromJsonString(TailscaleStatusJson)); |
| 71 | 80 | ||
| 72 | function normalizeMagicDnsName(status: TailscaleStatusJson): string | null { | 81 | function normalizeMagicDnsName(status: TailscaleStatusJson): Option.Option<string> { |
| 73 | const dnsName = status.Self?.DNSName; | 82 | const dnsName = status.Self?.DNSName; |
| 74 | if (typeof dnsName !== "string") { | 83 | if (typeof dnsName !== "string") { |
| 75 | return null; | 84 | return Option.none(); |
| 76 | } | 85 | } |
| 77 | 86 | ||
| 78 | const normalized = dnsName.trim().replace(/\.$/u, ""); | 87 | const normalized = dnsName.trim().replace(/\.$/u, ""); |
| 79 | return normalized.length > 0 ? normalized : null; | 88 | return normalized.length > 0 ? Option.some(normalized) : Option.none(); |
| 80 | } | 89 | } |
| 81 | 90 | ||
| 82 | export const parseTailscaleMagicDnsName = ( | 91 | export const parseTailscaleMagicDnsName = ( |
| 83 | rawStatusJson: string, | 92 | rawStatusJson: string, |
| 84 | ): Effect.Effect<string | null, TailscaleStatusParseError> => | 93 | ): Effect.Effect<Option.Option<string>, TailscaleStatusParseError> => |
| 85 | decodeTailscaleStatusJson(rawStatusJson).pipe( | 94 | decodeTailscaleStatusJson(rawStatusJson).pipe( |
| 86 | Effect.mapError((cause) => new TailscaleStatusParseError({ cause })), | 95 | Effect.mapError((cause) => new TailscaleStatusParseError({ cause })), |
| 87 | Effect.map(normalizeMagicDnsName), | 96 | Effect.map(normalizeMagicDnsName), |
| @@ -177,7 +186,7 @@ export const readTailscaleStatus: Effect.Effect< · 89 unmodified lines | |||
| 177 | return yield* parseTailscaleStatus(stdout); | 186 | return yield* parseTailscaleStatus(stdout); |
| 178 | }).pipe( | 187 | }).pipe( |
| 179 | Effect.scoped, | 188 | Effect.scoped, |
| 180 | Effect.timeoutOption(TAILSCALE_STATUS_TIMEOUT_MS), | 189 | Effect.timeoutOption(TAILSCALE_STATUS_TIMEOUT), |
| 181 | Effect.flatMap((result) => | 190 | Effect.flatMap((result) => |
| 182 | Option.match(result, { | 191 | Option.match(result, { |
| 183 | onNone: () => | 192 | onNone: () => |
| @@ -209,7 +218,7 @@ const runTailscaleCommand = ( · 25 unmodified lines | |||
| 209 | readonly runMessage: string; | 218 | readonly runMessage: string; |
| 210 | readonly exitMessage: (exitCode: number) => string; | 219 | readonly exitMessage: (exitCode: number) => string; |
| 211 | readonly timeoutMessage: string; | 220 | readonly timeoutMessage: string; |
| 212 | readonly timeoutMs: number; | 221 | readonly timeout: Duration.Input; |
| 213 | }, | 222 | }, |
| 214 | ): Effect.Effect<void, TailscaleCommandError, ChildProcessSpawner.ChildProcessSpawner> => | 223 | ): Effect.Effect<void, TailscaleCommandError, ChildProcessSpawner.ChildProcessSpawner> => |
| 215 | Effect.gen(function* () { | 224 | Effect.gen(function* () { |
| @@ -246,7 +255,7 @@ const runTailscaleCommand = ( · 30 unmodified lines | |||
| 246 | } | 255 | } |
| 247 | }).pipe( | 256 | }).pipe( |
| 248 | Effect.scoped, | 257 | Effect.scoped, |
| 249 | Effect.timeoutOption(input.timeoutMs), | 258 | Effect.timeoutOption(input.timeout), |
| 250 | Effect.flatMap((result) => | 259 | Effect.flatMap((result) => |
| 251 | Option.match(result, { | 260 | Option.match(result, { |
| 252 | onNone: () => Effect.fail(tailscaleCommandError(args, input.timeoutMessage, null)), | 261 | onNone: () => Effect.fail(tailscaleCommandError(args, input.timeoutMessage, null)), |
| @@ -268,7 +277,7 @@ export const ensureTailscaleServe = (input: { · 15 unmodified lines | |||
| 268 | runMessage: "Failed to run tailscale serve.", | 277 | runMessage: "Failed to run tailscale serve.", |
| 269 | exitMessage: (exitCode) => `Tailscale serve exited with code ${exitCode}.`, | 278 | exitMessage: (exitCode) => `Tailscale serve exited with code ${exitCode}.`, |
| 270 | timeoutMessage: "Tailscale serve timed out.", | 279 | timeoutMessage: "Tailscale serve timed out.", |
| 271 | timeoutMs: TAILSCALE_SERVE_TIMEOUT_MS, | 280 | timeout: TAILSCALE_SERVE_TIMEOUT, |
| 272 | }); | 281 | }); |
| 273 | }; | 282 | }; |
| 274 | 283 | ||
| @@ -284,13 +293,13 @@ export const disableTailscaleServe = ( · 9 unmodified lines | |||
| 284 | runMessage: "Failed to run tailscale serve off.", | 293 | runMessage: "Failed to run tailscale serve off.", |
| 285 | exitMessage: (exitCode) => `Tailscale serve off exited with code ${exitCode}.`, | 294 | exitMessage: (exitCode) => `Tailscale serve off exited with code ${exitCode}.`, |
| 286 | timeoutMessage: "Tailscale serve off timed out.", | 295 | timeoutMessage: "Tailscale serve off timed out.", |
| 287 | timeoutMs: TAILSCALE_SERVE_TIMEOUT_MS, | 296 | timeout: TAILSCALE_SERVE_TIMEOUT, |
| 288 | }); | 297 | }); |
| 289 | }); | 298 | }); |
| 290 | 299 | ||
| 291 | export const probeTailscaleHttpsEndpoint = (input: { | 300 | export const probeTailscaleHttpsEndpoint = (input: { |
| 292 | readonly baseUrl: string; | 301 | readonly baseUrl: string; |
| 293 | readonly timeoutMs?: number; | 302 | readonly timeout?: Duration.Input; |
| 294 | }): Effect.Effect<boolean, never, HttpClient.HttpClient> => | 303 | }): Effect.Effect<boolean, never, HttpClient.HttpClient> => |
| 295 | Effect.gen(function* () { | 304 | Effect.gen(function* () { |
| 296 | const client = yield* HttpClient.HttpClient; | 305 | const client = yield* HttpClient.HttpClient; |
| @@ -298,7 +307,7 @@ export const probeTailscaleHttpsEndpoint = (input: { · 1 unmodified line | |||
| 298 | const url = new URL("/.well-known/t3/environment", input.baseUrl); | 307 | const url = new URL("/.well-known/t3/environment", input.baseUrl); |
| 299 | const request = HttpClientRequest.get(url.toString()); | 308 | const request = HttpClientRequest.get(url.toString()); |
| 300 | return yield* client.execute(request); | 309 | return yield* client.execute(request); |
| 301 | }).pipe(Effect.timeoutOption(input.timeoutMs ?? TAILSCALE_PROBE_TIMEOUT_MS)); | 310 | }).pipe(Effect.timeoutOption(input.timeout ?? TAILSCALE_PROBE_TIMEOUT)); |
| 302 | 311 | ||
| 303 | return Option.match(response, { | 312 | return Option.match(response, { |
| 304 | onNone: () => false, | 313 | onNone: () => false, |
| @@ -311,17 +320,17 @@ export const resolveTailscaleHttpsBaseUrl = ( · 6 unmodified lines | |||
| 311 | readonly servePort?: number; | 320 | readonly servePort?: number; |
| 312 | } = {}, | 321 | } = {}, |
| 313 | ): Effect.Effect< | 322 | ): Effect.Effect< |
| 314 | string | null, | 323 | Option.Option<string>, |
| 315 | TailscaleCommandError | TailscaleStatusParseError, | 324 | TailscaleCommandError | TailscaleStatusParseError, |
| 316 | ChildProcessSpawner.ChildProcessSpawner | 325 | ChildProcessSpawner.ChildProcessSpawner |
| 317 | > => | 326 | > => |
| 318 | readTailscaleStatus.pipe( | 327 | readTailscaleStatus.pipe( |
| 319 | Effect.map((status) => | 328 | Effect.map((status) => |
| 320 | status.magicDnsName | 329 | Option.map(status.magicDnsName, (magicDnsName) => |
| 321 | ? buildTailscaleHttpsBaseUrl({ | 330 | buildTailscaleHttpsBaseUrl({ |
| 322 | magicDnsName: status.magicDnsName, | 331 | magicDnsName, |
| 323 | ...(input.servePort === undefined ? {} : { servePort: input.servePort }), | 332 | ...(input.servePort === undefined ? {} : { servePort: input.servePort }), |
| 324 | }) | 333 | }), |
| 325 | : null, | 334 | ), |
| 326 | ), | 335 | ), |
| 327 | ); | 336 | ); |