UNPKG

the-moby-effect

Version:
194 lines (171 loc) 7.21 kB
import type * as Scope from "effect/Scope"; import type * as HttpClient from "effect/unstable/http/HttpClient"; import * as Effect from "effect/Effect"; import * as Function from "effect/Function"; import * as Layer from "effect/Layer"; import * as Socket from "effect/unstable/socket/Socket"; import type * as net from "node:net"; import type * as MobyConnection from "../../MobyConnection.js"; import type * as ssh2 from "ssh2"; import type * as undici from "undici"; import * as internalAgnostic from "./agnostic.js"; import * as internalConnection from "./connection.js"; /** @internal */ export const makeDispatcher: ( agentOptions?: undici.Agent.Options | undefined ) => Effect.Effect<undici.Agent, never, Scope.Scope> = Effect.fnUntraced(function* ( agentOptions?: undici.Agent.Options | undefined ) { const undiciLazy = yield* Effect.promise(() => import("undici")); const acquire = Effect.sync(() => new undiciLazy.Agent(agentOptions)); const release = (agent: undici.Dispatcher) => Effect.promise(() => agent.destroy()); const resource = Effect.acquireRelease(acquire, release); return yield* resource; }); /** @internal */ export const makeSshDispatcher: ( connectionOptions: MobyConnection.SshConnectionOptions ) => Effect.Effect<undici.Agent, never, Scope.Scope> = Effect.fnUntraced(function* ( connectionOptions: MobyConnection.SshConnectionOptions ) { const ssh2Lazy = yield* Effect.promise(() => import("ssh2")); const undiciLazy = yield* Effect.promise(() => import("undici")); const sshClient = new ssh2Lazy.Client(); let sshConnection: "unopened" | "connecting" | "open-failed" | "ready" = "unopened"; let openFailedError: Error | null = null; const connector: undici.buildConnector.connector = ( // oxlint-disable-next-line only-used-in-recursion _options: undici.buildConnector.Options, callback: undici.buildConnector.Callback ) => { const onError = (error: Error): void => callback(error, null); // No connection yet, start connecting if (sshConnection === "unopened") { sshConnection = "connecting"; const unableToOpen = (error: Error & ssh2.ClientErrorExtensions) => { sshConnection = "open-failed"; openFailedError = error; onError(error); }; sshClient .once("ready", () => { sshConnection = "ready"; sshClient.off("error", unableToOpen); connector(_options, callback); }) .once("error", unableToOpen) .connect(connectionOptions); } // Already tried to connect but failed else if (sshConnection === "open-failed") { callback(openFailedError!, null); } // Another connection attempt is already in progress, wait for it else if (sshConnection === "connecting") { sshClient .once("ready", () => { sshClient.off("error", onError); connector(_options, callback); }) .once("error", onError); } // We are connected to the ssh server, now forward our stream local else if (sshConnection === "ready") { sshClient .openssh_forwardOutStreamLocal( connectionOptions.remoteSocketPath, (error: Error | undefined, stream: ssh2.ClientChannel) => { sshClient.off("error", onError); if (error) return callback(error, null); else return callback(null, stream as unknown as net.Socket); } ) .once("error", onError); } // Should never happen else { return Function.absurd<undefined>(sshConnection); } }; const acquire = Effect.sync(() => new undiciLazy.Agent({ connect: connector })); const release = (_agent: undici.Dispatcher) => Effect.promise(async () => { sshClient.end(); sshClient.destroy(); // await agent.close(); // await agent.destroy(); }); const resource = Effect.acquireRelease(acquire, release); return yield* resource; }); /** @internal */ export const getUndiciDispatcher: ( connectionOptions: MobyConnection.MobyConnectionOptions ) => Effect.Effect<undici.Dispatcher, never, Scope.Scope> = internalConnection.MobyConnectionOptions.$match({ ssh: (options) => makeSshDispatcher(options), socket: (options) => makeDispatcher({ connect: { socketPath: options.socketPath, }, }), http: (options) => makeDispatcher({ connect: { host: options.host, port: options.port, path: options.path, }, }), https: (options) => makeDispatcher({ connect: { ca: options.ca, key: options.key, cert: options.cert, passphrase: options.passphrase, host: options.host, port: options.port, path: options.path, }, }), }); /** @internal */ export const getUndiciWebsocketConstructor = ( connectionOptions: MobyConnection.MobyConnectionOptions ): Effect.Effect<(url: string, protocols?: string | Array<string> | undefined) => globalThis.WebSocket, never, never> => Function.pipe( Effect.promise(() => import("ws")), Effect.map((ws) => { const prependedUrl = internalAgnostic.makeWebsocketRequestUrl(connectionOptions); return (url: string, protocols?: string | Array<string> | undefined) => new ws.WebSocket(`${prependedUrl}${url}`, protocols) as unknown as globalThis.WebSocket; }) ); /** @internal */ export const makeUndiciHttpClientLayer = ( connectionOptions: MobyConnection.MobyConnectionOptions ): Layer.Layer<HttpClient.HttpClient | Socket.WebSocketConstructor, never, never> => { const dispatcherLive = Function.pipe( Effect.promise(() => import("@effect/platform-node/NodeHttpClient")), Effect.map((nodeHttpClientLazy) => Layer.effect(nodeHttpClientLazy.Dispatcher, getUndiciDispatcher(connectionOptions)) ), Layer.unwrap ); const websocketConstructorLive = Layer.effect( Socket.WebSocketConstructor, getUndiciWebsocketConstructor(connectionOptions) ); const undiciHttpClientLive = Function.pipe( Effect.promise(() => import("@effect/platform-node/NodeHttpClient")), Effect.map((nodeHttpClientLazy) => nodeHttpClientLazy.layerUndiciNoDispatcher), Layer.unwrap ); const agnosticHttpClientLayer = internalAgnostic.makeAgnosticHttpClientLayer(connectionOptions); return agnosticHttpClientLayer.pipe( Layer.merge(websocketConstructorLive), Layer.provide(undiciHttpClientLive), Layer.provide(dispatcherLive) ); };