| author | |
| committer | |
| log | a4446e263d77bd5c32ce6bb4921d6464000765de |
| tree | e43bf38bf4a5de0b88162f68b559fb367f7daa93 |
| parent | 494350cc0b8b85702d1207cd96912c8325588a63 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Julius Marminge <juliusmarminge@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>4 files changed, 115 insertions(+), 39 deletions(-)
apps/server/src/vcs/VcsProjectConfig.test.ts+43| ... | @@ -67,4 +67,47 @@ describe("VcsProjectConfig", () => { | ... | @@ -67,4 +67,47 @@ describe("VcsProjectConfig", () => { |
| 67 | }), | 67 | }), |
| 68 | ); | 68 | ); |
| 69 | }); | 69 | }); |
| 70 | |||
| 71 | it.layer(TestLayer)("falls back to auto when config JSON is malformed", (it) => { | ||
| 72 | it.effect("returns auto", () => | ||
| 73 | Effect.gen(function* () { | ||
| 74 | const fileSystem = yield* FileSystem.FileSystem; | ||
| 75 | const path = yield* Path.Path; | ||
| 76 | const root = yield* fileSystem.makeTempDirectoryScoped({ | ||
| 77 | prefix: "t3-vcs-config-test-", | ||
| 78 | }); | ||
| 79 | const configDir = path.join(root, ".t3code"); | ||
| 80 | yield* fileSystem.makeDirectory(configDir, { recursive: true }); | ||
| 81 | yield* fileSystem.writeFileString(path.join(configDir, "vcs.json"), "{not json"); | ||
| 82 | |||
| 83 | const config = yield* VcsProjectConfig.VcsProjectConfig; | ||
| 84 | const kind = yield* config.resolveKind({ cwd: root }); | ||
| 85 | |||
| 86 | assert.equal(kind, "auto"); | ||
| 87 | }), | ||
| 88 | ); | ||
| 89 | }); | ||
| 90 | |||
| 91 | it.layer(TestLayer)("falls back to auto when config kind is invalid", (it) => { | ||
| 92 | it.effect("returns auto", () => | ||
| 93 | Effect.gen(function* () { | ||
| 94 | const fileSystem = yield* FileSystem.FileSystem; | ||
| 95 | const path = yield* Path.Path; | ||
| 96 | const root = yield* fileSystem.makeTempDirectoryScoped({ | ||
| 97 | prefix: "t3-vcs-config-test-", | ||
| 98 | }); | ||
| 99 | const configDir = path.join(root, ".t3code"); | ||
| 100 | yield* fileSystem.makeDirectory(configDir, { recursive: true }); | ||
| 101 | yield* fileSystem.writeFileString( | ||
| 102 | path.join(configDir, "vcs.json"), | ||
| 103 | `{"vcs":{"kind":"svn"}}`, | ||
| 104 | ); | ||
| 105 | |||
| 106 | const config = yield* VcsProjectConfig.VcsProjectConfig; | ||
| 107 | const kind = yield* config.resolveKind({ cwd: root }); | ||
| 108 | |||
| 109 | assert.equal(kind, "auto"); | ||
| 110 | }), | ||
| 111 | ); | ||
| 112 | }); | ||
| 70 | }); | 113 | }); |
apps/server/src/vcs/VcsProjectConfig.ts+19-29| ... | @@ -2,10 +2,12 @@ import * as Context from "effect/Context"; | ... | @@ -2,10 +2,12 @@ import * as Context from "effect/Context"; |
| 2 | import * as Effect from "effect/Effect"; | 2 | import * as Effect from "effect/Effect"; |
| 3 | import * as FileSystem from "effect/FileSystem"; | 3 | import * as FileSystem from "effect/FileSystem"; |
| 4 | import * as Layer from "effect/Layer"; | 4 | import * as Layer from "effect/Layer"; |
| 5 | import * as Option from "effect/Option"; | ||
| 5 | import * as Path from "effect/Path"; | 6 | import * as Path from "effect/Path"; |
| 6 | import * as Schema from "effect/Schema"; | 7 | import * as Schema from "effect/Schema"; |
| 7 | 8 | ||
| 8 | import { VcsDriverKind, type VcsDriverKind as VcsDriverKindType } from "@t3tools/contracts"; | 9 | import { VcsDriverKind, type VcsDriverKind as VcsDriverKindType } from "@t3tools/contracts"; |
| 10 | import { fromLenientJson } from "@t3tools/shared/schemaJson"; | ||
| 9 | 11 | ||
| 10 | const ProjectVcsConfig = Schema.Struct({ | 12 | const ProjectVcsConfig = Schema.Struct({ |
| 11 | vcs: Schema.optional( | 13 | vcs: Schema.optional( |
| ... | @@ -15,16 +17,10 @@ const ProjectVcsConfig = Schema.Struct({ | ... | @@ -15,16 +17,10 @@ const ProjectVcsConfig = Schema.Struct({ |
| 15 | ), | 17 | ), |
| 16 | vcsKind: Schema.optional(VcsDriverKind), | 18 | vcsKind: Schema.optional(VcsDriverKind), |
| 17 | }); | 19 | }); |
| 18 | const isProjectVcsConfig = Schema.is(ProjectVcsConfig); | 20 | const ProjectVcsConfigJson = fromLenientJson(ProjectVcsConfig); |
| 21 | const decodeProjectVcsConfigJson = Schema.decodeUnknownOption(ProjectVcsConfigJson); | ||
| 19 | 22 | ||
| 20 | interface ProjectVcsConfigFile { | 23 | type ProjectVcsConfigFile = typeof ProjectVcsConfig.Type; |
| 21 | readonly vcs?: | ||
| 22 | | { | ||
| 23 | readonly kind?: VcsDriverKindType | undefined; | ||
| 24 | } | ||
| 25 | | undefined; | ||
| 26 | readonly vcsKind?: VcsDriverKindType | undefined; | ||
| 27 | } | ||
| 28 | 24 | ||
| 29 | export interface VcsProjectConfigResolveInput { | 25 | export interface VcsProjectConfigResolveInput { |
| 30 | readonly cwd: string; | 26 | readonly cwd: string; |
| ... | @@ -45,14 +41,8 @@ function configuredKind(config: ProjectVcsConfigFile): VcsDriverKindType | "auto | ... | @@ -45,14 +41,8 @@ function configuredKind(config: ProjectVcsConfigFile): VcsDriverKindType | "auto |
| 45 | return config.vcs?.kind ?? config.vcsKind ?? "auto"; | 41 | return config.vcs?.kind ?? config.vcsKind ?? "auto"; |
| 46 | } | 42 | } |
| 47 | 43 | ||
| 48 | function parseConfig(raw: string): ProjectVcsConfigFile | null { | 44 | const parseConfig = (raw: string): Option.Option<ProjectVcsConfigFile> => |
| 49 | try { | 45 | decodeProjectVcsConfigJson(raw); |
| 50 | const parsed = JSON.parse(raw) as unknown; | ||
| 51 | return isProjectVcsConfig(parsed) ? parsed : null; | ||
| 52 | } catch { | ||
| 53 | return null; | ||
| 54 | } | ||
| 55 | } | ||
| 56 | 46 | ||
| 57 | export const make = Effect.fn("makeVcsProjectConfig")(function* () { | 47 | export const make = Effect.fn("makeVcsProjectConfig")(function* () { |
| 58 | const fileSystem = yield* FileSystem.FileSystem; | 48 | const fileSystem = yield* FileSystem.FileSystem; |
| ... | @@ -63,12 +53,12 @@ export const make = Effect.fn("makeVcsProjectConfig")(function* () { | ... | @@ -63,12 +53,12 @@ export const make = Effect.fn("makeVcsProjectConfig")(function* () { |
| 63 | while (true) { | 53 | while (true) { |
| 64 | const candidate = path.join(current, ".t3code", "vcs.json"); | 54 | const candidate = path.join(current, ".t3code", "vcs.json"); |
| 65 | if (yield* fileSystem.exists(candidate).pipe(Effect.orElseSucceed(() => false))) { | 55 | if (yield* fileSystem.exists(candidate).pipe(Effect.orElseSucceed(() => false))) { |
| 66 | return candidate; | 56 | return Option.some(candidate); |
| 67 | } | 57 | } |
| 68 | 58 | ||
| 69 | const parent = path.dirname(current); | 59 | const parent = path.dirname(current); |
| 70 | if (parent === current) { | 60 | if (parent === current) { |
| 71 | return null; | 61 | return Option.none(); |
| 72 | } | 62 | } |
| 73 | current = parent; | 63 | current = parent; |
| 74 | } | 64 | } |
| ... | @@ -78,26 +68,27 @@ export const make = Effect.fn("makeVcsProjectConfig")(function* () { | ... | @@ -78,26 +68,27 @@ export const make = Effect.fn("makeVcsProjectConfig")(function* () { |
| 78 | configPath: string, | 68 | configPath: string, |
| 79 | ) { | 69 | ) { |
| 80 | const raw = yield* fileSystem.readFileString(configPath).pipe( | 70 | const raw = yield* fileSystem.readFileString(configPath).pipe( |
| 71 | Effect.map(Option.some), | ||
| 81 | Effect.catch((error) => | 72 | Effect.catch((error) => |
| 82 | Effect.logWarning("failed to read VCS project config", { | 73 | Effect.logWarning("failed to read VCS project config", { |
| 83 | configPath, | 74 | configPath, |
| 84 | error, | 75 | error, |
| 85 | }).pipe(Effect.as(null)), | 76 | }).pipe(Effect.as(Option.none())), |
| 86 | ), | 77 | ), |
| 87 | ); | 78 | ); |
| 88 | if (raw === null) { | 79 | if (Option.isNone(raw)) { |
| 89 | return "auto" as const; | 80 | return "auto" as const; |
| 90 | } | 81 | } |
| 91 | 82 | ||
| 92 | const parsed = parseConfig(raw); | 83 | const parsed = parseConfig(raw.value); |
| 93 | if (parsed === null) { | 84 | if (Option.isNone(parsed)) { |
| 94 | yield* Effect.logWarning("invalid VCS project config", { | 85 | yield* Effect.logWarning("invalid VCS project config", { |
| 95 | configPath, | 86 | configPath, |
| 96 | }); | 87 | }); |
| 97 | return "auto" as const; | 88 | return "auto" as const; |
| 98 | } | 89 | } |
| 99 | 90 | ||
| 100 | return configuredKind(parsed); | 91 | return configuredKind(parsed.value); |
| 101 | }); | 92 | }); |
| 102 | 93 | ||
| 103 | const resolveKind: VcsProjectConfigShape["resolveKind"] = Effect.fn( | 94 | const resolveKind: VcsProjectConfigShape["resolveKind"] = Effect.fn( |
| ... | @@ -108,11 +99,10 @@ export const make = Effect.fn("makeVcsProjectConfig")(function* () { | ... | @@ -108,11 +99,10 @@ export const make = Effect.fn("makeVcsProjectConfig")(function* () { |
| 108 | } | 99 | } |
| 109 | 100 | ||
| 110 | const configPath = yield* findConfigPath(input.cwd); | 101 | const configPath = yield* findConfigPath(input.cwd); |
| 111 | if (configPath === null) { | 102 | return yield* Option.match(configPath, { |
| 112 | return "auto"; | 103 | onNone: () => Effect.succeed("auto" as const), |
| 113 | } | 104 | onSome: readConfiguredKind, |
| 114 | 105 | }); | |
| 115 | return yield* readConfiguredKind(configPath); | ||
| 116 | }); | 106 | }); |
| 117 | 107 | ||
| 118 | return VcsProjectConfig.of({ | 108 | return VcsProjectConfig.of({ |
packages/tailscale/src/tailscale.test.ts+42| ... | @@ -1,8 +1,10 @@ | ... | @@ -1,8 +1,10 @@ |
| 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 Fiber from "effect/Fiber"; | ||
| 3 | import * as Layer from "effect/Layer"; | 4 | import * as Layer from "effect/Layer"; |
| 4 | import * as Sink from "effect/Sink"; | 5 | import * as Sink from "effect/Sink"; |
| 5 | import * as Stream from "effect/Stream"; | 6 | import * as Stream from "effect/Stream"; |
| 7 | import * as TestClock from "effect/testing/TestClock"; | ||
| 6 | import { ChildProcessSpawner } from "effect/unstable/process"; | 8 | import { ChildProcessSpawner } from "effect/unstable/process"; |
| 7 | 9 | ||
| 8 | import { | 10 | import { |
| ... | @@ -13,6 +15,7 @@ import { | ... | @@ -13,6 +15,7 @@ import { |
| 13 | parseTailscaleMagicDnsName, | 15 | parseTailscaleMagicDnsName, |
| 14 | parseTailscaleStatus, | 16 | parseTailscaleStatus, |
| 15 | readTailscaleStatus, | 17 | readTailscaleStatus, |
| 18 | TAILSCALE_STATUS_TIMEOUT, | ||
| 16 | } from "./tailscale.ts"; | 19 | } from "./tailscale.ts"; |
| 17 | 20 | ||
| 18 | const encoder = new TextEncoder(); | 21 | const encoder = new TextEncoder(); |
| ... | @@ -35,6 +38,22 @@ function mockHandle(result: { stdout?: string; stderr?: string; code?: number }) | ... | @@ -35,6 +38,22 @@ function mockHandle(result: { stdout?: string; stderr?: string; code?: number }) |
| 35 | }); | 38 | }); |
| 36 | } | 39 | } |
| 37 | 40 | ||
| 41 | function neverFinishingMockHandle() { | ||
| 42 | return ChildProcessSpawner.makeHandle({ | ||
| 43 | pid: ChildProcessSpawner.ProcessId(1), | ||
| 44 | exitCode: Effect.never, | ||
| 45 | isRunning: Effect.succeed(true), | ||
| 46 | kill: () => Effect.void, | ||
| 47 | unref: Effect.succeed(Effect.void), | ||
| 48 | stdin: Sink.drain, | ||
| 49 | stdout: Stream.empty, | ||
| 50 | stderr: Stream.empty, | ||
| 51 | all: Stream.empty, | ||
| 52 | getInputFd: () => Sink.drain, | ||
| 53 | getOutputFd: () => Stream.empty, | ||
| 54 | }); | ||
| 55 | } | ||
| 56 | |||
| 38 | function mockSpawnerLayer( | 57 | function mockSpawnerLayer( |
| 39 | handler: ( | 58 | handler: ( |
| 40 | command: string, | 59 | command: string, |
| ... | @@ -112,6 +131,29 @@ describe("tailscale", () => { | ... | @@ -112,6 +131,29 @@ describe("tailscale", () => { |
| 112 | }); | 131 | }); |
| 113 | }); | 132 | }); |
| 114 | 133 | ||
| 134 | it.effect("times out tailscale status through TestClock", () => { | ||
| 135 | const layer = Layer.merge( | ||
| 136 | TestClock.layer(), | ||
| 137 | Layer.succeed( | ||
| 138 | ChildProcessSpawner.ChildProcessSpawner, | ||
| 139 | ChildProcessSpawner.make(() => Effect.succeed(neverFinishingMockHandle())), | ||
| 140 | ), | ||
| 141 | ); | ||
| 142 | |||
| 143 | return Effect.gen(function* () { | ||
| 144 | const fiber = yield* readTailscaleStatus.pipe(Effect.flip, Effect.forkScoped); | ||
| 145 | yield* Effect.yieldNow; | ||
| 146 | yield* TestClock.adjust(TAILSCALE_STATUS_TIMEOUT); | ||
| 147 | const error = yield* Fiber.join(fiber); | ||
| 148 | |||
| 149 | if (error._tag !== "TailscaleCommandError") { | ||
| 150 | assert.fail(`Expected TailscaleCommandError, received ${error._tag}.`); | ||
| 151 | } | ||
| 152 | assert.equal(error.message, "Tailscale status timed out."); | ||
| 153 | assert.equal(error.exitCode, null); | ||
| 154 | }).pipe(Effect.provide(layer)); | ||
| 155 | }); | ||
| 156 | |||
| 115 | it.effect("configures tailscale serve through the process spawner service", () => { | 157 | it.effect("configures tailscale serve through the process spawner service", () => { |
| 116 | const layer = mockSpawnerLayer((command, args) => { | 158 | const layer = mockSpawnerLayer((command, args) => { |
| 117 | assert.equal(command, "tailscale"); | 159 | assert.equal(command, "tailscale"); |
packages/tailscale/src/tailscale.ts+11-10| ... | @@ -1,5 +1,6 @@ | ... | @@ -1,5 +1,6 @@ |
| 1 | import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; | 1 | import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; |
| 2 | import * as Data from "effect/Data"; | 2 | import * as Data from "effect/Data"; |
| 3 | import * as Duration from "effect/Duration"; | ||
| 3 | import * as Effect from "effect/Effect"; | 4 | import * as Effect from "effect/Effect"; |
| 4 | import * as Option from "effect/Option"; | 5 | import * as Option from "effect/Option"; |
| 5 | import * as Schema from "effect/Schema"; | 6 | import * as Schema from "effect/Schema"; |
| ... | @@ -8,9 +9,9 @@ import { HttpClient, HttpClientRequest } from "effect/unstable/http"; | ... | @@ -8,9 +9,9 @@ import { HttpClient, HttpClientRequest } from "effect/unstable/http"; |
| 8 | import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; | 9 | import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; |
| 9 | 10 | ||
| 10 | export const DEFAULT_TAILSCALE_SERVE_PORT = 443; | 11 | export const DEFAULT_TAILSCALE_SERVE_PORT = 443; |
| 11 | export const TAILSCALE_STATUS_TIMEOUT_MS = 1_500; | 12 | export const TAILSCALE_STATUS_TIMEOUT = Duration.millis(1_500); |
| 12 | export const TAILSCALE_SERVE_TIMEOUT_MS = 10_000; | 13 | export const TAILSCALE_SERVE_TIMEOUT = Duration.seconds(10); |
| 13 | export const TAILSCALE_PROBE_TIMEOUT_MS = 2_500; | 14 | export const TAILSCALE_PROBE_TIMEOUT = Duration.millis(2_500); |
| 14 | 15 | ||
| 15 | // tailscale is a real executable everywhere (`tailscale.exe` on Windows), so | 16 | // tailscale is a real executable everywhere (`tailscale.exe` on Windows), so |
| 16 | // it is always spawned directly rather than through cmd.exe shell mode. | 17 | // it is always spawned directly rather than through cmd.exe shell mode. |
| ... | @@ -180,7 +181,7 @@ export const readTailscaleStatus: Effect.Effect< | ... | @@ -180,7 +181,7 @@ export const readTailscaleStatus: Effect.Effect< |
| 180 | return yield* parseTailscaleStatus(stdout); | 181 | return yield* parseTailscaleStatus(stdout); |
| 181 | }).pipe( | 182 | }).pipe( |
| 182 | Effect.scoped, | 183 | Effect.scoped, |
| 183 | Effect.timeoutOption(TAILSCALE_STATUS_TIMEOUT_MS), | 184 | Effect.timeoutOption(TAILSCALE_STATUS_TIMEOUT), |
| 184 | Effect.flatMap((result) => | 185 | Effect.flatMap((result) => |
| 185 | Option.match(result, { | 186 | Option.match(result, { |
| 186 | onNone: () => | 187 | onNone: () => |
| ... | @@ -212,7 +213,7 @@ const runTailscaleCommand = ( | ... | @@ -212,7 +213,7 @@ const runTailscaleCommand = ( |
| 212 | readonly runMessage: string; | 213 | readonly runMessage: string; |
| 213 | readonly exitMessage: (exitCode: number) => string; | 214 | readonly exitMessage: (exitCode: number) => string; |
| 214 | readonly timeoutMessage: string; | 215 | readonly timeoutMessage: string; |
| 215 | readonly timeoutMs: number; | 216 | readonly timeout: Duration.Input; |
| 216 | }, | 217 | }, |
| 217 | ): Effect.Effect<void, TailscaleCommandError, ChildProcessSpawner.ChildProcessSpawner> => | 218 | ): Effect.Effect<void, TailscaleCommandError, ChildProcessSpawner.ChildProcessSpawner> => |
| 218 | Effect.gen(function* () { | 219 | Effect.gen(function* () { |
| ... | @@ -246,7 +247,7 @@ const runTailscaleCommand = ( | ... | @@ -246,7 +247,7 @@ const runTailscaleCommand = ( |
| 246 | } | 247 | } |
| 247 | }).pipe( | 248 | }).pipe( |
| 248 | Effect.scoped, | 249 | Effect.scoped, |
| 249 | Effect.timeoutOption(input.timeoutMs), | 250 | Effect.timeoutOption(input.timeout), |
| 250 | Effect.flatMap((result) => | 251 | Effect.flatMap((result) => |
| 251 | Option.match(result, { | 252 | Option.match(result, { |
| 252 | onNone: () => Effect.fail(tailscaleCommandError(args, input.timeoutMessage, null)), | 253 | onNone: () => Effect.fail(tailscaleCommandError(args, input.timeoutMessage, null)), |
| ... | @@ -268,7 +269,7 @@ export const ensureTailscaleServe = (input: { | ... | @@ -268,7 +269,7 @@ export const ensureTailscaleServe = (input: { |
| 268 | runMessage: "Failed to run tailscale serve.", | 269 | runMessage: "Failed to run tailscale serve.", |
| 269 | exitMessage: (exitCode) => `Tailscale serve exited with code ${exitCode}.`, | 270 | exitMessage: (exitCode) => `Tailscale serve exited with code ${exitCode}.`, |
| 270 | timeoutMessage: "Tailscale serve timed out.", | 271 | timeoutMessage: "Tailscale serve timed out.", |
| 271 | timeoutMs: TAILSCALE_SERVE_TIMEOUT_MS, | 272 | timeout: TAILSCALE_SERVE_TIMEOUT, |
| 272 | }); | 273 | }); |
| 273 | }; | 274 | }; |
| 274 | 275 | ||
| ... | @@ -284,13 +285,13 @@ export const disableTailscaleServe = ( | ... | @@ -284,13 +285,13 @@ export const disableTailscaleServe = ( |
| 284 | runMessage: "Failed to run tailscale serve off.", | 285 | runMessage: "Failed to run tailscale serve off.", |
| 285 | exitMessage: (exitCode) => `Tailscale serve off exited with code ${exitCode}.`, | 286 | exitMessage: (exitCode) => `Tailscale serve off exited with code ${exitCode}.`, |
| 286 | timeoutMessage: "Tailscale serve off timed out.", | 287 | timeoutMessage: "Tailscale serve off timed out.", |
| 287 | timeoutMs: TAILSCALE_SERVE_TIMEOUT_MS, | 288 | timeout: TAILSCALE_SERVE_TIMEOUT, |
| 288 | }); | 289 | }); |
| 289 | }); | 290 | }); |
| 290 | 291 | ||
| 291 | export const probeTailscaleHttpsEndpoint = (input: { | 292 | export const probeTailscaleHttpsEndpoint = (input: { |
| 292 | readonly baseUrl: string; | 293 | readonly baseUrl: string; |
| 293 | readonly timeoutMs?: number; | 294 | readonly timeout?: Duration.Input; |
| 294 | }): Effect.Effect<boolean, never, HttpClient.HttpClient> => | 295 | }): Effect.Effect<boolean, never, HttpClient.HttpClient> => |
| 295 | Effect.gen(function* () { | 296 | Effect.gen(function* () { |
| 296 | const client = yield* HttpClient.HttpClient; | 297 | const client = yield* HttpClient.HttpClient; |
| ... | @@ -298,7 +299,7 @@ export const probeTailscaleHttpsEndpoint = (input: { | ... | @@ -298,7 +299,7 @@ export const probeTailscaleHttpsEndpoint = (input: { |
| 298 | const url = new URL("/.well-known/t3/environment", input.baseUrl); | 299 | const url = new URL("/.well-known/t3/environment", input.baseUrl); |
| 299 | const request = HttpClientRequest.get(url.toString()); | 300 | const request = HttpClientRequest.get(url.toString()); |
| 300 | return yield* client.execute(request); | 301 | return yield* client.execute(request); |
| 301 | }).pipe(Effect.timeoutOption(input.timeoutMs ?? TAILSCALE_PROBE_TIMEOUT_MS)); | 302 | }).pipe(Effect.timeoutOption(input.timeout ?? TAILSCALE_PROBE_TIMEOUT)); |
| 302 | 303 | ||
| 303 | return Option.match(response, { | 304 | return Option.match(response, { |
| 304 | onNone: () => false, | 305 | onNone: () => false, |