diff --git a/packages/opencode/specs/effect-migration.md b/packages/opencode/specs/effect-migration.md index 4f195917fde..8e29e0d2d28 100644 --- a/packages/opencode/specs/effect-migration.md +++ b/packages/opencode/specs/effect-migration.md @@ -140,5 +140,5 @@ Still open and likely worth migrating: - [ ] `SessionCompaction` - [ ] `Provider` - [ ] `Project` -- [ ] `LSP` +- [x] `LSP` - [ ] `MCP` diff --git a/packages/opencode/src/effect/instances.ts b/packages/opencode/src/effect/instances.ts index 6fcfddb24f5..d197ba2c2a2 100644 --- a/packages/opencode/src/effect/instances.ts +++ b/packages/opencode/src/effect/instances.ts @@ -3,6 +3,7 @@ import { File } from "@/file/service" import { FileTime } from "@/file/time-service" import { FileWatcher } from "@/file/watcher" import { Format } from "@/format/service" +import { LSP } from "@/lsp" import { Permission } from "@/permission/service" import { Instance } from "@/project/instance" import { Vcs } from "@/project/vcs" @@ -26,6 +27,7 @@ export type InstanceServices = | File.Service | Skill.Service | Snapshot.Service + | LSP.Service // NOTE: LayerMap only passes the key (directory string) to lookup, but we need // the full instance context (directory, worktree, project). We read from the @@ -46,6 +48,7 @@ function lookup(_key: string) { File.layer, Skill.defaultLayer, Snapshot.defaultLayer, + LSP.layer, ).pipe(Layer.provide(ctx)) } diff --git a/packages/opencode/src/effect/runtime.ts b/packages/opencode/src/effect/runtime.ts index e6f1f326262..2cbc4b9e7e6 100644 --- a/packages/opencode/src/effect/runtime.ts +++ b/packages/opencode/src/effect/runtime.ts @@ -20,6 +20,10 @@ export function runPromiseInstance(effect: Effect.Effect(effect: Effect.Effect) { + return runtime.runSync(effect.pipe(Effect.provide(Instances.get(Instance.directory)))) +} + export function disposeRuntime() { return runtime.dispose() } diff --git a/packages/opencode/src/lsp/index.ts b/packages/opencode/src/lsp/index.ts index 2eb1ad93e98..3b8b749ba12 100644 --- a/packages/opencode/src/lsp/index.ts +++ b/packages/opencode/src/lsp/index.ts @@ -1,5 +1,7 @@ import { BusEvent } from "@/bus/bus-event" import { Bus } from "@/bus" +import { InstanceContext } from "@/effect/instance-context" +import { runPromiseInstance } from "@/effect/runtime" import { Log } from "../util/log" import { LSPClient } from "./client" import path from "path" @@ -7,10 +9,10 @@ import { pathToFileURL, fileURLToPath } from "url" import { LSPServer } from "./server" import z from "zod" import { Config } from "../config/config" -import { Instance } from "../project/instance" import { Flag } from "@/flag/flag" import { Process } from "../util/process" import { spawn as lspspawn } from "./launch" +import { Effect, Fiber, Layer, ServiceMap } from "effect" export namespace LSP { const log = Log.create({ service: "lsp" }) @@ -77,77 +79,6 @@ export namespace LSP { } } - const state = Instance.state( - async () => { - const clients: LSPClient.Info[] = [] - const servers: Record = {} - const cfg = await Config.get() - - if (cfg.lsp === false) { - log.info("all LSPs are disabled") - return { - broken: new Set(), - servers, - clients, - spawning: new Map>(), - } - } - - for (const server of Object.values(LSPServer)) { - servers[server.id] = server - } - - filterExperimentalServers(servers) - - for (const [name, item] of Object.entries(cfg.lsp ?? {})) { - const existing = servers[name] - if (item.disabled) { - log.info(`LSP server ${name} is disabled`) - delete servers[name] - continue - } - servers[name] = { - ...existing, - id: name, - root: existing?.root ?? (async () => Instance.directory), - extensions: item.extensions ?? existing?.extensions ?? [], - spawn: async (root) => { - return { - process: lspspawn(item.command[0], item.command.slice(1), { - cwd: root, - env: { - ...process.env, - ...item.env, - }, - }), - initialization: item.initialization, - } - }, - } - } - - log.info("enabled LSP servers", { - serverIds: Object.values(servers) - .map((server) => server.id) - .join(", "), - }) - - return { - broken: new Set(), - servers, - clients, - spawning: new Map>(), - } - }, - async (state) => { - await Promise.all(state.clients.map((client) => client.shutdown())) - }, - ) - - export async function init() { - return state() - } - export const Status = z .object({ id: z.string(), @@ -160,162 +91,6 @@ export namespace LSP { }) export type Status = z.infer - export async function status() { - return state().then((x) => { - const result: Status[] = [] - for (const client of x.clients) { - result.push({ - id: client.serverID, - name: x.servers[client.serverID].id, - root: path.relative(Instance.directory, client.root), - status: "connected", - }) - } - return result - }) - } - - async function getClients(file: string) { - const s = await state() - const extension = path.parse(file).ext || file - const result: LSPClient.Info[] = [] - - async function schedule(server: LSPServer.Info, root: string, key: string) { - const handle = await server - .spawn(root) - .then((value) => { - if (!value) s.broken.add(key) - return value - }) - .catch((err) => { - s.broken.add(key) - log.error(`Failed to spawn LSP server ${server.id}`, { error: err }) - return undefined - }) - - if (!handle) return undefined - log.info("spawned lsp server", { serverID: server.id }) - - const client = await LSPClient.create({ - serverID: server.id, - server: handle, - root, - }).catch(async (err) => { - s.broken.add(key) - await Process.stop(handle.process) - log.error(`Failed to initialize LSP client ${server.id}`, { error: err }) - return undefined - }) - - if (!client) { - return undefined - } - - const existing = s.clients.find((x) => x.root === root && x.serverID === server.id) - if (existing) { - await Process.stop(handle.process) - return existing - } - - s.clients.push(client) - return client - } - - for (const server of Object.values(s.servers)) { - if (server.extensions.length && !server.extensions.includes(extension)) continue - - const root = await server.root(file) - if (!root) continue - if (s.broken.has(root + server.id)) continue - - const match = s.clients.find((x) => x.root === root && x.serverID === server.id) - if (match) { - result.push(match) - continue - } - - const inflight = s.spawning.get(root + server.id) - if (inflight) { - const client = await inflight - if (!client) continue - result.push(client) - continue - } - - const task = schedule(server, root, root + server.id) - s.spawning.set(root + server.id, task) - - task.finally(() => { - if (s.spawning.get(root + server.id) === task) { - s.spawning.delete(root + server.id) - } - }) - - const client = await task - if (!client) continue - - result.push(client) - Bus.publish(Event.Updated, {}) - } - - return result - } - - export async function hasClients(file: string) { - const s = await state() - const extension = path.parse(file).ext || file - for (const server of Object.values(s.servers)) { - if (server.extensions.length && !server.extensions.includes(extension)) continue - const root = await server.root(file) - if (!root) continue - if (s.broken.has(root + server.id)) continue - return true - } - return false - } - - export async function touchFile(input: string, waitForDiagnostics?: boolean) { - log.info("touching file", { file: input }) - const clients = await getClients(input) - await Promise.all( - clients.map(async (client) => { - const wait = waitForDiagnostics ? client.waitForDiagnostics({ path: input }) : Promise.resolve() - await client.notify.open({ path: input }) - return wait - }), - ).catch((err) => { - log.error("failed to touch file", { err, file: input }) - }) - } - - export async function diagnostics() { - const results: Record = {} - for (const result of await runAll(async (client) => client.diagnostics)) { - for (const [path, diagnostics] of result.entries()) { - const arr = results[path] || [] - arr.push(...diagnostics) - results[path] = arr - } - } - return results - } - - export async function hover(input: { file: string; line: number; character: number }) { - return run(input.file, (client) => { - return client.connection - .sendRequest("textDocument/hover", { - textDocument: { - uri: pathToFileURL(input.file).href, - }, - position: { - line: input.line, - character: input.character, - }, - }) - .catch(() => null) - }) - } - enum SymbolKind { File = 1, Module = 2, @@ -356,114 +131,504 @@ export namespace LSP { SymbolKind.Enum, ] - export async function workspaceSymbol(query: string) { - return runAll((client) => - client.connection - .sendRequest("workspace/symbol", { - query, + export interface Interface { + readonly init: () => Effect.Effect + readonly status: () => Effect.Effect + readonly hasClients: (file: string) => Effect.Effect + readonly touchFile: (input: string, waitForDiagnostics?: boolean) => Effect.Effect + readonly diagnostics: () => Effect.Effect> + readonly hover: (input: { file: string; line: number; character: number }) => Effect.Effect + readonly workspaceSymbol: (query: string) => Effect.Effect + readonly documentSymbol: (uri: string) => Effect.Effect<(LSP.DocumentSymbol | LSP.Symbol)[]> + readonly definition: (input: { file: string; line: number; character: number }) => Effect.Effect + readonly references: (input: { file: string; line: number; character: number }) => Effect.Effect + readonly implementation: (input: { file: string; line: number; character: number }) => Effect.Effect + readonly prepareCallHierarchy: (input: { file: string; line: number; character: number }) => Effect.Effect + readonly incomingCalls: (input: { file: string; line: number; character: number }) => Effect.Effect + readonly outgoingCalls: (input: { file: string; line: number; character: number }) => Effect.Effect + } + + export class Service extends ServiceMap.Service()("@opencode/LSP") {} + + export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const instance = yield* InstanceContext + const directory = instance.directory + + const clients: LSPClient.Info[] = [] + const servers: Record = {} + const broken = new Set() + const spawning = new Map>() + + // Load server configs lazily — forkScoped so it doesn't block layer construction + const load = Effect.fn("LSP.load")(function* () { + yield* Effect.promise(async () => { + const cfg = await Config.get() + + if (cfg.lsp === false) { + log.info("all LSPs are disabled") + return + } + + for (const server of Object.values(LSPServer)) { + servers[server.id] = server + } + + filterExperimentalServers(servers) + + for (const [name, item] of Object.entries(cfg.lsp ?? {})) { + const existing = servers[name] + if (item.disabled) { + log.info(`LSP server ${name} is disabled`) + delete servers[name] + continue + } + servers[name] = { + ...existing, + id: name, + root: existing?.root ?? (async () => directory), + extensions: item.extensions ?? existing?.extensions ?? [], + spawn: async (root) => { + return { + process: lspspawn(item.command[0], item.command.slice(1), { + cwd: root, + env: { + ...process.env, + ...item.env, + }, + }), + initialization: item.initialization, + } + }, + } + } + + log.info("enabled LSP servers", { + serverIds: Object.values(servers) + .map((server) => server.id) + .join(", "), + }) + }) + }) + + const loadFiber = yield* load().pipe( + Effect.catchCause((cause) => Effect.sync(() => log.error("init failed", { cause }))), + Effect.forkScoped, + ) + + // Cleanup: shut down all LSP clients when the scope is closed + yield* Effect.addFinalizer(() => + Effect.promise(async () => { + await Promise.all(clients.map((client) => client.shutdown())) + }), + ) + + async function getClientsForFile(file: string) { + const extension = path.parse(file).ext || file + const result: LSPClient.Info[] = [] + + async function schedule(server: LSPServer.Info, root: string, key: string) { + const handle = await server + .spawn(root) + .then((value) => { + if (!value) broken.add(key) + return value + }) + .catch((err) => { + broken.add(key) + log.error(`Failed to spawn LSP server ${server.id}`, { error: err }) + return undefined + }) + + if (!handle) return undefined + log.info("spawned lsp server", { serverID: server.id }) + + const client = await LSPClient.create({ + serverID: server.id, + server: handle, + root, + }).catch(async (err) => { + broken.add(key) + await Process.stop(handle.process) + log.error(`Failed to initialize LSP client ${server.id}`, { error: err }) + return undefined + }) + + if (!client) { + return undefined + } + + const existing = clients.find((x) => x.root === root && x.serverID === server.id) + if (existing) { + await Process.stop(handle.process) + return existing + } + + clients.push(client) + return client + } + + for (const server of Object.values(servers)) { + if (server.extensions.length && !server.extensions.includes(extension)) continue + + const root = await server.root(file) + if (!root) continue + if (broken.has(root + server.id)) continue + + const match = clients.find((x) => x.root === root && x.serverID === server.id) + if (match) { + result.push(match) + continue + } + + const inflight = spawning.get(root + server.id) + if (inflight) { + const client = await inflight + if (!client) continue + result.push(client) + continue + } + + const task = schedule(server, root, root + server.id) + spawning.set(root + server.id, task) + + task.finally(() => { + if (spawning.get(root + server.id) === task) { + spawning.delete(root + server.id) + } + }) + + const client = await task + if (!client) continue + + result.push(client) + Bus.publish(Event.Updated, {}) + } + + return result + } + + async function runAllClients(input: (client: LSPClient.Info) => Promise): Promise { + const tasks = clients.map((x) => input(x)) + return Promise.all(tasks) + } + + async function runForFile(file: string, input: (client: LSPClient.Info) => Promise): Promise { + const matched = await getClientsForFile(file) + const tasks = matched.map((x) => input(x)) + return Promise.all(tasks) + } + + const init = Effect.fn("LSP.init")(function* () { + yield* Fiber.join(loadFiber) + }) + + const status = Effect.fn("LSP.status")(function* () { + yield* Fiber.join(loadFiber) + const result: Status[] = [] + for (const client of clients) { + result.push({ + id: client.serverID, + name: servers[client.serverID].id, + root: path.relative(directory, client.root), + status: "connected", + }) + } + return result + }) + + const hasClients = Effect.fn("LSP.hasClients")(function* (file: string) { + yield* Fiber.join(loadFiber) + return yield* Effect.promise(async () => { + const extension = path.parse(file).ext || file + for (const server of Object.values(servers)) { + if (server.extensions.length && !server.extensions.includes(extension)) continue + const root = await server.root(file) + if (!root) continue + if (broken.has(root + server.id)) continue + return true + } + return false + }) + }) + + const touchFile = Effect.fn("LSP.touchFile")(function* (input: string, waitForDiagnostics?: boolean) { + yield* Fiber.join(loadFiber) + yield* Effect.promise(async () => { + log.info("touching file", { file: input }) + const matched = await getClientsForFile(input) + await Promise.all( + matched.map(async (client) => { + const wait = waitForDiagnostics ? client.waitForDiagnostics({ path: input }) : Promise.resolve() + await client.notify.open({ path: input }) + return wait + }), + ).catch((err) => { + log.error("failed to touch file", { err, file: input }) + }) + }) + }) + + const diagnostics = Effect.fn("LSP.diagnostics")(function* () { + yield* Fiber.join(loadFiber) + return yield* Effect.promise(async () => { + const results: Record = {} + for (const result of await runAllClients(async (client) => client.diagnostics)) { + for (const [path, diagnostics] of result.entries()) { + const arr = results[path] || [] + arr.push(...diagnostics) + results[path] = arr + } + } + return results + }) + }) + + const hover = Effect.fn("LSP.hover")(function* (input: { file: string; line: number; character: number }) { + yield* Fiber.join(loadFiber) + return yield* Effect.promise(async () => { + return runForFile(input.file, (client) => { + return client.connection + .sendRequest("textDocument/hover", { + textDocument: { + uri: pathToFileURL(input.file).href, + }, + position: { + line: input.line, + character: input.character, + }, + }) + .catch(() => null) + }) + }) + }) + + const workspaceSymbol = Effect.fn("LSP.workspaceSymbol")(function* (query: string) { + yield* Fiber.join(loadFiber) + return yield* Effect.promise(async () => { + return runAllClients((client) => + client.connection + .sendRequest("workspace/symbol", { + query, + }) + .then((result: any) => result.filter((x: LSP.Symbol) => kinds.includes(x.kind))) + .then((result: any) => result.slice(0, 10)) + .catch(() => []), + ).then((result) => result.flat() as LSP.Symbol[]) + }) + }) + + const documentSymbol = Effect.fn("LSP.documentSymbol")(function* (uri: string) { + yield* Fiber.join(loadFiber) + return yield* Effect.promise(async () => { + const file = fileURLToPath(uri) + return runForFile(file, (client) => + client.connection + .sendRequest("textDocument/documentSymbol", { + textDocument: { + uri, + }, + }) + .catch(() => []), + ) + .then((result) => result.flat() as (LSP.DocumentSymbol | LSP.Symbol)[]) + .then((result) => result.filter(Boolean)) + }) + }) + + const definition = Effect.fn("LSP.definition")(function* (input: { + file: string + line: number + character: number + }) { + yield* Fiber.join(loadFiber) + return yield* Effect.promise(async () => { + return runForFile(input.file, (client) => + client.connection + .sendRequest("textDocument/definition", { + textDocument: { uri: pathToFileURL(input.file).href }, + position: { line: input.line, character: input.character }, + }) + .catch(() => null), + ).then((result) => result.flat().filter(Boolean)) + }) + }) + + const references = Effect.fn("LSP.references")(function* (input: { + file: string + line: number + character: number + }) { + yield* Fiber.join(loadFiber) + return yield* Effect.promise(async () => { + return runForFile(input.file, (client) => + client.connection + .sendRequest("textDocument/references", { + textDocument: { uri: pathToFileURL(input.file).href }, + position: { line: input.line, character: input.character }, + context: { includeDeclaration: true }, + }) + .catch(() => []), + ).then((result) => result.flat().filter(Boolean)) + }) + }) + + const implementation = Effect.fn("LSP.implementation")(function* (input: { + file: string + line: number + character: number + }) { + yield* Fiber.join(loadFiber) + return yield* Effect.promise(async () => { + return runForFile(input.file, (client) => + client.connection + .sendRequest("textDocument/implementation", { + textDocument: { uri: pathToFileURL(input.file).href }, + position: { line: input.line, character: input.character }, + }) + .catch(() => null), + ).then((result) => result.flat().filter(Boolean)) }) - .then((result: any) => result.filter((x: LSP.Symbol) => kinds.includes(x.kind))) - .then((result: any) => result.slice(0, 10)) - .catch(() => []), - ).then((result) => result.flat() as LSP.Symbol[]) + }) + + const prepareCallHierarchy = Effect.fn("LSP.prepareCallHierarchy")(function* (input: { + file: string + line: number + character: number + }) { + yield* Fiber.join(loadFiber) + return yield* Effect.promise(async () => { + return runForFile(input.file, (client) => + client.connection + .sendRequest("textDocument/prepareCallHierarchy", { + textDocument: { uri: pathToFileURL(input.file).href }, + position: { line: input.line, character: input.character }, + }) + .catch(() => []), + ).then((result) => result.flat().filter(Boolean)) + }) + }) + + const incomingCalls = Effect.fn("LSP.incomingCalls")(function* (input: { + file: string + line: number + character: number + }) { + yield* Fiber.join(loadFiber) + return yield* Effect.promise(async () => { + return runForFile(input.file, async (client) => { + const items = (await client.connection + .sendRequest("textDocument/prepareCallHierarchy", { + textDocument: { uri: pathToFileURL(input.file).href }, + position: { line: input.line, character: input.character }, + }) + .catch(() => [])) as any[] + if (!items?.length) return [] + return client.connection + .sendRequest("callHierarchy/incomingCalls", { item: items[0] }) + .catch(() => []) + }).then((result) => result.flat().filter(Boolean)) + }) + }) + + const outgoingCalls = Effect.fn("LSP.outgoingCalls")(function* (input: { + file: string + line: number + character: number + }) { + yield* Fiber.join(loadFiber) + return yield* Effect.promise(async () => { + return runForFile(input.file, async (client) => { + const items = (await client.connection + .sendRequest("textDocument/prepareCallHierarchy", { + textDocument: { uri: pathToFileURL(input.file).href }, + position: { line: input.line, character: input.character }, + }) + .catch(() => [])) as any[] + if (!items?.length) return [] + return client.connection + .sendRequest("callHierarchy/outgoingCalls", { item: items[0] }) + .catch(() => []) + }).then((result) => result.flat().filter(Boolean)) + }) + }) + + log.info("init") + return Service.of({ + init, + status, + hasClients, + touchFile, + diagnostics, + hover, + workspaceSymbol, + documentSymbol, + definition, + references, + implementation, + prepareCallHierarchy, + incomingCalls, + outgoingCalls, + }) + }), + ) + + // Async facades + export async function init() { + return runPromiseInstance(Service.use((svc) => svc.init())) + } + + export async function status() { + return runPromiseInstance(Service.use((svc) => svc.status())) + } + + export async function hasClients(file: string) { + return runPromiseInstance(Service.use((svc) => svc.hasClients(file))) + } + + export async function touchFile(input: string, waitForDiagnostics?: boolean) { + return runPromiseInstance(Service.use((svc) => svc.touchFile(input, waitForDiagnostics))) + } + + export async function diagnostics() { + return runPromiseInstance(Service.use((svc) => svc.diagnostics())) + } + + export async function hover(input: { file: string; line: number; character: number }) { + return runPromiseInstance(Service.use((svc) => svc.hover(input))) + } + + export async function workspaceSymbol(query: string) { + return runPromiseInstance(Service.use((svc) => svc.workspaceSymbol(query))) } export async function documentSymbol(uri: string) { - const file = fileURLToPath(uri) - return run(file, (client) => - client.connection - .sendRequest("textDocument/documentSymbol", { - textDocument: { - uri, - }, - }) - .catch(() => []), - ) - .then((result) => result.flat() as (LSP.DocumentSymbol | LSP.Symbol)[]) - .then((result) => result.filter(Boolean)) + return runPromiseInstance(Service.use((svc) => svc.documentSymbol(uri))) } export async function definition(input: { file: string; line: number; character: number }) { - return run(input.file, (client) => - client.connection - .sendRequest("textDocument/definition", { - textDocument: { uri: pathToFileURL(input.file).href }, - position: { line: input.line, character: input.character }, - }) - .catch(() => null), - ).then((result) => result.flat().filter(Boolean)) + return runPromiseInstance(Service.use((svc) => svc.definition(input))) } export async function references(input: { file: string; line: number; character: number }) { - return run(input.file, (client) => - client.connection - .sendRequest("textDocument/references", { - textDocument: { uri: pathToFileURL(input.file).href }, - position: { line: input.line, character: input.character }, - context: { includeDeclaration: true }, - }) - .catch(() => []), - ).then((result) => result.flat().filter(Boolean)) + return runPromiseInstance(Service.use((svc) => svc.references(input))) } export async function implementation(input: { file: string; line: number; character: number }) { - return run(input.file, (client) => - client.connection - .sendRequest("textDocument/implementation", { - textDocument: { uri: pathToFileURL(input.file).href }, - position: { line: input.line, character: input.character }, - }) - .catch(() => null), - ).then((result) => result.flat().filter(Boolean)) + return runPromiseInstance(Service.use((svc) => svc.implementation(input))) } export async function prepareCallHierarchy(input: { file: string; line: number; character: number }) { - return run(input.file, (client) => - client.connection - .sendRequest("textDocument/prepareCallHierarchy", { - textDocument: { uri: pathToFileURL(input.file).href }, - position: { line: input.line, character: input.character }, - }) - .catch(() => []), - ).then((result) => result.flat().filter(Boolean)) + return runPromiseInstance(Service.use((svc) => svc.prepareCallHierarchy(input))) } export async function incomingCalls(input: { file: string; line: number; character: number }) { - return run(input.file, async (client) => { - const items = (await client.connection - .sendRequest("textDocument/prepareCallHierarchy", { - textDocument: { uri: pathToFileURL(input.file).href }, - position: { line: input.line, character: input.character }, - }) - .catch(() => [])) as any[] - if (!items?.length) return [] - return client.connection.sendRequest("callHierarchy/incomingCalls", { item: items[0] }).catch(() => []) - }).then((result) => result.flat().filter(Boolean)) + return runPromiseInstance(Service.use((svc) => svc.incomingCalls(input))) } export async function outgoingCalls(input: { file: string; line: number; character: number }) { - return run(input.file, async (client) => { - const items = (await client.connection - .sendRequest("textDocument/prepareCallHierarchy", { - textDocument: { uri: pathToFileURL(input.file).href }, - position: { line: input.line, character: input.character }, - }) - .catch(() => [])) as any[] - if (!items?.length) return [] - return client.connection.sendRequest("callHierarchy/outgoingCalls", { item: items[0] }).catch(() => []) - }).then((result) => result.flat().filter(Boolean)) - } - - async function runAll(input: (client: LSPClient.Info) => Promise): Promise { - const clients = await state().then((x) => x.clients) - const tasks = clients.map((x) => input(x)) - return Promise.all(tasks) - } - - async function run(file: string, input: (client: LSPClient.Info) => Promise): Promise { - const clients = await getClients(file) - const tasks = clients.map((x) => input(x)) - return Promise.all(tasks) + return runPromiseInstance(Service.use((svc) => svc.outgoingCalls(input))) } export namespace Diagnostic {