| 1 | import type { DesktopBridge, DesktopTerminalConnection } from "@t3tools/contracts"; |
| 2 | import * as Deferred from "effect/Deferred"; |
| 3 | import * as Effect from "effect/Effect"; |
| 4 | import * as Queue from "effect/Queue"; |
| 5 | import * as Socket from "effect/socket/Socket"; |
| 6 | import type { PreparedConnection } from "../connection/model.ts"; |
| 7 | |
| 8 | export function desktopTerminalSocket( |
| 9 | bridge: Pick<DesktopBridge, "supportsLocalTerminal" | "openLocalTerminal"> | undefined, |
| 10 | prepared: PreparedConnection, |
| 11 | ): Socket.Socket | undefined { |
| 12 | if (!bridge?.openLocalTerminal || bridge.supportsLocalTerminal?.(prepared) !== true) return; |
| 13 | const open = bridge.openLocalTerminal; |
| 14 | const connected = Deferred.makeUnsafe<DesktopTerminalConnection>(); |
| 15 | const closed = () => |
| 16 | new Socket.SocketError({ reason: new Socket.SocketCloseError({ code: 1001 }) }); |
| 17 | return { |
| 18 | [Socket.TypeId]: Socket.TypeId, |
| 19 | reader: Effect.gen(function* () { |
| 20 | const queue = yield* Queue.unbounded<string, Socket.SocketError>(); |
| 21 | const run = Effect.runForkWith(yield* Effect.context<never>()); |
| 22 | yield* Effect.acquireRelease( |
| 23 | Effect.sync(() => { |
| 24 | const connection = open( |
| 25 | { environmentId: prepared.environmentId, url: prepared.socketUrl }, |
| 26 | (data) => { |
| 27 | run(Queue.offer(queue, data)); |
| 28 | }, |
| 29 | () => { |
| 30 | run(Queue.fail(queue, closed())); |
| 31 | }, |
| 32 | ); |
| 33 | run(Deferred.succeed(connected, connection)); |
| 34 | return connection; |
| 35 | }), |
| 36 | (connection) => |
| 37 | Effect.sync(connection.close).pipe(Effect.andThen(Queue.fail(queue, closed()))), |
| 38 | ); |
| 39 | return { |
| 40 | pull: Queue.take(queue).pipe(Effect.map((data) => [data] as const)), |
| 41 | upgrade: () => |
| 42 | Effect.fail(new Socket.SocketError({ reason: new Socket.SocketUpgradeError({}) })), |
| 43 | }; |
| 44 | }), |
| 45 | writer: Effect.succeed({ |
| 46 | write: (data) => |
| 47 | Deferred.await(connected).pipe( |
| 48 | Effect.flatMap((connection) => |
| 49 | Effect.sync(() => { |
| 50 | if (Socket.isCloseEvent(data)) connection.close(); |
| 51 | else |
| 52 | connection.send(typeof data === "string" ? data : new TextDecoder().decode(data)); |
| 53 | }), |
| 54 | ), |
| 55 | ), |
| 56 | writeAll: (chunks) => |
| 57 | Deferred.await(connected).pipe( |
| 58 | Effect.flatMap((connection) => |
| 59 | Effect.sync(() => { |
| 60 | for (const data of chunks) |
| 61 | connection.send(typeof data === "string" ? data : new TextDecoder().decode(data)); |
| 62 | }), |
| 63 | ), |
| 64 | ), |
| 65 | }), |
| 66 | }; |
| 67 | } |